/* =============================================================================
   SDEIRA - SHARED PAGE SECTIONS
   =============================================================================

   The component library every generated page draws from. Loaded AFTER
   sdeira-subpage.css and before any page sheet, so a page tweak wins on source
   order alone and nothing here ever needs !important.

   WHAT BELONGS HERE

   A component used on two or more pages. One page's bespoke layout does not
   belong here - it belongs in assets/css/page-<slug>.css, and if a second such
   file is ever proposed that is a signal the component set is wrong (see
   docs/component-build-architecture.md section 2.5).

   THE RULES THIS FILE OBEYS, same as sdeira-subpage.css

   - No @layer, no !important, no box-shadow.
   - One border-radius: var(--r). Never a literal.
   - letter-spacing: 0 everywhere - Arabic is joined cursive.
   - RESERVE, DO NOT CLAMP. A card reserves height with
     min-block-size: calc(N * 1lh); it never hides copy with -webkit-line-clamp.
   - Every selector is defined ONCE. All breakpoint work is collected in the
     RESPONSIVE section at the end, each rule saying what it changes.
   - Decorative geometry uses PHYSICAL coordinates (top/left), not logical ones,
     with an explicit [dir] pair authored in the same commit. A real failure is
     on record where logical properties dropped decoration on top of the text of
     an RTL page.
   - Every colour, size, duration and easing comes from sdeira-tokens.css. No
     custom property defined here shadows one that exists there.

   DERIVATION

   Each component below names the LIVE element whose measured computed values it
   was authored from. Nothing here was copied out of sdeira-v2.css or
   sdeira-v2-en.css: those files define .hero 51 times across 107 media blocks
   and source order decides the winner, so a lifted declaration carries the
   markup and not the behaviour.

   CONTENTS
     1.  Section surfaces and shared bits
     2.  C1  .split          text + media, two columns
     3.  C2  .stat-strip     figure rows
     4.  C3  .icon-grid      icon + title + line
     5.  C4  .step-list      numbered steps and the timeline variant
     6.  C5  .row-list       one-line rows: routes, downloads, contacts
     7.  C7  .logo-wall      logo tiles
     8.  C8  .people-grid    portraits
     9.  C9  .spec-table     rows of specification
     9b. C10 .gallery        + .lightbox
     9c. C11 .map-panel      pinned static map
     9d. C14 .faq            native details/summary
     9e. C12 .cta-band       closing call to action
     9f. C13 .photo-band     mid-page photograph, glass copy block, no actions
     10. C16 .contact-details
     10b.C17 .org-diagram    a hierarchy drawn from the record
     11. Pending markers
     12. RESPONSIVE
     13. Reduced motion
   ========================================================================== */


/* =============================================================================
   1. SECTION SURFACES AND SHARED BITS
   ========================================================================== */

/* .pad in sdeira-subpage.css hardcodes background: var(--surface-base), so a
   section cannot alternate its ground without a second class. These are the
   only four grounds any page uses, and naming them here stops each page
   inventing its own. `.pad--flush` exists because a hero-adjacent band
   sometimes has to sit against the section above it with no seam. */
.pad--raised { background: var(--surface-raised); }
/* A card on the raised ground takes the next rung down, or it disappears
   into its own section.
   .news-item-body ADDED 4 Sep 2026. The Liwa migration E1 pass below
   (search --surface-sunken) corrected .news-item, .news-card,
   .project-card-base AND .project-card-copy, but never .news-item-body,
   which sdeira-subpage.css leaves on --surface-raised. On a white
   pad--raised section that panel is white on white: the photograph and its
   caption float with no card around them and a void where the body should
   be. No group page pairs a bare .news-item with a raised ground, which is
   why the gap survived; four Khadamat routes do, in both languages, and
   they were the pages a reviewer called unfinished. Verified before
   changing: zero group pages have .news-item-body inside a pad--raised
   section, so this is inert everywhere except where it is the fix. */
.pad--raised :is(.row-item, .stat-strip, .logo-tile, .faq-item, .icon-item, .news-item-body) { background: var(--surface-sunken) !important; }
/* NO LOCAL --wipe-fill OVERRIDE HERE, 31 Aug 2026. This used to force
   --surface-sunken-strong (near-white on the raised ground's own sunken
   card), a leftover from before .row-item's own hover was made Liwa
   (--accent-deep, 27 Aug 2026, see the docstring at .row-item::before
   below). .row-item already flips its text to white on hover regardless of
   ground, so white text over a near-white wipe read as almost no hover at
   all - the defect flagged in a preview comment on /business/investments.
   Removing the override lets every .row-item, on every ground, take the one
   shared Liwa wipe instead of a second, stale implementation. */
/* THE SAND GROUND CARRIES ITS OWN PATTERN. See section 5b of the token file:
   --surface-sunken is the lit half of a ground whose other half is
   --ground-sand, and a section takes both or neither. It is drawn here rather
   than left to each record so that asking for the colour and forgetting the
   lines is not a thing a record can do - which is exactly what every existing
   record had to remember, and why all 30 of them write `pad--sunken pad--field`
   in full. Those stay valid and render identically; the second class is now
   the default rather than the instruction.
   A section that declares one of the OTHER two treatments has chosen its
   ground deliberately and keeps it - .pad--edge and .pad--wedge are alternative
   patterns on this same colour, not decoration layered over this one, and both
   draw into the same ::before. */
.pad--sunken { background: var(--surface-sunken); }
.pad--inverse { background: var(--surface-inverse); }
.pad--flush { padding-block-start: 0; }

/* The documented three-band section-glass group. It is the full-section
   counterpart to the bands over a photograph: three shared identity bands,
   one origin, no repeated field texture. */
.section-glass {
  position: absolute;
  z-index: 0;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.section--glass {
  position: relative;
  isolation: isolate;
}

.section--glass > .wrap {
  position: relative;
  z-index: 1;
}

/* A grouped run lets one identity-glass field span adjacent semantic sections
   without turning their headings and landmarks into one oversized section. */
.section-group--glass {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--surface-base);
}

.section-group--glass > .pad {
  position: relative;
  z-index: 1;
  background: transparent;
}

.section-glass span {
  position: absolute;
  top: var(--identity-band-top);
  block-size: var(--identity-band-height);
  inline-size: var(--identity-band-width);
  inset-inline-start: calc(var(--band-origin, 68%) + var(--band-step, 0) * 10%);
  border: var(--identity-band-border) solid
    color-mix(in srgb, var(--identity-mark) var(--identity-band-line-ambient), transparent);
  border-radius: var(--r);
  background: color-mix(in srgb, var(--identity-mark) var(--identity-band-fill-ambient), transparent);
  transform: skewX(-24deg);
  animation: sectionBandDrift var(--dur-ambient) var(--ease-out) infinite alternate;
}

.section-glass .b1 { --band-step: 0; }
.section-glass .b2 { --band-step: 1; }
.section-glass .b3 { --band-step: 2; }
.section-glass--start { --band-origin: -4%; }
.section-glass--end { --band-origin: 68%; }

/* Photo-hosted section glass uses the same geometry but the on-photo identity
   tone and a static mark, matching .split-ribbon rather than flat-ground glass. */
.section-glass--photo span {
  border-color: color-mix(in srgb, var(--identity-mark) var(--identity-band-line-on-photo), transparent);
  background: color-mix(in srgb, var(--identity-mark) var(--identity-band-fill-on-photo), transparent);
  animation: none;
}

@keyframes sectionBandDrift {
  from { transform: skewX(-24deg) translate3d(0, 0, 0); }
  to { transform: skewX(-24deg) translate3d(0, -14px, 0); }
}

@media (max-width: 920px) {
  .section-glass { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .section-glass span { animation: none; }
}

/* On the dark ground every text role has to be restated. Half the token file's
   roles (--label, --figure, --pending, --muted) are authored for a light
   surface and fail contrast on --surface-inverse; the file documents an
   -on-dark counterpart for each, and this is the one place they are applied so
   no component has to remember. */
/* :where() on the wrapper keeps this at the same weight as a plain element
   selector, so `.pad--inverse .sec-head h2` below is what actually decides -
   see the note there. */
.pad--inverse :is(h2, h3, h4) { color: var(--brand-white); }

/* .sec-head h2 in sdeira-subpage.css sets --charcoal at (0,1,1), the exact
   weight of the rule above it, so white-on-dark was winning by source order
   alone. If a future edit ever loads that file second, every heading on a dark
   section becomes #414042 on #414042 - invisible, with nothing in the console.
   This wins on specificity instead, which no load order can undo. */
.pad--inverse .sec-head h2 { color: var(--brand-white); }
.pad--inverse p { color: var(--muted-on-dark); }
.pad--inverse .kicker { color: var(--label-on-dark); }
.pad--inverse .sec-head p { color: var(--muted-on-dark); }

/* The Sdeira mark on a kicker is a dark-ink PNG. On the inverse ground it
   disappears into the surface, so the band takes the accent instead - the same
   move the footer already makes. */
.pad--inverse .kicker::before { display: none; }

/* NO BORDERS IN THIS FILE. NOT ONE.
   The system separates surfaces by TONE - a raised surface on a base one - and
   it has no second mechanism: no shadow, no rule, no outline. Every hairline
   that was in this file (row separators, card outlines, table rules, field
   frames, the FAQ divider) has been replaced by a tonal step, which is the
   only separation vocabulary the design system actually owns.

   The one exception in the whole file is :focus-visible, which is an outline
   rather than a border and exists because a keyboard user has to be able to
   see where they are. It is an accessibility affordance, not a decoration.

   .btn-ghost is the site's ONE sanctioned border and it is sanctioned only
   over photography, where a white outline is the only thing giving a
   transparent control a hit area. It is therefore never emitted on a page
   section: components.py offers exactly two action levels, .btn-primary and
   .more, and neither carries a line. */

/* The card list's column count. .news-list is auto-fill, which is right for a
   feed of unknown length and wrong for a fixed set: four sector cards land
   three-up with one stranded underneath. A caller that knows how many cards it
   has says so; one that does not keeps the auto-fill behaviour untouched. */
.news-list--cols {
  grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr));
}

/* One project-card base across the homepage and generated business pages.
   Legacy family names remain as variants; media and copy geometry live here. */
.project-card-base {
  min-inline-size: 0;
}

.project-card-media {
  min-inline-size: 0;
}

.project-card-media img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

/* THE p.43 LINE MARK, ON PHOTOGRAPHY (Liwa migration D3,
   docs/content/liwa-migration.md; owner, 23 Aug 2026). The line set appears
   twice, diagonally opposite, the way the guideline's own cover uses it
   (the second instance is the first rotated 180deg so the 45deg slant stays
   one angle in both). PROMOTED here (26 Aug 2026 sub-page rollout) from
   assets/css/sdeira-liwa.css, which is homepage-only: .project-card-media and
   .business-needs-routes .needs-route-media are this file's own shared
   components (business/, careers/, companies/, projects/, property-development/
   all use them), so they were missing this mark on every page but the
   homepage. .about-visual stays in sdeira-liwa.css - it has no sub-page
   equivalent.

   PROJECT CARDS: single instance only, site owner 28 Aug 2026. The
   bottom-right ::after (still kept, unchanged, on .needs-route-media) crowded
   the same corner .card-chevron already occupies inside .project-card-copy
   below the photo - two marks reading as clutter in one corner while the
   photo's other corner carried nothing. .project-card-media keeps only the
   mirrored ::before (top-left, unaffected by this change) so the photo mark
   and the card-chevron sticker now sit in opposite corners of the card
   instead of stacking in the same one. */
.needs-route-media::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  inline-size: var(--photo-mark-width);
  aspect-ratio: var(--photo-mark-p43-ratio);
  background: url("../images/patterns/lines.svg?v=d874099296964b63") no-repeat right bottom / contain;
  pointer-events: none;
  z-index: 2;
  transform: scale(1);
  transform-origin: 100% 100%;
  transition: transform var(--dur-normal, 200ms) var(--ease-out, ease-out);
}

[dir="rtl"] #company-eskan .card-chevron::after,
[dir="rtl"] #company-khadamat .card-chevron::after{
    /* right: auto; */
    left: 0;
    transform-origin: 0% 100%;
    transform: translateX(100%) scale(-1, 1);
}

.needs-route-media::before,
.project-card--feature .project-card-img::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  inline-size: var(--photo-mark-width);
  aspect-ratio: var(--photo-mark-p43-ratio);
  background: url("../images/patterns/lines-rotated-180.svg?v=bc182f344a4d87fd") no-repeat left top / contain;
  pointer-events: none;
  z-index: 2;
  transform: scale(1);
  transform-origin: 0 0;
  transition: transform var(--dur-normal, 200ms) var(--ease-out, ease-out);
}
.needs-route:not(:where(.business-needs-route)):is(:hover, :focus-within) .needs-route-media::after { transform: scale(1.1); }
.needs-route:not(:where(.business-needs-route)):is(:hover, :focus-within) .needs-route-media::before { transform: scale(1.1); }
:is(.project-card, .project-card--secondary):is(:hover, :focus-within) .project-card-media::before { transform: scale(1.1); }
.project-card--feature:is(:hover, :focus-within) .project-card-img::before { transform: scale(1.1); }

/* ==================================================== THE PHOTO MARK MIRRORS
   IN RTL, 15 Sep 2026. Site owner's standing decision, applied here: the
   English page is the BASELINE and the Arabic twin matches it in count,
   images and presentation while the DIRECTION suits Arabic. Both marks above
   were anchored to fixed physical corners - the ::after to the photo's
   bottom-RIGHT, the ::before pair to its top LEFT - so on a card whose own
   layout does move with direction they stop being the card's corner accent.
   Measured on the Arabic homepage (1440, before this block): .needs-route puts
   the photograph in the RIGHT half in RTL, so its ::after landed on the
   photo's outer edge, and the copy panel's own mark ended up floating at the
   panel's middle-left beside its action link instead of in the panel's own
   far corner. The owner's p.43 rule is "anchored in the corner furthest from
   reading start", which is the physical LEFT in Arabic; docs/quality/
   p43-corner-mark-rtl-mirror.md states the same requirement, and the marks
   on .icon-grid--amenities already mirror for the same reason.

   WHY A TRANSLATE + SCALE PAIR AND NOT A BARE scaleX(-1). A plain scaleX(-1)
   mirrors only about the box's own centre, which is also the pivot the hover
   scale has to use, so the hover would grow from the middle of the mark
   instead of from the card's corner. Pivoting a bare scaleX(-1) at the
   anchored corner instead throws the whole box a full width off the card
   (a flip about the left edge lands the box one width to the left, and about
   the right edge one width to the right). Carrying both halves in one
   transform keeps the box exactly where the mirrored inset puts it, mirrors
   the artwork inside it, and keeps the hover growing outward from the
   mirrored corner:
     rest  : translateX(100%) scale(-1, 1)          mirror, in place
     hover : translateX(100% or 110%) scale(-1.1, 1.1)
   The translate is the box's OWN width - that is the displacement the mirror
   would otherwise introduce - and its value follows the anchored corner: a
   box anchored at its LEFT edge (.needs-route-media::after) needs 100% at
   rest and 110% hovered, a box anchored at its RIGHT edge (the ::before
   pair) needs 100% in both states. Both endpoints and the frames between
   them were checked against the English page in a headless browser: the
   Arabic box is the exact horizontal mirror of the English one at rest and
   at 1.1, with no drift at the pinned edge mid-transition.

   background-position is deliberately NOT restated. The artwork is mirrored
   by the transform, so the English rule's own `right bottom` / `left top`
   anchor mirrors with it; re-anchoring the background here would mirror
   twice and pull the mark back out of its corner. The origins below are the
   mirrored corner in the box's own coordinates: 0 100% for the left-anchored
   ::after (bottom-left), 0 0 for the right-anchored ::before pair, which is
   the reflection of the English box's own top-left edge.

   THE HOSTS ARE NAMED, NOT BLANKET. These rules cover the card families
   whose box the shared sheet itself moves with direction. .split-media,
   .subpage-hero--photo, .cta-glass, .faq-item, .logo-tile and .company-card
   are deliberately absent: their hosts do NOT change physical side in RTL,
   the 28 Aug 2026 rule in p43-corner-mark-rtl-mirror.md keeps their marks
   physical, and this change does not overturn it.

   TWO HOSTS ARE CARVED OUT OF THE ::before PAIR, both for the same reason: a
   mirror only works when the box's own edge and the artwork move together, and
   these two have their box placed and their origin set by someone else.

   1. `.project-card--feature .project-card-img::before` is moved to the
      photograph's bottom-right by sdeira-liwa.css on the two homepages (the
      shared base above puts it top-left, which is what every generated page
      gets). Its RTL rule therefore belongs in that same sheet, next to the
      rule that moves it; the shared rule below is the generated-page case.
   2. `[data-brand="zoad"]` keeps its own corner geometry: zoad-theme.css
      anchors these marks to a physical bottom-right with its own `inset`, its
      own `transform-origin: center`, its own dots artwork and its own rotate
      and mask, and has no RTL branch at all. Overriding the origin here while
      that sheet keeps the box where it is would leave the Zoad mark mirroring
      in place but growing from the wrong corner on hover, which is a worse
      outcome than leaving a brand's own documented corner alone. Measured
      before and after on a real Zoad page: unchanged, property for property. */
html[dir="rtl"] .needs-route-media::after {
  right: auto;
  left: 0;
  transform-origin: 0 100%;
  transform: translateX(100%) scale(-1, 1);
}

html[dir="rtl"]:not([data-brand="zoad"]) :is(.needs-route-media, .project-card-media)::before {
  left: auto;
  right: 0;
  transform-origin: 0 0;
  transform: translateX(100%) scale(-1, 1);
}

/* The generated pages' own case: the shared base anchors this one top-left,
   so it mirrors to the top-right. The two homepages override both the box and
   this rule in sdeira-liwa.css, which loads after this sheet. */
html[dir="rtl"] .project-card--feature .project-card-img::before {
  left: auto;
  right: 0;
  transform-origin: 0 0;
  transform: translateX(100%) scale(-1, 1);
}

/* The hover half, at the same specificity the English rules use: the two
   .needs-route-media rules above and the two .project-card-media rules are
   (0,3,1) and their selectors are not direction-scoped, so an RTL override
   has to out-specify them rather than rely on source order. */
html[dir="rtl"] .needs-route:not(:where(.business-needs-route)):is(:hover, :focus-within) .needs-route-media::after {
  transform: translateX(110%) scale(-1.1, 1.1);
}

html[dir="rtl"] .needs-route:not(:where(.business-needs-route)):is(:hover, :focus-within) .needs-route-media::before,
html[dir="rtl"]:not([data-brand="zoad"]) :is(.project-card, .project-card--secondary):is(:hover, :focus-within) .project-card-media::before,
html[dir="rtl"] .project-card--feature:is(:hover, :focus-within) .project-card-img::before {
  transform: translateX(100%) scale(-1.1, 1.1);
}

/* The needs-v2 and card-grid blocks carry no corner mark on the image itself
   (site owner decision, CSS-04). The card-level chevron and wipe are
   unchanged; only the image's own ::before / ::after marks are disabled,
   scoped to the two block wrappers so no shared photo mark elsewhere is
   affected. */
.wp-block-sdeira-needs-v2 .needs-route-media::before,
.wp-block-sdeira-needs-v2 .needs-route-media::after,
.wp-block-sdeira-card-grid :is(.needs-route-media, .news-item-media, .split-media, .project-card-media)::before,
.wp-block-sdeira-card-grid :is(.needs-route-media, .news-item-media, .split-media, .project-card-media)::after {
  display: none;
}

.project-card-copy {
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
}

/* Canonical project-card geometry and state. The homepage and generated
   business pages add only their named content/layout variants around it. */
.project-card-base.project-card-base {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  overflow: visible;
  background: var(--surface-raised);
  border: 0;
  border-radius: var(--r);
}

/* .is-clickable is added by sdeira-chrome.js once it confirms the card has a
   real destination - see that file for the click delegation itself. Matches
   the homepage's own `.project-card.is-clickable { cursor: pointer; }`. */
.project-card-base.is-clickable {
  cursor: pointer;
}

/* .project-card-media is BOTH .split-media (the figure component, which
   must stay overflow: visible - see that rule's own comment - so a caption
   badge can hang outside it) AND .news-item-media (which sets
   overflow: hidden so its :hover scale-zoom stays inside the photo's own
   rounded frame). Same specificity, same element, and .split-media loads
   later in this file, so its visible won - the zoomed image spilled past
   the card's corner on hover instead of staying cropped to it. A project
   card never carries a caption badge, so it never needed the visible
   escape hatch; this restores the clip for this one media role without
   touching .split-media's own rule, which real figures still depend on. */
.project-card-base .project-card-media {
  flex: 0 0 auto;
  aspect-ratio: var(--media-card);
  min-block-size: 0;
  overflow: hidden;
}

.project-card-base .project-card-copy {
  position: relative;
  flex: 1 1 auto;
  overflow: hidden;
  padding: clamp(24px, 2.4vw, 34px);
  background: var(--surface-sunken);
}

.project-card-base .project-card-copy > :not(.card-chevron) {
  position: relative;
  z-index: 2;
}

.project-card-base .project-card-copy h3 {
  font-size: var(--type-card);
  line-height: var(--leading-card);
}

@property --wipe-p {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

@property --wipe-op {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 72%;
}

.project-card-base .project-card-copy::after {
  content: "";
  position: absolute;
  z-index: 0;
  /* CANCELS A COLLISION WITH sdeira-subpage.css's OWN ::after ON THE SAME
     ELEMENT - a project-card-copy with no action-link (job-card variant, no
     `href` on any catalog item) also matches
     `.news-item-body:not(:has(.action-link))::after`, which reserves the
     action-link's footprint with `block-size: 44px`. Both rules are
     (0,2,0)-specificity and target the identical pseudo-element on the
     identical node, so the browser merges them property by property rather
     than picking one winner outright - an explicit height set by either
     rule beats a height this rule only implies through `inset`, regardless
     of load order. The travelling panel collapsed to a 44px strip instead
     of the full card body (flagged live, 7 Sep 2026, on
     /khadamat/en/services/ with the fill re-enabled on these cards - see
     components.py's service_catalog()). Restating block-size: auto here
     keeps every other caller of the shared reservation rule unaffected -
     it targets only this pseudo, on this class - and lets top/bottom from
     `inset` govern this element's height again, as intended. */
  block-size: auto;
  /* THE OVERSCAN TRICK, copied exactly from .cta-action::before below (this
     file) - not `inset: 0`. A plain inset:0 host leaves the clip-path's own
     diagonal notch visible INSIDE the panel forever, even once fully
     revealed: at rest AND at settled hover, the card showed a permanent
     diagonal slice instead of one clean solid fill. Widening the host 40%
     past each side moves both notches (top-right and bottom-left corners of
     the polygon) entirely outside the card's own bounds, so a fully
     revealed panel paints edge-to-edge with no visible cut - exactly what
     .cta-action's own settled hover looks like, and what this component
     looked like before the travelling-panel rewrite. */
  inset: -1px -40%;
  /* THE TRAVELLING PANEL, not a grown gradient. This layer used to fill by
     animating --wipe-p, a registered custom property driving a 315deg
     gradient stop. Two defects, both reproduced live on the Khadamat
     service catalog (6 Sep 2026): the leading edge is a hard diagonal that
     spends its whole slow tail crawling through the description text (the
     ease-out curve parks 87%->100% across the copy zone for ~300ms), and an
     @property animation that loses its end frame - hover out mid-flight,
     scroll the card out of view, an interrupted frame - leaves the panel
     FROZEN partway across the text with no rule to recover it. Several
     cards on one page then read as a wall of green slices cutting the
     copy, which is exactly what the site owner's screenshots show.
     The system's own answer is the travelling wipe every filled button
     already uses (.cta-action::before, sdeira-sections.css; .nav-cta,
     sdeira-nav-controls.css): a full-size panel with the same diagonal
     clip-path, parked off-panel by translate3d and slid in on hover.
     Transform-only animation is compositor-side: it cannot freeze at a
     partial fill, and its mid-flight shape is the complete panel arriving,
     never a diagonal crossing live text. Same endpoints as before (rest =
     nothing visible, hover = the full accent panel), same duration and
     easing tokens, and the geometry mirrors by direction exactly as
     .cta-action's own [dir="ltr"] block does. */
  /* Use the business-route motion with this card's original color token. */
  inset: 0;
  --wipe-fill: var(--accent-deep);
  --wipe-p: 0%;
  background: linear-gradient(var(--route-card-wipe-angle), var(--wipe-fill) 0 var(--wipe-p), transparent var(--wipe-p) 100%);
  clip-path: none;
  transform: none;
  transition: --wipe-p var(--dur-slow) var(--ease-out);
  pointer-events: none;
}

[dir="ltr"] .project-card-base .project-card-copy::after {
  transform: none;
}

.project-card-base:is(:hover, :focus-within) .project-card-copy::after {
  --wipe-p: 100%;
  --wipe-op: 100%;
  transform: none;
}

/* The copy changes on the wipe's clock, not at hover start. This keeps the
   text readable while the diagonal surface travels across the card. */
.project-card-base .project-card-copy > :not(.card-chevron) {
  transition: color var(--dur-slow) var(--ease-out);
}

.project-card-base:is(:hover, :focus-within)
  .project-card-copy > :not(.card-chevron) {
  color: var(--brand-white);
}

/* UNLINKED CARDS NEVER TURN WHITE. The whitening above exists so text stays
   legible ON the travelling panel; a .news-item--pending card keeps its panel
   parked (its own rule, sdeira-subpage.css), so whitening its text would put
   white on the light card with no dark fill behind it - invisible copy. The
   pending rule `.news-item--pending:is(:hover...) .news-item-body > *` sets
   --ink, but both selectors are (0,4,1)/(0,4,0)-class and the shared sheet
   loads after, so this narrow exclusion is what actually binds. */
.project-card-base.news-item--pending:is(:hover, :focus-within)
  .project-card-copy > :not(.card-chevron) {
  color: inherit;
}

/* The badge role (.news-item-meta .date/.tag/.partner, sdeira-subpage.css)
   sets its own explicit --badge-text color, which wins over the inherited
   white above and stays orange-on-orange under the fill. The homepage's own
   project cards hit this exact contrast failure and fixed it - "orange on
   #cb6c2e measures 1.28", forced white on hover - but only in its page-scoped
   sdeira-v2.css/-en.css, on .project-type, never in this shared component. */
.project-card-base:is(:hover, :focus-within) .news-item-meta :is(.date, .tag, .partner) {
  color: var(--brand-white);
}

/* The same exclusion as the copy-whitening exception above, one level down:
   on an UNLINKED (pending) card the panel never arrives, so the badge keeps
   its own --badge-text (Khadamat: Deep Forest on Light Grey) instead of
   whitening into invisibility on the bare card. */
.project-card-base.news-item--pending:is(:hover, :focus-within)
  .news-item-meta :is(.date, .tag, .partner) {
  color: var(--badge-text);
}

/* Same gap, one component over: .row-item-meta i.ph (the map-pin/calendar/bed
   icons in the card's detail row) also sets colour directly on itself -
   `.row-item-meta i.ph { color: var(--icon); }` above - rather than
   inheriting, so it stayed --icon-grey under the fill too. The homepage's own
   project cards carry the matching fix already
   (`.project-card--secondary:is(:hover, :focus-within) .project-meta i.ph`),
   again only in its page-scoped sdeira-v2.css/-en.css. */
.project-card-base:is(:hover, :focus-within) .row-item-meta i.ph {
  color: var(--brand-white);
}

/* ==================================================== THE P.43 PATTERN
   FORBIDDEN, PERMANENTLY: the solid two-tone chevron triangle this block
   replaces. Site owner, 28 Aug 2026, after this exact regression happened
   TWICE in one day - once when a page ("/about/certifications") drifted
   back to the old triangle because a stylesheet load order let it win, and
   again minutes later when an attempted "fix" deleted this block outright
   and reintroduced the triangle site-wide, including on the two homepages.
   Both were reverted. The instruction is explicit and unconditional: never
   render the solid chevron anywhere on this platform again, on any card,
   for any reason. If a future request describes "the triangle mark", "the
   solid chevron", or asks to make a card look like the old destination-card
   corner sticker - that request describes exactly the shape banned here.
   Point to this comment and confirm with the site owner before touching
   `.card-chevron` again; do not silently "restore" or "improve" it.
   scripts/components.py's card_chevron() carries the matching warning and
   now emits an empty span - it never draws the triangle inline again.

   Owner, 21-23 Aug: the approved pattern (V9 p43, vector-traced in
   docs/prototypes/patterns/) replaces the old chevron sticker and hairline
   bands. The mark sizes at 92% of the card's height and anchors in the corner
   furtherest from reading start; hover scales 1.1 from that corner. Paths are
   the live equivalents of the prototype's, now under assets/images/patterns.
   Owner, 28 Aug 2026: raised from 80% to 92% of the panel height (bigger
   mark), and the mark is opaque at every state - no partial-opacity variant
   of this sticker survives anywhere it is used.
   PROMOTED HERE FROM sdeira-liwa.css, 28 Aug 2026: that file only loads on
   the two homepages (see its own header comment - "it loads LAST in <head>
   on index.html and en/index.html"), so every generated sub-page
   (/business/, /companies/, /careers/, /projects/) never got this rule and
   kept rendering the RETIRED solid-triangle SVG the markup still ships
   inline (`.card-chevron svg` below), instead of the shared p.43 line mark
   the homepage cards carry - flagged by the site owner from a screenshot of
   /business/'s "Five connected businesses" cards next to the homepage's own.
   This file is shared by the homepage and every generated page, so the fix
   belongs here, once, not copied into each page's own stylesheet. */
.card-chevron svg { display: none; }
.card-chevron.card-chevron {
  inset: 0;
  inline-size: auto;
  width: auto;
  aspect-ratio: auto;
  opacity: 1;
  pointer-events: none;
  transform-origin: 100% 100%;
  transition: transform var(--dur-normal, 200ms) var(--ease-out, ease-out);
}
/* NO html[dir="ltr"] RE-ASSERTION HERE, DELIBERATELY, 28 Aug 2026. A rule
   used to sit right here - `html[dir="ltr"] .card-chevron.card-chevron
   { left: 0; right: 0; }` - re-declaring exactly what `inset: 0` two lines
   up already sets. It did nothing for this base rule (inset: 0 already
   means left:0/right:0/top:0/bottom:0), but its extra `html` type selector
   gave it specificity (0,3,1) - one tick above every family's own LTR
   mirror below (.project-card-base, .business-needs-routes: (0,3,0) each,
   two classes past [dir="ltr"]), so it silently overrode them and pinned
   the sticker to the wrong corner - full-width, top-left, crossing the
   card's own text - on any family whose mirror selector had only two
   classes. .company-grid--content and .icon-grid--chevron never showed the
   bug because their own mirrors are already one class deeper (0,4,0) and
   outrank it regardless.
   Found and silently re-broken THREE TIMES because each fix patched one
   family's mirror to match the phantom rule's specificity instead of
   asking why a "no-op" rule kept winning fights it had no business
   entering: /en/business/property-development/ (.project-card-base, 28 Aug
   2026), then /en/business/'s "Five connected businesses"
   (.business-needs-routes, same day) after the family list moved. Removing
   the phantom rule fixes every current and future family at once - a
   plain `[dir="ltr"] <family> .card-chevron { left: auto; right: -7%; }`
   is now sufficient again, and no family needs the `html` prefix hack
   invented to out-specify a rule that added no visual behaviour of its
   own. Before touching this again: if a family's mark still lands in the
   wrong corner, check for a NEW blanket .card-chevron rule reintroducing
   this pattern, not a low-specificity mirror to "fix". */
.card-chevron::after {
  content: "";
  position: absolute;
  inset: 0;
  background: url("../images/patterns/lines.svg?v=d874099296964b63") no-repeat right 0 bottom 0 / auto 92%;
  pointer-events: none;
  transform: scale(1);
  transform-origin: 100% 100%;
  transition: transform var(--dur-normal, 200ms) var(--ease-out, ease-out);
}
/* THE RTL BACKGROUND RE-ANCHOR IS GONE, REMOVED 15 Sep 2026. A rule used to
   sit here (`html[dir="rtl"] .card-chevron::after { background-position:
   left 0 bottom 0 }`), written when the box moved to the panel's left and the
   artwork had not followed it yet. Ten days later the artwork gained its own
   mirror (the block below), which already lands it on the box's left edge, so
   both anchors applied and the mark stopped 8% of the box's own width short
   of the corner: measured on the Arabic homepage at 1440, the painted strokes
   sat at x 20-132 inside a 249.9px box while the English page's mark was
   flush, its strokes at x 394-520 of a 281.9px box. (Those are stroke
   extents from a pixel diff; the artwork's own box is the same 249.9px wide.)
   Removing the background re-anchor makes the
   Arabic mark the exact mirror of the English one (x 0-112 in the same box),
   and the same fix moves the company cards' Arabic mark from 21px inside
   their right edge to flush (x 480-596), where their English twin already
   was. One anchor per axis: the BOX places, the ARTWORK mirrors. Restating
   the background here re-breaks both families at once. */
:is(.needs-route:not(:where(.business-needs-route)), .project-card-base, .company-card, .project-card--feature):is(:hover, :focus-within) .card-chevron,
.business-needs-routes .business-needs-route .needs-route-copy:is(:hover, :focus-within) .card-chevron,
.business-needs-routes .business-needs-route:focus-visible .needs-route-copy .card-chevron {
  transform: scale(1.1);
  transform-origin: 100% 100%;
}
/* One rule per direction rather than two identical branches: the LTR branch
   is the English baseline, untouched; the RTL branch scales from the box's
   LEFT edge, which is the corner the ink occupies once the artwork's own
   mirror (the 15 Sep 2026 block below) has moved it there. NO translate and
   NO second scale(-1, 1) here: the mirror lives on the artwork and the grow
   lives on the box, and putting a mirror on both mirrored the mark a second
   time, cancelled the artwork's own, and let the compensating 110% throw the
   sticker a full box width off its corner on hover. Measured on the Arabic
   homepage at 1440: the ink slid from x 20-132 to x 131-253 inside a 520px
   panel, and at 390 it landed across the action link. The English ink, which
   has no mirror to cancel, held its corner in both states. */
html[dir="ltr"] :is(.project-card-base, .project-card--secondary, .project-card--feature):is(:hover, :focus-within) .card-chevron {
  transform: scale(1.1);
  transform-origin: 100% 100%;
}
html[dir="rtl"] :is(.project-card-base, .project-card--secondary, .project-card--feature):is(:hover, :focus-within) .card-chevron {
  transform: scale(1.1);
  transform-origin: 0 100%;
}

/* ==================================================== THE STICKER MIRRORS IN
   RTL TOO, 15 Sep 2026 - the artwork half. The box is already mirrored by the
   family rules above and below (.needs-route / .project-card--secondary and
   .project-card-base both sit at the panel's left in RTL and are moved to the
   right edge by their own plain [dir="ltr"] rules), but the artwork inside the
   box was not: .card-chevron::after anchors its own background to the box's
   `right 0 bottom 0` in BOTH languages, so in Arabic the whole mark ended up
   pushed to the box's far side - measured on the Arabic homepage at 1440, the
   sticker's strokes sat at x 299-394 of a copy panel starting at x 112, i.e.
   adrift in the middle of the panel beside the action link rather than flush
   in the panel's own far corner. The English page is the baseline, and this
   mark is documented as "anchored in the corner furthest from reading start",
   which is the physical left in Arabic.

   Same translate + scale pair as the photo mark above, same reason, same
   derivation, and the same hidden trap avoided: a bare scaleX(-1) pivots at
   the box centre and would make the hover scale grow from the middle of the
   mark, and pivoting it at the anchored corner instead throws the box a full
   width off the card. Here the box is anchored at its LEFT edge, so it is
   100% at rest and 110% hovered. The box's own background anchor is left
   alone on purpose: the transform mirrors it along with the artwork, and
   re-anchoring the background in RTL would mirror it twice.

   THE COMPANY CARDS ARE DELIBERATELY OUT OF SCOPE, and the selector list says
   so rather than trusting a later blanket rule. .company-card's box is
   `inset: auto 0 0 auto` - a physical bottom-right corner in BOTH languages,
   documented a few hundred lines down as not a logical position - so there is
   nothing for its artwork to follow, and mirroring it alone would push that
   mark out of its own corner. Mirrored position and mirrored artwork have to
   move together or not at all; this block is for the families where the box
   moves.

   [data-brand="zoad"] IS CARVED OUT FOR THE SAME REASON ITS OTHER MARKS
   ALREADY ARE (see the media-mark mirror above and zoad-theme.css): that
   brand's corner marks are physical in both languages and carry their own
   `rotate: 180deg`, which turns about `transform-origin`, so this block's
   `transform-origin: 0 100%` spun the card-chevron dots a full box off the
   panel and they stopped painting at all. Measured on the Arabic
   /zoad/services/production/ at 1440: the pseudo's own element-pixel diff
   went from 27/255 against the panel to 0/255, while the English page, which
   never received this block, kept painting them. */
html[dir="rtl"]:not([data-brand="zoad"]) :is(.needs-route, .project-card-base, .project-card--secondary, .project-card--feature, .business-needs-routes) .card-chevron::after {
  transform-origin: 0 100%;
  transform: translateX(100%) scale(-1, 1);
}

/* The hover half. The shared (0,3,0) rule four lines up is not direction
   scoped, so it needs an RTL rule of its own above it, which is what the
   html prefix buys. .project-card-base / .project-card--secondary are also
   covered by the (0,4,1) pair above, so that pair's RTL branch is the one
   actually edited - leaving both branches identical would have kept the
   origin pinned to the wrong corner on the homepage's project cards. Same
   plain scale(1.1) and same reason as that pair: this box carries the grow,
   the artwork carries the mirror, and a mirror here would cancel it. */
html[dir="rtl"] :is(.needs-route:not(:where(.business-needs-route)), .project-card-base, .project-card--feature):is(:hover, :focus-within) .card-chevron {
  transform-origin: 0 100%;
  transform: scale(1.1);
}

/* ==================================================== THE GROW'S PIVOT HOLDS
   IN BOTH STATES, 18 Sep 2026. Every RTL branch in this file (and in
   sdeira-liwa.css, and the business-route pair further down) declares
   `transform-origin: 0 100%` on the HOVER selector only, and transform-origin
   is in none of these transitions - they carry `transform` alone - so the
   pivot snapped back to `100% 100%` the instant the pointer left while the
   scale was still mid-flight. Measured on the Arabic homepage's company cards:
   one frame at scale 1.091 with origin `0 100%`, the next at scale 1.091 with
   origin `100% 100%`, i.e. a 260px pivot change applied in a single frame, so
   the mark jumped sideways as it shrank. Site owner, 18 Sep 2026: "the shapes
   change place on hover", and it reads as abrupt at exactly the moment the
   rest of the card is easing back.

   Declaring the pivot on the REST state as well makes it constant. At rest the
   transform is `none`, so a different pivot paints nothing different there:
   this block is invisible until the hover-out it fixes.

   `0 100%` is the corner the ink occupies once each family's own artwork
   mirror has run, and it is the value every one of those hover rules already
   declares; this block moves the declaration to rest, it does not choose a new
   corner. `.company-card` is in the list because its own RTL block further
   down mirrors the artwork IN PLACE, which lands that ink on the box's left
   edge for the same reason.

   [data-brand="zoad"] IS EXCLUDED, because that brand's own mode pivots the
   other way: zoad-theme.css's `:is(...):is(:hover, :focus-within)
   .card-chevron.card-chevron { transform-origin: 100% 100% }` is the physical
   bottom-right in both directions (the brand's box is the whole panel and its
   dots carry `rotate: 180deg`, so this file's mirror is carved out for it
   already). A rest state declared `0 100%` there would be the very snap this
   block exists to remove. */
html[dir="rtl"]:not([data-brand="zoad"]) :is(.needs-route, .project-card-base, .project-card--secondary, .project-card--feature, .company-card, .business-needs-routes) .card-chevron {
  transform-origin: 0 100%;
}

/* COMPANY CARD: the mark reads oversized here for a reason specific to this
   card. `.card-chevron.card-chevron` above is `inset: 0`, so the artwork's
   own `auto 92%` background-size is 92% of whatever box it fills - on
   .needs-route and .project-card-base that box is only the copy HALF of a
   split card, so 92% of it reads as a normal corner accent. .company-card
   has no split: the host box IS the whole card, so 92% of it reads as a
   full-card watermark instead. Shrinking the host box to a fixed corner
   size (not a percentage of card height - the card's height is content-driven
   and auto, so a percentage height would not resolve) restores the same
   corner-accent proportion the other two card families already have.
   Physical bottom-right via `inset`, matching the p.43 mark's site-wide
   "never mirrored in RTL" rule - not a logical position.

   THE BOX MUST SHARE THE ARTWORK'S OWN ASPECT RATIO (434/281, same
   --photo-mark-p43-ratio every other p.43 host uses), or the mark gets
   clipped by a straight edge instead of tapering on its own. A background
   image sized by height alone (`auto 92%`) keeps the artwork's native
   width-to-height ratio; the previous 260x190 box (ratio 1.37) was
   narrower than the artwork's own ratio (1.54), so at 190px tall the mark
   wanted to paint ~294px wide and the box's own left edge sliced straight
   through the diagonal strokes mid-line - the "chopped off" look flagged
   28 Aug 2026. Width-driven with aspect-ratio, not two independent clamps,
   so the box can never again drift out of the artwork's own proportion. */
.company-card .card-chevron.card-chevron {
  inset: auto 0 0 auto;
  inline-size: clamp(160px, 20vw, 260px);
  aspect-ratio: 434 / 281;
  block-size: auto;
}

/* NEEDS-ROUTE AND PROJECT-CARD--SECONDARY: same over-specified-box bug as
   .company-card above, on the homepage's other two card families. Both are
   positioned in sdeira-v2.css/sdeira-v2-en.css by a plain `.card-chevron`
   rule (specificity 0,1,0) that sets its own left/bottom, but the blanket
   `.card-chevron.card-chevron` two class levels up (0,2,0) always wins the
   cascade for any longhand it also touches - and `inset: 0` above touches
   all four (top, right, bottom, left), so top/right stayed pinned at 0
   under the homepage rule's left/bottom regardless of what either family
   file wrote. Absolutely positioned with left+bottom+width all set and
   right also pinned at 0 is over-constrained; the browser resolves that by
   dropping `right` to auto (left wins) but LEAVES `top: 0`, so the sticker
   anchored to the card's top-left instead of a small bottom corner and
   stretched to its declared width from there - exactly the "spans the
   middle of the card" look flagged from a screenshot of the homepage's
   project/company card grids, 28 Aug 2026. Doubling the class here, the
   same pattern .company-card already uses successfully above, is the only
   reliable fix: it raises this rule to 0,3,0, which beats the blanket
   0,2,0 regardless of which file loads first. */
:is(.needs-route, .project-card--secondary, .project-card--feature) .card-chevron.card-chevron {
  inset: auto;
  left: 0;
  bottom: 0;
  /* FLUSH, NOT OVERHUNG. This host is `overflow: hidden` (.needs-route-copy),
     so a box positioned past the corner with a negative left/bottom (the
     previous -7%/-8%) had its own straight box edge sliced off by the
     clip - the artwork's natural tapered point never got to render, and
     the mark read as abruptly cut/shifted rather than tapering into the
     corner. Flush to 0/0 keeps the whole box, and therefore the whole
     taper, inside the visible clip. Site owner, 29 Aug 2026, from a
     screenshot of this exact card.

     WIDTH-DRIVEN, 15 Sep 2026, the same clamp .company-card already carries
     above. Sizing off the panel's HEIGHT (the previous min(80%, 220px)) tied
     the mark to the copy: every one of these panels clips its own sticker and
     its height grows with the text that wraps into it, so the sticker grew
     with it and stopped being a corner accent. Measured on the Arabic
     homepage, .project-card--secondary's mark was 47.0% of its panel's width
     at 1440 but 82.8% at 768 and 80.5% at 390, lying across the paragraph
     (.needs-route, whose panel is shorter, measured 48.1% / 56.3% / 59.9%
     over the same three widths). One clamp on the inline
     size, with the artwork's own ratio doing the rest, bounds it at every
     width and gives the families listed above one size; .project-card-base
     and .business-needs-routes keep the height-driven boxes their own 28 Aug
     2026 owner note chose, so the mark is NOT yet one size site-wide, and
     whether it should be is an open question against that note.

     `.project-card--feature` joins this family for the FILTERED view only:
     the editorial "all" state hides its chevron, and once the filter demotes
     the card to a plain white surface it is the same card as its siblings, on
     loan "from the same shared .card-chevron" per its own rule in
     sdeira-v2.css. That rule sizes and places the sticker too, but it sits in
     @layer legacy, so the blanket unlayered `.card-chevron.card-chevron
     { inset: 0 }` out-ranked it and the sticker stretched over the whole
     594px panel instead of its bottom corner. */
  inline-size: clamp(160px, 20vw, 260px);
  block-size: auto;
  aspect-ratio: 434 / 281;
  /* NEVER TALLER THAN ITS OWN PANEL, and never so tall that its own hover
     step runs off the top. On a short panel the width-driven box outgrew the
     panel it sits in: Khadamat's route panels are 163.6px at 1700 and 154px
     at 1280, while the clamp produced 168 and 166px boxes, so the panel's
     own overflow clipped up to 7px off the top of the mark at rest and 28px
     once the hover grow was in play. 100% resolves here because the panel is
     an absolutely positioned element's containing block, which is the same
     reason the percentage cap it replaced resolved at all.
     90% is 1 / 1.1 rounded down, the site's own hover step: the mark is
     capped so that grow-the-box-by-1.1 still lands inside the panel, which is
     true of this mark on every other card family and was the property the
     percentage cap had. It only binds on short panels, so the desktop
     proportions are unchanged. */
  max-block-size: 90%;
}

html[dir="ltr"] :is(.needs-route, .project-card--secondary, .project-card--feature) .card-chevron.card-chevron {
  left: auto;
  right: 0;
}

/* THE FOUNDATIONAL HALF OF THIS RULE WAS MISSING - only the corner offsets
   were ever written here, so the sticker had no `position` to apply them
   against and rendered in normal flow instead: a full-opacity, unclipped
   434x440 shape ahead of the tag and heading rather than a small sticker
   tucked behind the copy's low corner. .business-needs-routes and
   .company-grid--content both carry the complete rule a few hundred lines
   below - this is the one family that lost the other half. Values matched
   to those two and to design-system/cards.html's own documented defaults:
   the shared 434/440 ratio, .78 resting opacity, no pointer events.

   z-index 1, NOT 0 - raised above the hover wipe (.project-card-copy::after,
   below, also z-index 0 in the same stacking context) rather than tied with
   it. At an equal z-index a pseudo-element paints last, so with both at 0 the
   wipe painted over the sticker the instant it filled and the decoration
   vanished on hover/focus - the .project-card--secondary page override in
   sdeira-v2.css already carries this exact fix (wipe 0 / chevron 1 / copy 2)
   with a comment describing the same bug; this base component's copy z-index
   two rules up was raised to match it. Site owner flagged the sticker
   disappearing on hover, 17 Aug 2026, and asked for the base component fix
   specifically since every project-card-base family (news, project,
   whichever else adopts it) shares this rule. Owner, 28 Aug 2026: cap raised
   180px -> 220px (same 40% share) and the mark reads at full opacity, at
   rest and on hover - the .78 resting/-.94 hovered pair that used to fade it
   is gone site-wide for this sticker family. */
.project-card-base .card-chevron {
  position: absolute;
  z-index: 1;
  /* `inset: auto` FIRST, clearing the blanket `.card-chevron.card-chevron
     { inset: 0 }` reset a few hundred lines up (line ~448) - that rule sets
     top/right/bottom/left all to 0 so the sticker fills its host end to
     end, and this family's own left/bottom below never cleared the other
     two sides. The result: top:0, right:0 (from the blanket rule) PLUS
     left:-7%, bottom:-8% (from this rule) at once, which stretches the
     sticker across most of the card instead of anchoring a small corner
     mark - exactly the oversized, off-center look flagged from a
     screenshot of the project/company card grids, 28 Aug 2026. Every
     sibling rule below (.business-needs-routes, .company-grid--content
     .company-card) carried the same bug and needs the same fix. */
  inset: auto;
  left: 0;
  bottom: 0;
  /* FLUSH, NOT OVERHUNG - same fix as .needs-route above (see its own
     comment): this host is also overflow:hidden, and the previous
     -7%/-8% negative offsets pushed the box past the corner so the clip
     sliced off the artwork's own tapered point, reading as a shifted/cut
     mark rather than a clean corner taper. Site owner, 29 Aug 2026. */
  block-size: min(80%, 180px);
  inline-size: auto;
  aspect-ratio: 434 / 281;
  opacity: 1;
  pointer-events: none;
  transition: transform var(--dur-slow) var(--ease-out);
}

.project-card-base .card-chevron svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}

.project-card-base:is(:hover, :focus-within) .card-chevron {
  transform: translate3d(14px, -10px, 0);
}

/* Mirrored for LTR, matching the established precedent for this exact
   component: .business-needs-routes .card-chevron above already does this
   (left -> right, plus scaleX(-1) on the artwork so the drawing's own
   straight edges still line up with the card's corner instead of just
   sliding to the other one). sdeira-v2-en.css already carried a page-scoped
   attempt at this same mirror, at equal specificity to this rule and loaded
   earlier in the cascade - both unlayered, so this rule (later, same
   specificity) was winning regardless, and the homepage's project cards
   stayed left-mirrored on the English page. Site owner flagged, 12 Aug
   2026. */
[dir="ltr"] .project-card-base .card-chevron {
  left: auto;
  right: 0;
}

[dir="ltr"] .project-card-base .card-chevron svg {
  transform: scaleX(-1);
}

[dir="ltr"] .project-card-base:is(:hover, :focus-within) .card-chevron {
  transform: translate3d(-14px, -10px, 0);
}

.project-card-base .project-card-action:focus-visible,
.project-card-base .action-link:focus-visible {
  outline: 3px solid var(--brand-razeen);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .project-card-base .project-card-copy::after,
  .project-card-base .card-chevron { transition: none; }
}

/* News cards are the homepage's vertical editorial card in every context.
   The feature card keeps its homepage split layout; the archive uses the
   ordinary member below. */
.news-card:not(.news-card--feature) {
  min-inline-size: 0;
  display: grid;
  grid-template-rows: auto 1fr;
  overflow: hidden;
  background: var(--surface-raised);
  border-radius: var(--r);
  cursor: pointer;
}

.news-card:not(.news-card--feature) > * { min-inline-size: 0; }

.news-card:not(.news-card--feature) .news-image-link {
  display: block;
  overflow: hidden;
}

.news-card:not(.news-card--feature) .img {
  display: block;
  overflow: hidden;
  aspect-ratio: var(--media-wide);
  block-size: auto;
  min-block-size: 0;
  margin: 0;
  background: var(--brand-black);
}

.news-card:not(.news-card--feature) img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  filter: saturate(.94) contrast(1.04);
  transition: transform var(--dur-slow) var(--ease-out),
    filter var(--dur-base) var(--ease-out);
}

.news-card:not(.news-card--feature):is(:hover, :focus-within) img {
  filter: saturate(1) contrast(1.03) brightness(1);
}

.news-card:not(.news-card--feature) .news-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-block-size: 0;
  padding: 22px 24px 20px;
}

.news-card:not(.news-card--feature) .news-body > * {
  min-inline-size: 0;
  max-inline-size: 100%;
}

.news-card:not(.news-card--feature) .date {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-block-start: 14px;
  color: var(--badge-text);
  font-size: var(--badge-size);
  font-weight: var(--badge-weight);
  letter-spacing: 0;
}

.news-card:not(.news-card--feature) .date::before {
  content: "◷";
  color: currentColor;
  font-size: var(--icon-inline);
}

.news-card:not(.news-card--feature) h3 {
  min-block-size: 0;
  margin-block-start: 14px;
  color: var(--brand-black);
  font-size: var(--type-card);
  line-height: var(--leading-card);
  text-wrap: balance;
  transition: color var(--dur-base) var(--ease-out);
}

.news-card:not(.news-card--feature) h3 a {
  color: inherit;
  text-decoration: none;
  display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.news-card:not(.news-card--feature) p {
  min-block-size: var(--leading-card);
  margin-block-start: 10px;
  overflow: hidden;
  color: var(--muted);
  font-size: var(--type-action);
  line-height: 1.7;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.news-card:not(.news-card--feature) .more {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-block-size: 44px;
  margin-block-start: 16px;
  padding-block-end: 4px;
  color: var(--brand-black);
  font-size: var(--badge-size);
  font-weight: var(--badge-weight);
}

.news-card:not(.news-card--feature) .more i.ph {
  color: currentColor;
  transition: transform var(--dur-base) var(--ease-out);
}

/* Hover/focus moves the accent to the action, not the headline: 1 Sep 2026,
   flagged in a preview comment - every other hover state in this system puts
   the interaction colour on the control the visitor is about to activate
   (the "Read the release" link), never on editorial text sitting beside it.
   The headline stays on its resting ink; only the action + its arrow pick up
   the accent, matching .news-card:is(:hover, :focus-within) .more on the
   homepage's own reused component (sdeira-liwa.css) that this list was
   supposed to match in the first place. */
.news-card:not(.news-card--feature):is(:hover, :focus-within) .more {
  color: var(--link-hover);
}

.news-card:not(.news-card--feature):is(:hover, :focus-within) .more i.ph {
  transform: translateX(4px);
}

@media (prefers-reduced-motion: reduce) {
  .news-card:not(.news-card--feature) img,
  .news-card:not(.news-card--feature) .more i.ph { transition: none; }
}

/* --label AND --pending FAIL CONTRAST ON THE SUNKEN GROUND, and the token file
   says so outright: --label is only for surfaces at Cloud or lighter, and on
   Silver the answer is --icon (Jais). Grey 70 on #F6E9D6 measures 4.26:1 at
   --type-meta, under the 4.5 floor for text that size. Every kicker and every
   pending note on a sunken section was landing there.

   This is the light-surface counterpart of the .pad--inverse block above: the
   ground a component sits on decides its tone, and the component keeps one
   definition. */
.pad--sunken :is(.kicker, .is-pending, .pending-note) { color: var(--icon); }
.pad--sunken .pending-note i.ph { color: var(--icon); }

/* Centred section heading. .sec-head is left-aligned and capped at 680px; this
   is the same block centred, for a band that introduces a grid rather than a
   column of prose. */
.sec-head--center {
  margin-inline: auto;
  text-align: center;
}

/* The property-development project heading is an approved two-line statement
   at desktop widths; its lead keeps the standard section reading measure.
   FIXED 28 Aug 2026: only the h2's own max-inline-size was raised here, but
   .sec-head above still caps the WHOLE block (kicker, h2, lead) at 680px, so
   the h2 could never actually reach 800px - the block itself has to widen
   too. The property-development page still read as two lines because that
   particular sentence happens to fit two lines within 680px; the identical
   markup on /about/our-story wrapped to three because its sentence is
   longer. Both now get the real 800px measure this variant promises. */
.sec-head--wide-heading { max-width: 800px; }
.sec-head--wide-heading h2 { max-inline-size: 800px; }

/* Split heading: kicker + heading in a start-edge column, the lead in an
   end-edge column, both bottom-aligned - two peer statements read side by
   side instead of one stacked column. .sec-head's own 680px cap and h2's
   18px margin-bottom (both above) are cancelled here: the heading no longer
   shares a column with the lead, so neither rule is the right measure for
   either half. */
.sec-head--split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: end;
  gap: clamp(24px, 3vw, 48px);
  max-width: none;
}

.sec-head--split h2 { margin-bottom: 0; }

.sec-head--split > p {
  margin: 0;
  font-size: var(--type-lead);
  line-height: var(--leading-lead);
  color: var(--muted);
}

/* The companies structure title is intentionally a two-line statement at the
   approved desktop measure; the lead remains readable in the narrower peer. */
.sec-head--split-wide-heading {
  grid-template-columns: minmax(0, 2fr) minmax(260px, .55fr);
}

/* The link card: a title and a line, no media. It was named in the component
   plan and never written, so six cards on /about carried a class that did
   nothing. Nothing broke, which is the problem - an undefined modifier is
   indistinguishable from a defined one until someone measures. */
.news-item--link .news-item-media { display: none; }

.news-item--link .news-item-body {
  padding: clamp(22px, 2.2vw, 30px) clamp(22px, 2.2vw, 28px);
}

/* A lead paragraph that is the whole of a short section. Same measure as
   .sec-head so two adjacent sections do not disagree about line length. */
.section-lead {
  max-width: 680px;
  font-size: var(--type-lead);
  line-height: var(--leading-lead);
  color: var(--muted);
}


/* =============================================================================
   1b. SECTION GROUND TEXTURES
   -----------------------------------------------------------------------------
   Three treatments, so that a page of eight sections is not eight identical
   cream rectangles. They are the ONLY three, and a page alternates between them
   rather than each section inventing its own.

   ALL THREE ARE THE SAME MARK AT THE SAME SLANT. The Sdeira mark has one
   physical angle, --brand-stripe-angle, and language direction may move a band
   group to the opposite edge but must never mirror or change that angle. What
   differs between the three is where the bands sit and how many, not what they
   are.

   THE TONE IS NOT A CHOICE EITHER. Section 5 of the token file names this exact
   case - a band drawn behind a section's OWN content, with text sitting
   directly over it and no scrim between - and fixes it at
   --identity-band-fill-ambient (2%) and --identity-band-line-ambient (10%).
   The brief attached to those numbers was "visible enough to add depth, quiet
   enough that it never competes with the words in front of it", and the 2% is
   deliberately near the floor of what renders. Nothing legible ever sits inside
   a band, so there is nothing inside one to keep legible.

   They are pointer-events: none and aria-hidden by construction (a pseudo
   element is not in the accessibility tree), and they sit at z-index 0 with
   .wrap at 1, so nothing here can ever land on top of the copy.
   ========================================================================== */

.pad--field,
.pad--sunken,
.pad--edge,
.pad--wedge {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

.pad--field > .wrap,
.pad--sunken > .wrap,
.pad--edge > .wrap,
.pad--wedge > .wrap {
  position: relative;
  z-index: 1;
}

/* SUNKEN, EDGE, FIELD are one rule. EDGE and FIELD retired the repeating-line
   ground on 27 Aug 2026, site owner: "delete the old patterns" (see PATTERN
   CLEAN-UP, PASS 2 in sdeira-liwa.css, 21 Aug: "the site's own stripe
   language leaves completely ... the p.43 line set is the only mark that
   remains" - true on the homepage since, never carried to sub-pages).
   .pad--sunken alone was missed in that pass and went on drawing the retired
   diagonal hatch (--ground-sand) everywhere it was used with no other ground
   modifier - /about/certifications's "Recognition published in 2026" strip
   is where the site owner caught it, 31 Aug 2026, and asked for the pattern
   gone site-wide, not patched on that one section. All three now paint
   solid-a.svg at 5% opacity, inset: 0 - the same p.43 mark
   .needs/.value-sections already draw on both homepages. EDGE's own
   left-74%-column boxing is gone with it - solid-a.svg reads as a ground
   texture across the whole section at this opacity, not a positioned band,
   so there is nothing left to box to one column. Every `pad--sunken`,
   `pad--field` and `pad--edge` record (under content/pages/, any depth)
   inherits this in one edit. */
.pad--sunken::before,
.pad--edge::before,
.pad--field::before {
  content: "";
  position: absolute;
  z-index: 0;
  inset: 0;
  background: url("../images/patterns/solid-a.svg?v=032800d78a7d41e0") no-repeat center / cover;
  opacity: 0.05;
  pointer-events: none;
}

/* WEDGE - no bands at all: a single soft tonal sweep at the gradient angle the
   token file carries for exactly this (--brand-stripe-gradient-angle, the
   companion value to the mark's own slant). For a section that already has a
   lot in it and needs depth rather than pattern. */
.pad--wedge::before {
  content: "";
  position: absolute;
  z-index: 0;
  inset: 0;
  background: linear-gradient(
    var(--brand-stripe-gradient-angle),
    transparent 0%,
    transparent 38%,
    color-mix(in srgb, var(--brand-sweihan) 3%, transparent) 68%,
    color-mix(in srgb, var(--brand-sweihan) 5%, transparent) 100%);
  pointer-events: none;
}

/* .pad--inverse.pad--edge's own dark-ground override retired the same day
   and for the same reason as .pad--field's (see above): both --edge and
   --field now paint the one solid-a.svg mark regardless of ground, matching
   the homepage's .value-sections/.needs (neither has an inverse variant
   either). No live record pairs pad--inverse with either --edge or --field
   today; if one ever does, that is a new dark-ground opacity to measure,
   not a reason to keep the retired stripe as a fallback. */

.pad--inverse.pad--wedge::before {
  background: linear-gradient(
    var(--brand-stripe-gradient-angle),
    transparent 0%,
    transparent 38%,
    color-mix(in srgb, var(--brand-white) 4%, transparent) 100%);
}


/* =============================================================================
   2. C1 - .split : TEXT AND MEDIA IN TWO COLUMNS
   -----------------------------------------------------------------------------
   Derived from the homepage's .about-layout / .about-visual pair, with one
   deliberate change: the media slot is var(--media-wide), not the homepage's
   4:5. There is no portrait delivery ratio (docs/component-build-architecture.md
   section 5.2) - a 4:5 crop keeps 45% of a 16:9 master's width, which collapses
   the safe area to a strip nobody can compose inside. The homepage's own 4:5
   predates the rule and is not retrofitted or copied forward.
   ========================================================================== */

.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(32px, 4vw, 64px);
}

/* Image-led editorial split: the same figure treatment with a wider media
   column. It is used when the photograph carries more of the section's
   meaning than the supporting copy. */
.split--media-dominant {
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  grid-template-areas:
    "heading heading"
    "body media"
    "actions media";
  align-items: start;
  row-gap: 20px;
  column-gap: var(--sec-head-gap);
}

/* The copy column. Its own vertical rhythm, so a caller never writes a margin
   on a child. */
.split-copy > * + * { margin-block-start: 20px; }
.split-copy > .kicker + h2 { margin-block-start: 0; }

/* Split copy follows its reading edge; the CTA band's centred actions remain
   centred because they are a different composition. */
.split-copy .cta-actions { justify-content: flex-start; }

/* `.action-link` is white by default because it is also used over imagery.
   A split action sits in the text column, however, on a documented light
   section ground. Give it the approved small-text accent role there; the
   inverse-ground counterpart keeps the image-context treatment. */
.split-copy .action-link { color: var(--accent-strong); }
.section-view-all { color: var(--accent-strong); }

/* Section introductions that carry one archive-level action use the same
   opposing-edge composition as the homepage project header. The heading keeps
   its editorial measure; the action sits on the other edge and aligns with
   the last line of the introduction before the card grid begins. */
.section-heading-row {
  display: grid;
  grid-template-columns: minmax(0, 680px) auto;
  align-items: end;
  justify-content: space-between;
  gap: var(--sec-head-gap);
  margin-block-end: var(--sec-head-gap);
}

.section-heading-row .sec-head { margin-block-end: 0; }
.section-heading-row .section-view-all { justify-self: end; }

@media (max-width: 860px) {
  .section-heading-row {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }

  .section-heading-row .section-view-all { justify-self: start; }
}
.pad--inverse .split-copy .action-link { color: var(--brand-white); }
.pad--inverse .split-copy .action-link:is(:hover, :focus-visible) {
  color: var(--link-hover-on-dark);
}

.split-copy p {
  font-size: var(--type-lead);
  line-height: var(--leading-lead);
  color: var(--muted);
}

/* THE FIGURE. This is the site's ONE way of presenting a photograph, in two
   variants that differ by a single optional child: with the caption block
   (.split-badge) and without it. Documented in design-system/index.html under
   الصورة, and nothing may author a second treatment.

   THREE ELEMENTS, AND THE CLIPPING IS NOT ON THE HOST. The host has to stay
   `overflow: visible` because the caption block deliberately hangs outside it
   on two sides. So the photograph carries its own radius and the ribbon
   carries its own clip, which is the structure the homepage's .about-visual
   already proved. Putting `overflow: hidden` back on .split-media does not
   look like a bug - it silently cuts the block's overhang off flush with the
   photo edge, which is a different component.

   aspect-ratio + object-fit: cover is the whole sizing contract for every
   photographic slot on the site; width/height never size one. */
.split-media {
  position: relative;
  overflow: visible;
  border-radius: var(--r);
  aspect-ratio: var(--media-wide);
  background: var(--surface-sunken);
}

.project-card-media{
  border-radius: 0;
}


:is(.btn, .btn-primary, .btn-ghost, .nav-cta, .news-form button):is(:hover, :focus-visible) > i.ph:last-child:not(.ph-arrow-square-out) {
  transform: translateX(4px);
}

[dir="rtl"] :is(.btn, .btn-primary, .btn-ghost, .nav-cta, .news-form button):is(:hover, :focus-visible) > i.ph:last-child:not(.ph-arrow-square-out) {
  transform: translateX(-4px);
}

.split-media img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* border-radius: var(--r); */
}

.split-media-link {
  display: block;
  color: inherit;
  text-decoration: none;
}

.split-media-link:focus-visible {
  outline: 3px solid var(--brand-razeen);
  outline-offset: 4px;
}

/* The identity band watermark, drawn ON the photograph, so it takes the
   on-photo tone (44% line / 5% fill) rather than the flat-surface one. At full
   strength the outlines stop reading as a watermark and compete with the
   panel's text - that is why the token file carries two tones and exactly two.

   An ELEMENT rather than .split-media::after, because it is the thing that
   owns the clip now. It is the reason a figure's markup always carries this
   span even when it looks like an empty node.

   PHYSICAL coordinates and an explicit [dir] pair: `left` here, mirrored in the
   RESPONSIVE section's direction block. The angle is never mirrored - the
   Sdeira mark has one physical slant, and language direction may move a stripe
   group to the other edge but must not flip it. */
.split-ribbon {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  border-radius: var(--r);
  pointer-events: none;
}

.split-ribbon::after {
  content: "";
  position: absolute;
  top: var(--identity-band-top);
  left: 8%;
  width: var(--identity-band-width);
  height: var(--identity-band-height);
  /* SWEIHAN, NOT WHITE. The strength was right and the hue was not: the token
     file's own note for this pair reasons about "bright orange diagonals" at
     full strength, which is only a sentence about Sweihan, and the homepage
     has drawn it in Sweihan at these exact two strengths since it was built
     (--ribbon-line / --ribbon-fill in sdeira-v2.css). A white band is not a
     quieter version of the identity band - it is a different mark, and the
     system has no white one. */
  border: var(--identity-band-border) solid
    color-mix(in srgb, var(--identity-mark) var(--identity-band-line-on-photo), transparent);
  background: color-mix(in srgb, var(--identity-mark) var(--identity-band-fill-on-photo), transparent);
  transform: rotate(var(--brand-stripe-angle));
}

/* Every landscape figure carries the primary p.43 corner mark. The old
   .split-ribbon is retired site-wide by the display:none above. Owner,
   27 Aug 2026: full-strength artwork, fully INSIDE the frame. PHYSICAL
   bottom-right, never mirrored in RTL. Partner logos and formal portraits
   remain clean by their named variants below.
   RETUNED 28 Aug 2026: --photo-mark-width scales within the shared content
   tier, from a visible minimum on card photography to a capped proportion on
   wide figures. This figure, the About photo and the feature project card
   therefore stay in one responsive mark system.
   Plain background-image, same as every other p.43 mark on the site - no
   filter, no clip-path. A per-element SVG filter was tried here 28 Aug 2026
   to compensate for the artwork reading thin at this figure's box size, but
   it made this one instance a visibly different rendering technique than
   the identical mark everywhere else (card stickers, hero, About visual) -
   owner flagged the inconsistency and asked for the shared system only.
   --photo-mark-width alone (already raised to 44% in sdeira-tokens.css)
   carries the size fix; there is no separate thickness knob, matching
   every other caller of this artwork. */
.split-media::after {
  content: "";
  position: absolute;
  z-index: 2;
  bottom: 0;
  right: 0;
  inline-size: var(--photo-mark-width);
  aspect-ratio: var(--photo-mark-p43-ratio);
  background: url("../images/patterns/lines.svg?v=d874099296964b63") no-repeat right bottom / contain;
  pointer-events: none;
}
/* A supplied partner logo is not a photograph and must never carry Sdeira's
   artwork. A pending portrait has no published photograph to mark. */
.split--logo .split-media::after,
.split--logo-wide .split-media::after,
.person-rows .split-media::after { display: none; }

/* Project cards (.project-card-media is BOTH .split-media and its own
   component) were getting two marks on one photo at once: this generic
   .split-media::after (bottom-right) plus .project-card-media::before
   (top-left, defined above alongside .needs-route-media::before). Site
   owner, 31 Aug 2026, viewing the construction-progress cards on
   /business/property-development/: keep only the top-left mark, drop this
   second bottom-right one for project cards specifically. */
.project-card-media::after { display: none; }

@media (max-width: 920px) {
  /* On narrow screens the full-width figure stays clean and readable. */
  .split-media::after { display: none; }
}

/* Media first. `order` rather than a second grid definition, so the two
   variants cannot drift apart. */
.split--reverse .split-media { order: -1; }

/* A LONG-SENTENCE HEADLINE BESIDE ITS OWN FIGURE, e.g. the news index's
   pinned press release. Every other .split caller writes a short, 2-6-word
   section title into its H2, so the bare `h2` rule (--type-section, a
   clamp(2.5rem, 4.3vw, 5rem) page heading) is correct for them and wrong
   here: a full editorial sentence at that size ran to ~9 lines on mobile and
   dwarfed the figure beside it. --type-headline already exists for exactly
   this shape - "a caption headline beside its own dedicated figure... one
   step above a card title, without reaching Feature's spotlight size" (see
   design-system/type.html) - and person-rows already uses it for the same
   reason on an H3. This is the same token applied to .split's H2 instead,
   scoped to this variant so the 20 other .split callers keep the section-
   title size unchanged. */
.split--headline .split-copy h2 {
    font-size: var(--type-headline);
    line-height: var(--leading-headline);
    font-weight: 700;

    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
}

.split--headline .split-copy p{
  display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
}

/* Both columns are copy - the Mission and Vision pair. The architecture record
   is explicit that these render as statement-as-heading, NOT as a boxed quote:
   the owner removed .article-quote on 2026-08-04 and it is not reintroduced
   here or anywhere. */
.split--text { align-items: start; }

.split--text .split-copy h3 {
  font-size: var(--type-card);
  line-height: var(--leading-card);
  /* No trailing margin. It was 16px, and on the LAST child of the column it
     did nothing but add to the space between the two statements - so the
     panel separated its blocks by the row gap PLUS an invisible 16, while a
     label sat 40px from the sentence it labels. A label belongs to its
     statement; the two statements belong apart. */
  margin-block-end: 0;
}

/* THE LABEL HUGS ITS STATEMENT, AND THE TWO BLOCKS STAND APART. That is the
   only proportion this panel has to get right and it had it backwards: 40px
   between "Vision" and the sentence it labels, against 74px between the two
   statements. Near enough to equal that the eye grouped a label with the
   statement ABOVE it.

   Two things were paying for that 40. The 20px column rhythm, and the
   kicker's own line box - --leading-label puts 32.4px around an 18px glyph, so
   a bare margin is never what it looks like. Both are tightened here; the mark
   before the kicker is centred on the text box, so it follows. */
.split--text .split-copy .kicker { line-height: 1.2; }
.split--text .split-copy > * + * { margin-block-start: 2px; }

/* And the separation is stated rather than inherited, so the ratio survives a
   retune of the shared column gap. */
.split--text { row-gap: clamp(56px, 5.5vw, 88px); }

/* .split--figures (kicker/heading beside a photo, a stat-strip attached below)
   was /about/numbers's only user. Removed 31 Aug 2026 with that section: no
   body text under the heading left the copy column stranded at the top of a
   column as tall as the photo, and the site owner asked for the photo gone
   rather than the gap patched. The section is now a standalone stat-strip. */

/* A LOGO IS NOT A CROP OF THE MASTER, and this is where that rule bites hardest:
   the media column's default treatment covers the slot and draws an identity
   band over it. Applied to a partner's logo that is two separate wrongs - a
   cropped wordmark, and Sdeira's own watermark laid over someone else's mark.
   The logo variant switches to contain, gives the mark a neutral tile to sit
   on, and suppresses the watermark. */
.split--logo .split-media,
.split--logo-wide .split-media {
  aspect-ratio: var(--media-card);
  display: grid;
  place-items: center;
  padding: clamp(24px, 3vw, 44px);
  /* A raised surface, not an outlined box. The tonal step is what says
     "the mark sits on its own ground"; a line around it says nothing the
     step does not already say. */
  background: var(--surface-raised);
}

.split--logo .split-media img {
  inline-size: auto;
  block-size: auto;
  max-inline-size: 100%;
  max-block-size: 100%;
  object-fit: contain;
}

.split--logo-body {
  grid-template-areas:
    "kicker kicker"
    "heading heading"
    "body media";
  align-items: start;
  row-gap: 20px;
}

.split--logo-body .split-copy { display: contents; }
.split--logo-body .split-copy > .kicker { grid-area: kicker; }
.split--logo-body .split-copy > h2 { grid-area: heading; }
.split--logo-body .split-copy > .split-copy-body { grid-area: body; }
.split--logo-body .split-copy > .cta-actions { grid-area: actions; }
.split--logo-body .split-media {
  grid-area: media;
  align-self: center;
}
.split--logo-body .split-media img { inline-size: min(100%, 320px); }

.split--logo .split-ribbon { display: none; }

/* Partner identity variant: keep the mark beside the heading, then let the
   supporting copy read at the full section measure below them. The mark uses
   the same raised logo surface, padding and neutral treatment as .split--logo;
   the section glass must never sit over a partner identity. */
/* A SIMPLE TWO-COLUMN ROW, not a per-row grid-template-areas scheme - site
   owner, 7 Sep 2026 (second pass, same section): the previous fix repeated
   "media" down every named row so the mark's own grid-area was one
   contiguous rectangle spanning the text column's height, which technically
   centers it against that height but still reads as pinned near the
   paragraph rather than simply centered in the row, and the explicit
   per-child grid-area assignments below (kicker/heading/body-one/body-two/
   actions, plus the nth-of-type paragraph targeting) are exactly the
   "تعقيد" the owner flagged: five named areas and four child selectors to
   get one thing - a logo centered beside its text - are more moving parts
   than the result needs. A flex row with align-items: center does the same
   job with none of it: .split-copy stays normal in-flow block content (no
   display: contents, no per-child grid-area), .split-media is the second
   flex item, and the browser centers both against whichever one is taller
   with no explicit height math anywhere. */
.split--logo-wide {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(24px, 4vw, 64px);
}

.split--logo-wide .split-copy {
  min-inline-size: 0;
  flex: 1 1 auto;
}

.split--logo-wide .split-copy > h2 {
  margin-block-start: 0;
}

.split--logo-wide .split-media {
  flex: 0 0 auto;
  aspect-ratio: auto;
  inline-size: min(100%, 320px);
}

.split--logo-wide .split-media img {
  inline-size: 100%;
  block-size: auto;
  max-inline-size: 100%;
}

.split--logo-wide .split-ribbon { display: none; }

/* A supplied transparent mark can sit directly on the section ground instead
   of receiving the neutral logo tile used by the default logo variant. */
.split--logo-wide.split--transparent .split-media {
  padding: 0;
  background: transparent;
}

/* Zoad's supplied lockup is portrait rather than a horizontal wordmark. Keep
   the shared logo-wide reading order, but give this existing logo variant a
   bounded media ratio so the mark does not dictate the section height. */
.split--logo-wide.split--logo .split-media {
  aspect-ratio: var(--media-card);
  min-block-size: 0;
  inline-size: min(100%, var(--logo-wide-portrait-inline));
}

.split--logo-wide.split--logo .split-media img {
  inline-size: auto;
  block-size: auto;
  min-block-size: 0;
  max-inline-size: 100%;
  max-block-size: 100%;
  object-fit: contain;
}

.split--logo-wide.split--logo .split-copy > p:nth-of-type(3) {
  max-inline-size: var(--logo-wide-copy-measure);
}

.split--logo-wide.split--copy-measured .split-copy > p:nth-of-type(2) {
  max-inline-size: var(--logo-wide-copy-measure);
}

.split--media-dominant .split-copy { display: contents; }
.split--media-dominant .split-copy > .split-heading {
  grid-area: heading;
  display: grid;
  gap: 0;
  margin-block-start: 0;
}
.split--media-dominant .split-heading h2 { margin-block-start: 0; }
.split--media-dominant .split-copy > .split-copy-body {
  grid-area: body;
  display: grid;
  gap: 10px;
  margin-block-start: var(--sec-head-gap);
}
.split--media-dominant .split-copy > .cta-actions { grid-area: actions; }
.split--media-dominant .split-media {
  grid-area: media;
  align-self: start;
  margin-block-start: var(--sec-head-gap);
  /* Explicit, not left to grid's default stretch: measured this collapsing to
     0x0 intermittently (about 1 run in 3, headless, both languages) on the
     no-actions mobile stack, correlated with the lazy img not yet complete
     when the grid track resolved. inline-size: 100% removes the dependency on
     stretch ever finishing before aspect-ratio needs a definite width to
     compute height from - a plain percentage doesn't have that race. */
  inline-size: 100%;
}

[dir="rtl"] .split--media-dominant {
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  grid-template-areas:
    "heading heading"
    "media body"
    "media actions";
}

.split--media-dominant-no-actions {
  grid-template-areas:
    "heading heading"
    "body media";
  row-gap: var(--sec-head-gap);
}
/* .split-copy > is load-bearing, not style: .split--media-dominant .split-copy
   > .split-copy-body (three classes) sets the with-actions margin-block-start,
   and a two-class override here loses that specificity fight and leaves it in
   place on top of the row-gap this variant already sets above - doubling the
   heading-to-body gap. Matching the combinator ties specificity, and source
   order then wins it.

   align-self was center until 15 Aug 2026: with a short copy block next to a
   tall photo (the usual shape here - two or three sentences beside a portrait-
   ish crop) that left as much dead space above the first line as below the
   last one, so the copy read as floating rather than anchored to its own
   heading. Site owner flagged this on the CERT partnership section
   specifically; every other instance of this variant has the same shape, so
   it moves to start everywhere rather than as a one-page exception. */
.split--media-dominant-no-actions .split-copy > .split-copy-body,
.split--media-dominant-no-actions .split-media {
  margin-block-start: 0;
  align-self: start;
}

[dir="rtl"] .split--media-dominant-no-actions {
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  grid-template-areas:
    "heading heading"
    "media body";
}

/* THE CAPTION BLOCK - the figure's second variant, and the only one there is.
   A year, what the year is, and one sentence saying why it matters.

   IT IS A FILL, NOT A SCRIM, and that is the decision this rule exists to
   record. A scrim reads as text laid over a photograph and inherits every
   argument about the alpha floor; the block reads as a panel the photograph is
   mounted against, which is what it is. It therefore takes --accent-fill
   (#B85D22, white at 4.54:1 - see the contrast page) and NEVER --accent
   (#D8702C, white at 3.35, below AA). No tone of the fill is ever authored
   locally: this component has one colour.

   IT OVERHANGS ON PURPOSE, on the reading edge and the bottom. The overhang is
   what stops the block reading as a label stuck inside the frame, and it is
   why .split-media is `overflow: visible` above. Both offsets are PHYSICAL and
   both have a mirrored half in the direction block. */
.split-badge {
  position: absolute;
  bottom: -36px;
  left: -36px;
  z-index: 2;
  max-inline-size: min(72%, 340px);
  padding: clamp(22px, 2.4vw, 34px) clamp(26px, 3vw, 42px);
  background: var(--accent-fill);
  color: var(--brand-white);
  border-radius: var(--r);
}

/* The numeral takes the figure role. A NUMERAL IS NOT A HEADING - mapping it
   onto --type-card is exactly the habit --figure-size was added to end. */
.split-badge b {
  display: block;
  font-family: var(--font-display);
  font-size: var(--figure-size);
  line-height: 1;
  font-weight: 400;
  color: var(--brand-white);
}

/* What the numeral is. Body size at badge weight, not a smaller role: it is
   read at the same distance as the sentence under it. */
.split-badge span {
  display: block;
  margin-block-start: 18px;
  font-size: var(--type-body);
  line-height: var(--leading-body);
  font-weight: var(--badge-weight);
  color: var(--brand-white);
}

/* The optional sentence. White only - every tint of white on this fill falls
   under 4.5, so --muted-on-dark is not available here. */
.split-badge p {
  margin: 0;
  font-size: var(--type-body);
  line-height: var(--leading-body);
  color: var(--brand-white);
}

.split-badge span + p { margin-block-start: 14px; }

/* The identity variant uses the same overhanging figure geometry as the
   numeric badge, but keeps the supplied company mark at its own proportions. */
.split-logo-badge {
  position: absolute;
  inset-block-end: -36px;
  inset-inline-start: -36px;
  z-index: 2;
  inline-size: min(50%, 320px);
  aspect-ratio: 4 / 3;
  display: grid;
  place-items: center;
  padding: clamp(20px, 2.5vw, 32px);
  min-block-size: 0;
  background: var(--surface-raised);
  border-radius: var(--r);
}

.split-logo-badge:is(a) {
  color: inherit;
  text-decoration: none;
}

.split-logo-badge img {
  display: block;
  inline-size: min(90%, 280px);
  block-size: auto;
  max-block-size: 100%;
  object-fit: contain;
}

.split-logo-badge--portrait {
  inline-size: min(21%, 112px);
  aspect-ratio: 1 / 1;
}

/* A partner identity can sit beside an image-led heading when the overhang
   would separate the mark from the message it identifies. The same white
   badge and logo proportions remain in use; only the placement changes. */
.split--heading-logo .split-heading {
  grid-template-columns: minmax(0, 1fr) minmax(0, 320px);
  column-gap: var(--sec-head-gap);
  row-gap: 0;
}

.split--heading-logo .split-heading > .kicker {
  grid-column: 1 / -1;
}

.split--heading-logo .split-heading > h2 {
  grid-column: 1;
  grid-row: 2;
}

.split--heading-logo .split-heading > .split-logo-badge--heading {
  position: static;
  grid-column: 2;
  grid-row: 2;
  align-self: center;
  justify-self: end;
  inline-size: min(100%, 320px);
}


/* =============================================================================
   3. C2 - .stat-strip : PUBLISHED FIGURES
   -----------------------------------------------------------------------------
   One connected strip holds the published figures, with the identity slant as
   an internal separator between values. Two implementation constraints apply:

   - The homepage's description uses -webkit-line-clamp: 3. Banned. This
     reserves three lines instead, so a longer line runs on rather than being
     hidden from the visitor.
   - A figure is not a card heading. Values therefore use --figure-size while
     labels and dates retain their existing roles.
   ========================================================================== */

.stat-strip {
  display: grid;
  grid-template-columns: repeat(var(--grid-cols, 4), minmax(0, 1fr));
  background: var(--surface-raised);
  border-radius: var(--r);
  overflow: hidden;
}

/* Attached figures follow a split/figure above them with the same section
   rhythm used by the homepage About composition. */
/* .stat-strip--attached { margin-block-start: var(--sec-head-gap); } */

/* CARD GROUNDS, INVERTED (Liwa migration E1, docs/content/liwa-migration.md;
   owner, 21 Aug 2026). The page floor is White, so a card on it takes Light
   Grey - promoted here (this file is shared by the homepage and every
   generated page) rather than kept in assets/css/sdeira-liwa.css, which is
   homepage-only, so sub-pages were previously white-on-white. */
.stat { background: var(--surface-sunken); }
.project-card--secondary .project-card-body { background: var(--surface-sunken); flex-grow: 1; }
.news-card { background: var(--surface-sunken); }
.news-item { background: var(--surface-sunken); }
/* .project-card-base and its copy panel set --surface-raised earlier with
   higher specificity than .news-item, so they escaped the same E1 ground
   correction and disappeared against a White section. Keep both levels on
   the card's shared sunken surface; the inverse below restores White where a
   project card sits on a sunken section. */
/*.project-card-base.project-card-base,
.project-card-base .project-card-copy { background: var(--surface-raised); }*/
/* Beat the shared :not(.news-card--feature) rule with a doubled class. */
.news-card.news-card { background: var(--surface-sunken); }

/* THE FLOOR CAN ALREADY BE SUNKEN. E1 assumed a White page floor, but a
   section authored `pad--sunken` (e.g. /en/news/'s "More from the newsroom",
   content/pages/news/index.toml) is Light Grey itself, so a Light Grey card
   on it repeats the same principle backwards: card and floor become the same
   tone and the card disappears. Same fix `.pad--sunken .org-company` already
   uses a few hundred lines down - on a sunken floor the card inverts to
   White instead, verified by screenshot on /en/news/ (26 Aug 2026 QA pass,
   docs/quality/liwa-subpage-rollout-qa-checklist-2026-08-26.md). */
.pad--sunken :is(.stat, .news-card, .news-item) { background: var(--surface-base); }
.pad--sunken .project-card-base.project-card-base,
.pad--sunken .project-card-base .project-card-copy { background: var(--surface-base); }

/* THE FIELD FAMILY STEPS UP ON THE SUNKEN GROUND, 23 Sep 2026. A field takes
   --surface-sunken at rest by the site's own convention (.search-field and
   .site-search-field in sdeira-search.css, .jobs-search input and
   .news-dropdown-toggle in sdeira-subpage.css), and that is the section's own
   colour the moment a section declares `pad--sunken`: on /careers/jobs/ the
   search field and the three filter toggles were #d4d4d4 on #d4d4d4 and read as
   no control at all. Site owner: "بس عدلهم يكون ابيض حتى يختلف عن الخلفية". It is
   the same step up the card family makes above, and it lives here rather than in
   each control's own block because the ground decides it, not the control. The
   open and hover states of a toggle keep this fill deliberately (see
   .news-dropdown-toggle:hover), and :focus-within on a search field already
   steps to --surface-sunken-strong, which resolves to White here. */
.pad--sunken :is(.search-field, .site-search-field, .jobs-search input, .news-dropdown-toggle) {
  background: var(--surface-raised);
}

.stat {
  position: relative;
  min-width: 0;
  padding: 30px clamp(18px, 1.9vw, 30px);
  display: grid;
  grid-template-columns: 42px minmax(0, 1fr);
  align-content: start;
  gap: 6px 14px;
}

/* The diagonal separator, at the mark's own slant. Physical `left`, mirrored in
   the direction block below; the angle itself is never mirrored. */
.stat + .stat::before {
  content: "";
  position: absolute;
  top: 50%;
  /* Sitting at the preceding cell's own padding edge (its full clamp(18px,
     1.9vw, 30px) as the offset) put the mark's centre exactly where a full
     label line naturally ends - a long line ("Best Workplaces in Real
     Estate, Middle East") reads right up to that edge whether it wraps to
     two lines or, past ~1750px, fits on one, and a 72px mark at
     --brand-stripe-angle (45deg) sweeps ~26px past its centre on both sides,
     which struck straight through the text. Splitting the offset in half
     centres the mark on the actual cell boundary instead, leaving the other
     half of the padding as clearance on the label side and the whole next
     cell's padding plus icon gap on the other; 24px keeps the sweep to ~9px,
     comfortably inside that halved clearance at every width the separator is
     shown (it is hidden below 1181px). */
  left: calc(clamp(18px, 1.9vw, 30px) / -2);
  width: 2px;
  height: 24px;
  background: var(--identity-mark);
  transform: translateY(-50%) rotate(var(--brand-stripe-angle));
}

.stat i.ph {
  grid-column: 1;
  grid-row: 1;
  align-self: center;
  color: var(--figure);
  font-size: var(--ic-lg);
}

.stat b {
  grid-column: 2;
  grid-row: 1;
  align-self: center;
  min-width: 0;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--figure-size);
  line-height: 1;
  font-weight: 500;
}

/* A figure that is a rank rather than a count (#21, #18) reads the "#" glyph
   as a typo at figure size - it is not a word in either language and sits at
   the same weight as the digits it is supposed to introduce. The fix stays
   inside the figure's own two existing type roles rather than inventing a
   third: the word takes --type-action, the same role already carried by the
   label line under it, at --muted so it recedes behind the digits instead of
   competing with them. Optional - .stat b with no .stat-figure-prefix child
   is unchanged. */
.stat-figure-prefix,
.stat-figure-suffix {
  margin-inline-end: 6px;
  font-size: var(--type-action);
  font-weight: 400;
  color: var(--muted);
}

.stat-figure-suffix {
  margin-inline-end: 0;
  margin-inline-start: 6px;
  white-space: nowrap;
}

.pad--inverse :is(.stat-figure-prefix, .stat-figure-suffix) { color: var(--muted-on-dark); }

.stat > span {
  grid-column: 2;
  grid-row: 2;
  color: var(--ink);
  font-size: var(--type-action);
  line-height: var(--leading-label);
}

.stat p {
  grid-column: 2;
  grid-row: 3;
  margin: 0;
  color: var(--muted);
  font-size: var(--type-meta);
  line-height: var(--leading-body);
  /* Reserved, not clamped. */
  /*min-block-size: calc(3 * 1lh);*/
}

/* When the date row is intentionally hidden, the note does not reserve its
   three-line publication slot and the cells use the compact facts rhythm. */
.stat-strip--compact .stat {
  padding-block: 18px;
}

.stat-strip--compact .stat p { min-block-size: auto; }

/* The as-at line. Every published figure on this site carries the date it
   refers to, because the source material contains figures three days apart that
   contradict each other - a number with no date is the thing that made them
   contradict. */
.stat time {
  grid-column: 2;
  grid-row: 4;
  color: var(--label);
  font-size: var(--type-caption);
}

/* On the dark ground the figure colour inverts: Sweihan fails contrast there
   and Razeen is the token file's answer. */
.pad--inverse .stat-strip { background: var(--card-dark); }
.operations-photo .stat-strip {
  background: var(--glass-card-fill);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(120%);
  backdrop-filter: blur(var(--glass-blur)) saturate(120%);
}
.pad--inverse .stat b,
.pad--inverse .stat > span { color: var(--brand-white); }
.pad--inverse .stat i.ph { color: var(--figure-on-dark); }
.pad--inverse .stat p { color: var(--muted-on-dark); }
.pad--inverse .stat time { color: var(--label-on-dark); }
/* The divider between stats reads --identity-mark (Liwa red) on a flat
   surface; that fails as decoration on a dark ground the same way the icon
   and every other role here does, so it takes the same on-dark figure tone
   they do rather than staying the light-surface accent. */
.pad--inverse .stat + .stat::before { background: var(--figure-on-dark); }

/* Operations figures intentionally read as text-only metrics. Collapse the
   icon column when that section omits its decorative marks. */
.operations-photo .stat { grid-template-columns: minmax(0, 1fr); }
.operations-photo .stat > :not(i) { grid-column: 1; }

/* Facts variant: label over value, no icon and no description. Used for a page
   hero's meta row and a project's At a Glance.

   IT IS THE SAME SLAB, and only the cell's contents differ. This variant used
   to break the strip into four detached tiles - background: none on the strip,
   its own --surface-raised and radius on each cell, a gap between them, and a
   56px slanted bar bled out of each cell's right edge as a "corner cut". Three
   things were wrong with it at once and they compounded:

     - Four marks. One diagonal per box, four across a row, against the one
       diagonal the base strip uses to separate the whole group. The identity
       stopped being a mark and became a texture.
     - The cut was drawn at 180% height and -40% top on a cell with
       overflow: hidden, so it read as a ragged sliced edge rather than as a
       cut corner - the boxes looked broken, not marked.
     - A fixed-height tile with a wrapping value ("2,780+ rooms · 25,728 beds")
       put its second line hard against the bottom edge, which is what reads as
       copy escaping its box.

   So the variant inherits the strip it belongs to: one raised slab, one radius,
   the group's own diagonal separators between cells, exactly like the homepage's
   figures row. Nothing here restates a value the base rule already sets. */
.stat-strip--facts .stat {
  grid-template-columns: minmax(0, 1fr);
  gap: 6px;
}

.stat-strip--facts .stat > span {
  grid-column: 1;
  grid-row: 1;
  color: var(--label);
  font-size: var(--type-meta);
}

.stat-strip--facts .stat b {
  grid-column: 1;
  grid-row: 2;
  align-self: start;
  font-size: var(--type-label);
}

/* Facts with a compact visual cue retain the facts hierarchy while using the
   shared feature icon role. */
.stat-strip--facts-icons .stat {
  grid-template-columns: var(--icon-feature) minmax(0, 1fr);
}

.stat-strip--facts-icons .stat > i.ph {
  grid-column: 1;
  grid-row: 1 / span 2;
  align-self: center;
  font-size: var(--icon-feature);
  color: var(--accent-strong);
}

.stat-strip--facts-icons .stat > span,
.stat-strip--facts-icons .stat > b { grid-column: 2; }


/* =============================================================================
   4. C3 - .icon-grid : ICON, TITLE, ONE LINE
   -----------------------------------------------------------------------------
   The icon is an unboxed display-role Phosphor glyph. The raised card already
   supplies the component surface; a second warm square behind every glyph
   duplicated emphasis and competed with the title.
   ========================================================================== */

.icon-grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-cols, 3), minmax(0, 1fr));
  gap: clamp(20px, 2.4vw, 34px);
}

/* Five equally weighted functions need a 3+2 rhythm, not five compressed
   columns. A six-track grid lets the final pair remain centred without making
   either card wider than the first three. */
.icon-grid--balanced-five {
  grid-template-columns: repeat(6, minmax(0, 1fr));
}

.icon-grid--balanced-five .icon-item { grid-column: span 2; }
.icon-grid--balanced-five .icon-item:nth-child(4) { grid-column: 2 / span 2; }
.icon-grid--balanced-five .icon-item:nth-child(5) { grid-column: 4 / span 2; }

/* Five linked responsibilities keep the same 3+2 rhythm, but the final pair
   stretches across the complete row instead of leaving a centred gap. */
.icon-grid--balanced-five-stretch {
  grid-template-columns: repeat(6, minmax(0, 1fr));
}

.icon-grid--balanced-five-stretch .icon-item { grid-column: span 2; }
.icon-grid--balanced-five-stretch .icon-item:nth-child(4),
.icon-grid--balanced-five-stretch .icon-item:nth-child(5) { grid-column: span 3; }

/* STACKED - a value list beside a .split figure, not a grid row. Site owner,
   27 Aug 2026: a 3-item icon-grid--amenities row read as too sparse over the
   empty ground below it (the "Our values" section on /about/, /about/our-
   story/ and /careers/why-sdeira/), so split() can now emit these items as
   the .split-copy column beside a real photograph instead of their own
   section. Column direction, one .icon-item per row - everything else
   (background, icon colour, no hover) is still .icon-grid--amenities'S own
   rule above; only the axis flips. */
.icon-grid--stacked {
  display: flex;
  flex-direction: column;
  gap: clamp(12px, 1.4vw, 18px);
}

/* Each tile is its own raised surface, the same tonal-step card shell
   .row-item and .news-item already use (--surface-raised + var(--r)), not a
   third card language. Un-boxed icon+title+line was the flattest thing on
   these pages - every "core functions" / "who we work with" grid on the site
   read as bare text floating on the section ground with nothing to look at.
   No hover wipe here: unlike .row-item these tiles carry no destination, so
   an interactive state would promise a click that does nothing. */
.icon-item {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: clamp(24px, 2.4vw, 32px);
  border-radius: var(--r);
  background: var(--surface-raised);
}

/* The identity band, not a second icon - and the AMBIENT tone, not the
   flat-surface one. The first attempt used --identity-band-fill (12%, solid
   outline) sized as a percentage of the card, which reads correctly on a
   full-height hero or CTA band and reads as a bold, oversized scribble on a
   card a few hundred pixels wide - a size and a strength both authored for a
   different host. This is the SAME shape at the tone .pad--edge already
   uses for its own quiet corner-of-section band
   (--identity-band-line-ambient 10% / --identity-band-fill-ambient 2%, the
   token file's own "ambient" pair).

   The geometry is now the band's own tokens throughout -
   --identity-band-top, --identity-band-height AND --identity-band-width,
   not a scaled-down guess at any of the three. A narrower custom width read
   as a different, thinner mark next to the one on photography; this is the
   same width so the two read as one mark at two sizes of host, not two
   marks. It runs the card's full height and past both edges the same way
   the real band overruns its host. Pulled in from the exact corner (8%,
   not flush with the edge) per earlier feedback: sitting right on the
   boundary read as clipped rather than placed. Physical `right`, mirrored
   below for RTL; the angle itself is never mirrored, per the same rule the
   hero and CTA bands already follow - language direction moves the band to
   the opposite edge, never flips its slant. */
.icon-item-band {
  position: absolute;
  z-index: 0;
  right: 8%;
  top: var(--identity-band-top);
  block-size: var(--identity-band-height);
  inline-size: var(--identity-band-width);
  border: var(--identity-band-border) solid
    color-mix(in srgb, var(--identity-mark) var(--identity-band-line-ambient), transparent);
  border-radius: var(--r);
  background: color-mix(in srgb, var(--identity-mark) var(--identity-band-fill-ambient), transparent);
  transform: rotate(var(--brand-stripe-angle));
  pointer-events: none;
}

[dir="rtl"] .icon-item-band { right: auto; left: 8%; }

.pad--inverse .icon-item-band {
  border-color: color-mix(in srgb, var(--brand-white) var(--identity-band-line-ambient), transparent);
  background: color-mix(in srgb, var(--brand-white) var(--identity-band-fill-ambient), transparent);
}

.icon-item-mark,
.icon-item h3,
.icon-item p,
.icon-item-action {
  position: relative;
  z-index: 1;
}

.icon-item-mark {
  display: grid;
  place-items: center;
  inline-size: 1em;
  block-size: 1em;
  color: var(--icon-feature-color);
  font-size: var(--icon-display);
}

.icon-item-mark > i.ph {
  inline-size: 1em;
  block-size: 1em;
  font-size: inherit;
}

/* `icon-grid--corner-mark`: the p.43 corner mark as a card's identity
   decoration, in place of the skewed .icon-item-band. The same lines.svg
   artwork every photograph on the site already carries in its bottom-right
   corner (.split-media::after, above), scaled down to card size the same
   way khadamat-theme.css already scales it for its own icon-grid cards -
   this is that same technique promoted into the shared component instead of
   staying a khadamat-only override, so any icon-grid section can opt in by
   name. .icon-item is already `position: relative; overflow: hidden`, which
   is what the mark's `absolute` anchors against and clips to. Physical
   `right`, mirrored for RTL below - the mark itself is never mirrored, the
   same rule every other p.43 placement on the site follows. */
.icon-grid--corner-mark .icon-item::after {
  content: "";
  position: absolute;
  z-index: 0;
  right: 0;
  bottom: 0;
  inline-size: 40%;
  aspect-ratio: var(--photo-mark-p43-ratio);
  background: url("../images/patterns/lines.svg?v=d874099296964b63") no-repeat right bottom / 100% 100%;
  pointer-events: none;
  /* Same hover-grow the mark already gets on a photo (.needs-route-media::after
     above): scale(1.1) off the corner it is anchored to, not the card centre,
     so it grows outward into the corner instead of drifting toward the middle.
     Site owner, 28 Aug 2026: apply the homepage's own icon-grid hover-scale to
     every icon-grid--corner-mark card sitewide. */
  transform: scale(1);
  transform-origin: 100% 100%;
  transition: transform var(--dur-normal, 200ms) var(--ease-out, ease-out);
}

[dir="rtl"] :is(.icon-grid--corner-mark, .icon-grid--interactive) .icon-item::after {
    right: auto;
    left: 0;
    transform-origin: 0% 100%;
    transform: translateX(100%) scale(-1, 1);
}

/* Interactive tiles (.icon-grid--interactive, no destination) grow on hover
   only; linked tiles (.icon-item--link, a real destination) grow on hover
   AND keyboard focus, the same split every other hover state on this card
   already keeps. */
.icon-grid--interactive .icon-item:hover::after,
.icon-grid--interactive .icon-item--link:is(:hover, :focus-visible)::after,
.icon-grid--corner-mark .icon-item--link:is(:hover, :focus-visible)::after {

    transform: scale(1.1);

}
[dir="rtl"] :is(.icon-grid--corner-mark, .icon-grid--interactive) .icon-item:hover::after,
[dir="rtl"] :is(.icon-grid--corner-mark, .icon-grid--interactive) .icon-item--link:is(:hover, :focus-visible)::after {
    transform: translateX(110%) scale(-1.1, 1.1);
}

.icon-item h3 {
  margin: 0;
  font-size: var(--type-card);
  line-height: var(--leading-card);
  font-weight: 700;
  color: var(--ink);
}

.icon-item p {
  margin: 0;
  min-block-size: calc(2 * 1lh);
  color: var(--muted);
}

/* THE VISIBLE "THIS CARD IS CLICKABLE" AFFORDANCE - .action-link, the
   system's one secondary-action treatment, the same one
   _needs_route_cards() already places inside its own whole-card link. A
   <span>, never a second <a>: .icon-item--link is already the one
   Tab-reachable target (see components.icon_grid()'s own docstring), so
   this has no href, no hover/focus of its own, and its underline-grow and
   icon-rotate instead key off the CARD's hover/focus state below - the same
   "the whole card reacts together" contract the wipe and the heading colour
   already keep.

   .action-link's own default (sdeira-subpage.css) is white text "placed
   over imagery" - illegible on this white card, so it takes --accent-strong
   at rest here, the same override .split-copy .action-link already makes
   for the same reason on a light ground. margin-block-start: auto pins it to
   the card's bottom edge regardless of how many lines the body above it
   wraps to, so it lines up across a row of uneven cards. */
.icon-item-action {
  margin-block-start: auto;
  color: var(--accent-strong);
  pointer-events: none;
}

/* Linked icon cards reuse the needs-route action markup. Keep its light-card
   resting state legible, then let the card wipe invert it on hover/focus. */
.icon-grid .needs-route-action {
  align-self: flex-start;
  margin-block-start: auto;
  color: var(--ink);
  pointer-events: none;
}

html[dir="ltr"] .needs-route-action > i.ph { transform: rotate(-35deg); }
html[dir="rtl"] .needs-route-action > i.ph { transform: rotate(35deg); }

html[dir="ltr"] .needs-route:is(:hover, :focus-within)
  .needs-route-action > i.ph,
html[dir="rtl"] .needs-route:is(:hover, :focus-within)
  .needs-route-action > i.ph,
:is(.icon-item--link:is(:hover, :focus-visible), .icon-grid--interactive .icon-item:hover)
  .needs-route-action > i.ph {
  transform: rotate(0deg);
}

:is(.icon-item--link:is(:hover, :focus-visible), .icon-grid--interactive .icon-item:hover)
  .icon-item-action {
  color: var(--brand-white);
}

:is(.icon-item--link:is(:hover, :focus-visible), .icon-grid--interactive .icon-item:hover)
  .needs-route-action {
  color: var(--brand-white);
}

:is(.icon-item--link:is(:hover, :focus-visible), .icon-grid--interactive .icon-item:hover)
  .icon-item-action::after {
  transform: scaleX(1);
}

:is(.icon-item--link:is(:hover, :focus-visible), .icon-grid--interactive .icon-item:hover)
  .icon-item-action > i.ph {
  transform: rotate(0deg);
}

/* Same inversion .row-item and .news-item-body already carry on a dark
   ground: the card body becomes --card-dark rather than staying white-on-dark. */
.pad--inverse .icon-item { background: var(--card-dark); }
.pad--inverse .icon-item-mark { color: var(--icon-feature-color-on-dark); }
.pad--inverse .icon-item h3 { color: var(--brand-white); }
.pad--inverse .icon-item p { color: var(--muted-on-dark); }
.pad--inverse .icon-item-action { color: var(--brand-white); }

/* Labels only: no description, denser grid, no card shell - this variant is a
   compact amenity list, not a tile grid, and stays flush with its ground. */
.icon-grid--labels-only .icon-item {
  flex-direction: row;
  align-items: center;
  gap: 12px;
  padding: 0;
  border-radius: 0;
  background: none;
}

.pad--inverse .icon-grid--labels-only .icon-item { background: none; }

.icon-grid--labels-only .icon-item-mark {
  color: var(--icon);
  font-size: var(--icon-default);
  flex: 0 0 auto;
}

.pad--inverse .icon-grid--labels-only .icon-item-mark { color: var(--icon-on-dark); }

/* Amenities: a compact raised surface with the feature-colour glyph. It is
   intentionally free of an identity band: six repeated bands turn the mark
   into background texture, while the orange icon already supplies hierarchy.

   BACKGROUND ONLY, NO HOVER - site owner, 27 Aug 2026, from a screenshot of
   /en/about/'s "Our values": these tiles carry no `href` (a closed,
   informational set - see the icon-item--link/icon-grid--interactive
   docstring above), and a hover state on a non-link tile promises a click
   that does nothing, which that same docstring rules out. Confirmed: a
   background is fine, a hover wipe is not, for this one variant.

   --surface-sunken, not --surface-raised: "a compact RAISED surface" was
   always the intent (see above), but .icon-item's base background is
   --surface-raised (White) and every live use of this variant sits on the
   site's default White floor - the same E1 card-ground bug .stat/.news-
   card/.news-item/.row-item all had, just never caught here because
   nothing about the white-on-white failure is visually loud on its own.
   The .pad--sunken inversion below is not speculative: about/our-story.toml
   and careers/why-sdeira.toml both already use this variant ON a sunken
   floor, where the fix would go white-on-white in the other direction
   without it. */
.icon-grid--amenities .icon-item {
  flex-direction: row;
  align-items: center;
  background: var(--surface-sunken);
}

.pad--sunken .icon-grid--amenities .icon-item { background: var(--surface-raised); }

/* THE P.43 CORNER MARK, in place of the photograph a split-with-figure
   layout would have needed here. Site owner, 27 Aug 2026: the flat 3-across
   row still read as empty; this is the same lines.svg mark the shared card
   photography (.needs-route-media, .project-card-media) and the closing CTA
   band (.cta-glass) already carry, applied to a flat card ground instead of
   a photograph - one more caller of an existing mark, not a new one. Sized
   off the card's own block-size (it is a short row, not a tall tile) the
   same way the sub-page hero's own corner marks are. z-index 0 sits above
   the card's flat background and below the icon/heading (z-index 1, the
   existing .icon-item-mark/h3 rule a few lines down). */
.icon-grid--amenities .icon-item::after {
  content: "";
  position: absolute;
  inset: auto 0 0 auto;
  z-index: 0;
  block-size: 130%;
  aspect-ratio: var(--photo-mark-p43-ratio);
  background: url("../images/patterns/lines.svg?v=d874099296964b63") no-repeat right bottom / 100% 100%;
  pointer-events: none;
}
.icon-item {
    position: relative;
    overflow: hidden;
}
.icon-grid--interactive .icon-item::after {
    content: "";
    position: absolute;
    z-index: 0;
    right: 0;
    bottom: 0;
    inline-size: 40%;
    aspect-ratio: var(--photo-mark-p43-ratio);
    background: url(../images/patterns/lines.svg?v=d874099296964b63) no-repeat right bottom / 100% 100%;
    pointer-events: none;
    transform: scale(1);
    transform-origin: 100% 100%;
    transition: transform var(--dur-normal, 200ms) var(--ease-out, ease-out);
}
/* Mirrored in RTL, unlike the D3 card-photography mark (.needs-route-media
   ::after / .project-card-media::after), which stays physical because
   nothing else on those cards moves with direction. Here .icon-item-mark
   does: .icon-grid--amenities's row layout ([dir="rtl"] .icon-item-mark
   above) puts the icon at the card's logical start, which is the physical
   right in Arabic - the same corner this mark was flush against, so the
   icon sat inside the diagonal instead of beside it. Moving the mark to the
   opposite corner, the same call .icon-item-band already makes two rules up,
   keeps the two clear of each other in both languages.

   THE ARTWORK ITSELF ALSO HAS TO MIRROR, NOT JUST THE BOX. background-size
   here is 100% 100%, so the image is stretched to fill the box exactly and
   background-position has no effect - repositioning the box alone left the
   unmirrored artwork's diagonal running the same absolute direction as the
   English corner, which reads as backwards/wrong in the Arabic corner
   instead of as a matching mirror image. scaleX(-1) mirrors the artwork in
   place; the box's own inset above already puts it at the correct physical
   corner, so no position offset is needed. */
[dir="rtl"] .icon-grid--amenities .icon-item::after {
  inset: auto auto 0 0;
  transform: scaleX(-1);
}

.icon-grid--amenities .icon-item-mark {
  flex: 0 0 auto;
  color: var(--accent-strong);
  font-size: var(--icon-feature);
}

.pad--inverse .icon-grid--amenities .icon-item-mark {
  color: var(--icon-feature-color-on-dark);
}

/* Two doors into the same hover, never two implementations of it.

   .icon-item--link is the real one: a card whose `href` is a verified
   destination (tel:, mailto:, a confirmed map link - never invented, see
   components.icon_grid()) renders as a single Tab-reachable anchor, the same
   contract .row-item--link already keeps. Its hover/focus is a genuine
   navigation affordance.

   .icon-grid--interactive is the decorative exception: the base .icon-item
   docstring above is explicit that a tile with no destination carries no
   hover by default, because a hover state on a non-link tile otherwise
   promises a click that does nothing. Opt in per grid only for a closed,
   purely informational set where the hover is decoration, not navigation;
   every other .icon-grid on the site keeps the documented no-hover default.

   Never both on the same tile - a tile is either a real destination or a
   decorative exception, not both at once.

   THE FILL IS THE ICON'S OWN RESTING COLOUR, never a new one: the mark
   already reads --icon-feature-color (--accent-strong, #A34D12), which is
   the same value --surface-brand-deep already ships white text on, so
   inverting to it and turning the icon/heading/body white is a tone flip,
   not a ninth colour. The identity band switches to the white ambient pair
   and shifts along its own slant - the "line shift" is that band's existing
   diagonal, not a new element.

   THE FILL TRAVELS, IT DOES NOT SNAP. An instant background swap read as a
   flash rather than a hover. This reuses .row-item's own wipe mechanism
   verbatim - the --wipe-p custom property (already registered once, above,
   as an animatable <percentage>) driving a gradient stop across the tile
   at --motion-wipe-gradient-angle - rather than inventing a second travelling
   fill. .icon-item is already `overflow: hidden` and its mark/heading/body
   already sit at z-index 1 (for the identity band below), so the wipe layer
   needs nothing new to stack under them. */
/* The base "whole card is one link" contract - color/underline reset and
   focus ring - is .row-item--link's, below. Shared here rather than
   re-declared, since both bodies were identical. */

:is(.icon-item--link, .icon-grid--interactive .icon-item) {
  transition: color var(--dur-slow) var(--ease-out);
  background-color: var(--surface-sunken);
}

/* The wipe and the copy leave together. Without this child transition the
   headings snap back to their dark resting colour while the fill is still
   receding, leaving dark text on the warm wipe during pointer exit. */
:is(.icon-item--link, .icon-grid--interactive .icon-item)
  :is(.icon-item-mark, h3, p, .icon-item-action) {
  /* Hold the resting colour until the wipe has left on pointer exit; the
     shorter colour response then clears the last frame instead of exposing
     dark text over the still-present fill. */
  transition: color var(--dur-fast) var(--ease-out)
    calc(var(--dur-base) - var(--dur-fast));
}

:is(.icon-item--link:is(:hover, :focus-visible), .icon-grid--interactive .icon-item:hover)
  :is(.icon-item-mark, h3, p, .icon-item-action) {
  transition-delay: 0s;
}

:is(.icon-item--link, .icon-grid--interactive .icon-item)::before {
  content: "";
  position: absolute;
  z-index: 0;
  inset: 0;
  --wipe-p: 0%;
  --wipe-fill: var(--icon-feature-color);
  background-image: linear-gradient(var(--motion-wipe-gradient-angle),
    var(--wipe-fill) 0 var(--wipe-p),
    transparent var(--wipe-p) 100%);
  transition: --wipe-p var(--dur-slow) var(--ease-out);
  pointer-events: none;
}

/* Entry is deliberate; exit is a quick desktop acknowledgement. */
:is(.icon-item--link, .icon-grid--interactive .icon-item)::before {
  transition-duration: var(--dur-base);
}

:is(.icon-item--link:is(:hover, :focus-visible), .icon-grid--interactive .icon-item:hover)::before {
  --wipe-p: 100%;
  transition-duration: var(--dur-slow);
}

:is(.icon-item--link:is(:hover, :focus-visible), .icon-grid--interactive .icon-item:hover)
  :is(.icon-item-mark, h3, p, .icon-item-action) {
  color: var(--brand-white);
}

.icon-item-band {
  transition: transform var(--dur-base) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}

:is(.icon-item--link:is(:hover, :focus-visible), .icon-grid--interactive .icon-item:hover) .icon-item-band {
  border-color: color-mix(in srgb, var(--brand-white) var(--identity-band-line-ambient), transparent);
  background: color-mix(in srgb, var(--brand-white) var(--identity-band-fill-ambient), transparent);
  transform: translateX(10px) rotate(var(--brand-stripe-angle));
}

[dir="rtl"] :is(.icon-item--link:is(:hover, :focus-visible), .icon-grid--interactive .icon-item:hover) .icon-item-band {
  transform: translateX(-10px) rotate(var(--brand-stripe-angle));
}


/* =============================================================================
   5. C4 - .step-list : NUMBERED STEPS, AND THE TIMELINE
   -----------------------------------------------------------------------------
   The number marker is the homepage's .company-num (live, 01-04 on the company
   cards). The retired .process-step is deliberately NOT the derivation source:
   it was never seen by a visitor and is still untranslated.

   The timeline is the same component with the marker showing a year instead of
   an ordinal and a connecting rule down the markers. One implementation, one
   variant - a timeline is a step list whose steps are dated.
   ========================================================================== */

.step-list {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(24px, 2.6vw, 38px);
  padding: clamp(24px, 2.6vw, 38px);
  /* Flat fill, no ground texture, 31 Aug 2026: this is a card sitting on a
     section, not a section ground itself - the same "a card is a surface in
     front of the ground, not another ground" rule --surface-raised cards
     already follow (sdeira-tokens.css section 5b). The retired diagonal
     hatch (--ground-sand) painted here twice: once as the section's own
     ground and once again inside this tile. */
  background: var(--surface-sunken);
  border-radius: var(--r);
}

.step {
  position: relative;
  display: grid;
  align-content: start;
  gap: 10px;
  min-width: 0;
}

/* THE MARK IS THE NUMERAL, NOT A CHIP AROUND IT. This carried a solid
   --surface-inverse box until 2026-08-09: a 56px dark square with a
   --type-label digit centred in it, which is a UI badge's proportions on a
   role the token file names for a different job. --figure-size and --figure
   exist for exactly this - "numerals: years, stats, steps" is the role's own
   description - and a step number sat outside both, the same drift the
   timeline's year was already pulled out of below. Removing the box is not
   losing an affordance: nothing here was ever a button, and the number is
   larger and reads faster without one. */
.step-mark {
  display: grid;
  place-items: start;
  min-inline-size: 0;
  block-size: auto;
  padding-inline: 0;
  color: var(--figure);
  font-family: var(--font-display);
  font-size: var(--figure-size);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.step h3 {
  margin: 0;
  font-size: var(--type-card);
  line-height: var(--leading-card);
  color: var(--ink);
}

.step p {
  margin: 0;
  color: var(--muted);
}

/* A short, ordered model can use the same one-surface scan as the figures
   strip. This keeps the step hierarchy and borrows only its separator. */
.step-list--divided {
  gap: 0;
  padding: 0;
  background: var(--surface-sunken);
  overflow: hidden;
}

/* SUNKEN ON A RAISED SECTION, 1 Sep 2026: flagged in a preview comment - the
   careers page's hiring-process section (#process.pad--raised) is white, so
   the base rule above put this white card directly on the white section
   ground with no separation at all. Every other .step-list--divided on the
   site avoids this: the photo-band variant overrides to a glass fill
   (.step-list-photo .step-list--divided below - investments and landbank
   also use id="process" but carry that class, so this selector excludes
   them explicitly rather than relying on source order against an ID), and
   the remaining plain use (submit-cv) sits on .pad--sunken, where white
   card on Light Grey ground is correct contrast. Careers is the one case
   with no ground step under it, so it needs the sunken card itself. */
#process.pad--raised .step-list--divided {
  background: var(--surface-sunken);
}

/* THE SAME DEFECT, THE MIRROR CASE, 3 Sep 2026: submit-cv's steps section
   (#next) is a plain White .pad since its ground swap of the same day - the
   form section above it took the Grey ground. The divided card's own raised
   fill then merged into the section exactly as #process's did, so this is
   the same fix from the other side: section White, card steps to the site's
   Light Grey field tone. Scoped by id alongside the rule above rather than
   widening the base component, whose raised fill stays correct everywhere
   else it is documented. */
#next .step-list--divided {
  background: var(--surface-sunken);
}

.step-list--divided .step {
  padding: 30px clamp(18px, 1.9vw, 30px);
}

/* A two-column ordered model keeps a short four-stage process balanced across
   two rows while retaining the same raised divided surface. */
.step-list--two-columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }

.step-list--two-columns.step-list--divided .step:nth-child(odd)::before,
.step-list--two-columns.step-list--divided .step:nth-child(even)::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: var(--step-separator-thickness);
  height: var(--step-separator-length);
  background: var(--identity-mark);
  transform: translateY(-50%) rotate(var(--brand-stripe-angle));
}

.step-list--two-columns.step-list--divided .step:nth-child(odd)::before { content: none; }

/* Three columns are the base step-list composition. A separator belongs only
   between neighbours in a row, never before the next row begins. */
.step-list--divided .step:nth-child(3n + 2)::before,
.step-list--divided .step:nth-child(3n)::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: var(--step-separator-thickness);
  height: var(--step-separator-length);
  background: var(--identity-mark);
  transform: translateY(-50%) rotate(var(--brand-stripe-angle));
}

/* Large numerals invert to Razeen on a dark ground, the same swap the figure
   role makes everywhere else on the site - see --figure-on-dark in the token
   file. Scoped through .step-list so it outranks .step-list--timeline
   .step-mark below rather than merely preceding it: equal specificity plus
   source order meant a timeline inside a dark section silently kept the
   light-ground treatment before this rule existed. */
.pad--inverse .step-list .step-mark { color: var(--figure-on-dark); }

.pad--inverse .step h3 { color: var(--brand-white); }

/* THE TIMELINE. Rebuilt: it was nine boxes stacked down a hairline, which is
   the flattest thing a nine-station history can be rendered as, and it read as
   a list rather than as a passage of time.

   It is now a spine with the stations alternating either side of it. That is
   not decoration for its own sake - it does three things a single column
   cannot: each station gets its own space, so a two-line description no longer
   pushes the next year down; the sequence is legible at a glance because the
   eye follows the zig-zag instead of scanning a column; and it fills the width
   the section actually has rather than leaving two thirds of it empty.

   THE SPINE IS THE MARK, NOT A RULE. It is Sweihan at a low tone - the same
   material as the section grounds above - so the line down the middle of the
   page belongs to the identity rather than being a border, which this system
   does not have. The node on it is a square at the mark's own slant, not a
   dot: this system has one shape vocabulary and it is not circles.

   Below 860px it collapses to a single column with the spine at the start
   edge, which is the only honest phone layout for a sequence. */
.step-list--timeline {
  position: relative;
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
  padding: 0;
  background: none;
  border-radius: 0;
}

/* Physical `left`, a percentage so it tracks the column split rather than a
   fixed number that would drift from it at every width. */
.step-list--timeline::before {
  content: "";
  position: absolute;
  top: 12px;
  bottom: 12px;
  left: calc(50% - 1px);
  width: 2px;
  background: color-mix(in srgb, var(--identity-mark) 26%, transparent);
}

.step-list--timeline .step {
  position: relative;
  grid-template-columns: minmax(0, 1fr);
  inline-size: calc(50% - clamp(28px, 3vw, 46px));
  gap: 8px;
  padding: clamp(18px, 1.9vw, 26px) 0;
  margin-block-end: clamp(24px, 2.6vw, 38px);
}

/* Odd stations on the start side, even ones on the end side. */
.step-list--timeline .step:nth-child(even) { margin-inline-start: auto; }

.step-list--timeline .step::after {
  content: "";
  position: absolute;
  top: clamp(24px, 2.4vw, 34px);
  right: calc(clamp(28px, 3vw, 46px) * -1 - 5px);
  width: 10px;
  height: 10px;
  background: var(--identity-mark);
  transform: rotate(var(--brand-stripe-angle));
}

.step-list--timeline .step:nth-child(even)::after {
  right: auto;
  left: calc(clamp(28px, 3vw, 46px) * -1 - 5px);
}

/* The year IS the marker now - display type in the figure colour, not a chip
   with a fill behind it. One fewer box on the page and a far bigger date. */
.step-list--timeline .step-mark {
  margin-block-end: 4px;
  line-height: 1;
}

.step-list--timeline .step h3 { grid-column: 1; align-self: start; }
.step-list--timeline .step p { grid-column: 1; }
.pad--inverse .step-list--timeline .step-mark {
  color: var(--figure-on-dark);
}
.pad--inverse .step-list--timeline::before {
  background: color-mix(in srgb, var(--brand-white) 22%, transparent);
}

/* Compact keeps the short sequence on one reading line per stage. */
.step-list--compact { grid-template-columns: minmax(0, 1fr); }

/* A short history is scanned as milestones, not as a vertical paragraph.
   It retains the shared divided separators and drops 3 → 2 → 1 responsively. */
.step-list--compact.step-list--divided { grid-template-columns: repeat(3, minmax(0, 1fr)); }

.step-list--compact .step-mark {
  color: var(--figure);
}

/* Chaptered history: one continuous corporate story, grouped into three
   phases rather than nine equal cells. It remains the step-list component;
   only the grouping and heading hierarchy change. */
.step-list--chapters {
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(16px, 1.8vw, 24px);
  padding: 0;
  background: none;
  border-radius: 0;
}

.step-chapter {
  position: relative;
  display: grid;
  grid-template-columns: minmax(220px, 30%) minmax(0, 1fr);
  overflow: hidden;
  border-radius: var(--r);
  background: var(--surface-raised);
}

.step-chapter-header {
  position: relative;
  overflow: visible;
  display: grid;
  align-content: center;
  gap: 12px;
  min-width: 0;
  padding: clamp(24px, 2.6vw, 38px);
  background: var(--surface-raised);
}

/* One white chapter card, with the established divided-step marker doing the
   separation. The previous outlined band made the header look like a second
   surface; this quiet tonal marker keeps the same shared system geometry
   without competing with the dates or chapter label. */
.step-chapter-band {
  position: absolute;
  z-index: 0;
  inset-inline-end: calc(var(--step-separator-thickness) / -2);
  top: 50%;
  inline-size: var(--step-separator-thickness);
  block-size: var(--step-separator-length);
  background: var(--line);
  transform: translateY(-50%) rotate(var(--brand-stripe-angle));
  pointer-events: none;
}

.step-chapter-header > :not(.step-chapter-band) {
  position: relative;
  z-index: 1;
}

.step-chapter-header h3 {
  margin: 0;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--type-card);
  line-height: var(--leading-card);
  font-weight: 400;
}

.step-chapter-header p {
  margin: 0;
  color: var(--figure);
  font-family: var(--font-display);
  font-size: var(--type-label);
  line-height: var(--leading-label);
}

.step-chapter-items {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: stretch;
}

.step-list--chapters .step {
  align-content: center;
  gap: 12px;
  padding: clamp(24px, 2.6vw, 38px);
}

.step-list--chapters .step-mark {
  color: var(--figure);
  font-size: var(--figure-size);
}

.step-list--chapters .step h4 {
  margin: 0;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--type-card);
  line-height: var(--leading-card);
  font-weight: 400;
}

.step-list--chapters .step p {
  margin: 0;
  color: var(--muted);
  font-size: var(--type-body);
  line-height: var(--leading-body);
}

/* .step-list--chapters .step--highlight h4 { font-weight: 700; } */


/* =============================================================================
   6. C5 - .row-list : ONE-LINE ROWS
   -----------------------------------------------------------------------------
   Derived from the homepage's .needs-route row. One component serves job
   listings, downloads, search results, contact routes and section indexes: they
   are all "a label, an optional detail, and one action" and giving each its own
   implementation is how six of them drift.
   ========================================================================== */

/* ROWS ARE CARDS WITH AIR BETWEEN THEM, NOT LINES.
   The hairline list was the flattest thing on the site: eight identical rules
   stacked down a page. Each row is now its own raised surface with a gap, so
   the separation comes from the tonal ladder the system already owns, and the
   row has somewhere to go on hover. */
.row-list {
  display: grid;
  gap: 10px;
}

.row-list--square {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.row-list--square .row-item {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto 1fr auto;
  align-items: start;
}

.row-list--square .row-item h3,
.row-list--square .row-item p {
  grid-column: 1 / -1;
}

.row-list--square .row-item-meta { grid-column: 1; }

.row-list--square .row-item p {
  align-self: start;
}

.row-list--square .row-item-meta {
  align-self: end;
}

.row-list--square .row-item-action {
  grid-column: 2;
  grid-row: 3;
  align-self: end;
}

/* Light Grey at rest, not White. Site owner, 27 Aug 2026, from a screenshot
   of /en/about/'s "Explore more": on the site's default White floor a
   --surface-raised row was white-on-white, the same E1 ground bug the news/
   stat cards had (see the 26 Aug CHANGELOG entry) - present here too because
   this component was never part of that pass. Same fix, same token. */
.row-item {
  position: relative;
  overflow: hidden;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px clamp(18px, 2vw, 32px);
  padding: clamp(18px, 1.8vw, 24px) clamp(20px, 2vw, 28px);
  border-radius: var(--r);
  background: var(--surface-sunken);
  transition: background var(--dur-fast) var(--ease-out);
}

/* THIS PAGE'S ONLY LIVE COMBINATION, AND IT USED TO BE TWO CONFLICTING
   COMPONENTS. `company-lines` (the .row-item-copy wrapper components.py
   needs) and `company-branded` (num suppression, the labelled action link)
   each shipped a COMPLETE, competing `.row-item` grid - two different
   grid-template-columns, two different .row-item-logo boxes, and neither
   rule set even targeted the real action element (`.row-item-action-wrap`;
   both wrote the singular `.row-item-action`, which is a different
   component's class). Whichever grid won by specificity left the logo box
   with no resolved width, which stopped its lazy-loading image from ever
   being considered near-viewport, which is why the row rendered as bare,
   unframed, wildly-different-sized source files with nothing sizing them -
   exactly the "different design system" look this was flagged for. Fixed as
   one rule set for the one real combination, not two half-finished ones.
   `company-lines` alone and `company-branded` alone are unused anywhere
   else in this codebase; nothing here removes that option, but nothing here
   is written to protect a combination that has no live example to check
   against.

   THE LOGO IS A FRAMED TILE, not a bare image at whatever size and colour
   its own source file happens to be. Khadamat, Zoad and Your Laundry's logo
   files disagree with each other on background colour and aspect ratio (one
   is white-ground, one is a dark square, one is light grey) - real
   differences in the source assets that no amount of `object-fit` alone
   reconciles. `.split-logo-badge` already solved exactly this for a single
   company mark on the split/overview section: a fixed `--surface-raised`
   card the mark sits inside, at its own aspect ratio, so the frame is
   uniform even where the artwork is not. Reused here at row scale rather
   than inventing a second "logo on a card" treatment.

   No `[dir="ltr"]`/`[dir="rtl"]` split needed for the column order: the grid
   inherits the page's own `direction` (rtl by default, ltr on the English
   mirror), so one `grid-template-columns` order - logo, copy, action -
   already reads logo-at-start, action-at-end in both languages. Only the
   image's own internal `object-position` needs a physical per-direction
   value, the same way every other decorative-geometry rule in this file
   does it. */
.row-list--company-lines.row-list--company-branded .row-item {
  grid-template-columns: 128px minmax(0, 1fr) auto;
  align-items: center;
}

.row-list--company-lines.row-list--company-branded .row-item > .row-item-logo {
  grid-column: 1;
}

.row-list--company-lines.row-list--company-branded .row-item > .row-item-copy {
  grid-column: 2;
  min-inline-size: 0;
}

.row-list--company-lines.row-list--company-branded .row-item > .row-item-action-wrap {
  grid-column: 3;
}

/* NARROWED FROM 42ch, format only, no copy change: at 42ch "Laundry services
   for the group's communities." (47 characters, but narrow letterforms) fit
   entirely inside the cap and rendered on one line, while its two siblings'
   longer sentences wrapped to two - three rows with the same reserved
   two-line height read as uneven. 30ch is the narrowest measure that still
   wraps the shortest of the three sentences onto a second line without
   producing an orphaned single word, keeping all three visually two lines.
   Site owner, 31 Aug 2026: formatting only, the sentences themselves are
   unchanged. */
.row-list--company-lines.row-list--company-branded .row-item-copy p {
  max-inline-size: 30ch;
}

/* Flex, not grid: a percentage max-block-size on a grid item resolves against
   the grid area's OWN track size, and an implicit row track sizes to its
   content by default even when the grid container itself has a definite
   block-size - so a portrait source (Zoad's) ignored the clamp entirely and
   overflowed the tile. A flex item's cross-size percentage resolves against
   the container's definite content-box height directly, which is the
   contract this tile actually needs: every logo, whatever its own aspect
   ratio, fits inside one fixed frame. */
.row-list--company-lines.row-list--company-branded .row-item-logo {
  display: flex;
  align-items: center;
  justify-content: center;
  inline-size: 128px;
  block-size: 72px;
}

.row-list--company-lines.row-list--company-branded .row-item-logo img {
  display: block;
  max-inline-size: 100%;
  max-block-size: 100%;
  inline-size: auto;
  block-size: auto;
  object-fit: contain;
}

.row-list--company-lines.row-list--company-branded .row-item-logo--swap {
  position: relative;
}

.row-list--company-lines.row-list--company-branded .row-item-logo--swap :is(.row-item-logo-default, .row-item-logo-hover) {
  transition: opacity var(--dur-slow) var(--ease-out);
}

/* ZOAD HAS NO SEPARATE WHITE WORDMARK FILE, so it never got the swap
   treatment above and stayed navy-on-Liwa on hover - illegible where every
   sibling row (Khadamat, Your Laundry) turns white. Site owner, 31 Aug
   2026. Same sanctioned filter the rest of the site already uses to force a
   dark mark to white on a dark ground (a.org-company:hover .org-company-logo,
   above) - no new asset, no blend mode, the mark's own shape stays exact. */
.row-list--company-lines.row-list--company-branded .row-item:is(:hover, :focus-within)
  .row-item-logo:not(.row-item-logo--swap) img {
  filter: brightness(0) invert(1);
}

.row-list--company-lines.row-list--company-branded .row-item-logo--swap .row-item-logo-hover {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  opacity: 0;
}

.row-list--company-lines.row-list--company-branded .row-item > .row-item-action-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 12px;
  align-self: center;
  padding-block-end: 4px;
  color: var(--accent-strong);
  font-size: var(--type-action);
  font-weight: 800;
  line-height: 1.3;
  transition: color var(--dur-fast) var(--ease-out);
}
.row-list--company-lines.row-list--company-branded .row-item-action-wrap .row-item-action-label {
  font-size: var(--type-meta);
}
/* NO LOCAL UNDERLINE RULE HERE. .row-item-action-wrap already carries the
   shared .action-link class in markup (components.py row_list()), so it
   should inherit that component's underline (2px, growing 42%->100% on
   hover/focus, direction-aware transform-origin) from sdeira-v2.css /
   sdeira-v2-en.css - the single definition AGENTS.md requires ("if the
   system already has it, use it, not something near it"). This file
   previously shipped a SECOND, competing ::after here (3px thick, its own
   physical-then-LTR-mirrored transform-origin pair) that duplicated and
   diverged from the shared one - the exact "shared class in two files is not
   a shared component" case that file also names. Site owner, 31 Aug 2026,
   flagged the visible mismatch (underline weight, fill behaviour) against
   the rest of the site on /business/services's row actions. Removed rather
   than re-tuned to match: a second definition would only drift again. */
.row-list--company-lines.row-list--company-branded .row-item {
  --company-row-hover-fill: var(--accent);
}
.row-list--company-lines.row-list--company-branded .row-item::before {
  --wipe-fill: var(--company-row-hover-fill);
}
.row-list--company-lines.row-list--company-branded .row-item:is(:hover, :focus-within) {
  color: var(--brand-white);
}
.row-list--company-lines.row-list--company-branded .row-item:is(:hover, :focus-within)
  :is(h3, p, .row-item-action-label, .row-item-action) {
  color: var(--brand-white);
}
.row-list--company-lines.row-list--company-branded .row-item:is(:hover, :focus-within) .row-item-logo--swap .row-item-logo-default {
  opacity: 0;
}
.row-list--company-lines.row-list--company-branded .row-item:is(:hover, :focus-within) .row-item-logo--swap .row-item-logo-hover {
  opacity: 1;
}
.row-list--company-lines.row-list--company-branded .row-item:is(:hover, :focus-within) .row-item-action-wrap {
  color: var(--brand-white);
}
/* SCALEX(1) STILL NEEDED HERE, deliberately: the shared .action-link:is(:hover,
   :focus-visible)::after only fires when the pointer is directly over the
   action-link span itself. This row's whole card is the hover target (the
   canonical equivalent is .company-card:hover .action-link::after in
   sdeira-v2.css), so the trigger has to read the ROW's hover state and apply
   to the link's own ::after - same pattern, not a duplicate of it. What was
   removed above was the ::after's geometry (block-size, transform-origin),
   which the shared component already owns. */
.row-list--company-lines.row-list--company-branded .row-item:is(:hover, :focus-within) .row-item-action-wrap::after {
  transform: scaleX(1);
}

/* THE FILL WIPE, the system's own hover vocabulary. A straight diagonal front
   travels across the row instead of the surface simply changing colour.
   z-index 0 with the content raised to 1 keeps it under the row's text without
   needing a stacking context on every child.

   IT IS A GRADIENT, NOT A CLIPPED PANEL, and the reason is a measured bug
   rather than a preference. The first version was a 136%-wide box clipped to
   polygon(22% 0, 100% 0, 78% 100%, 0 100%) and translated in from off the
   reading edge. Work the arithmetic through in ROW coordinates and the fully
   arrived state covers -18%..118% horizontally but the clip's top edge starts
   at 22% of the panel = 11.9% of the row, and its bottom edge ends at 78% of
   the panel = 88.1% of the row. So at rest-to-full the row keeps TWO unfilled
   triangles - about 12% of the width at the top-left corner and the same at the
   bottom-right - permanently, in the state a visitor is looking at. The fill
   never covered the whole shape, and no amount of retuning the overhang fixes
   it: a translated polygon reads as one straight front only when the travel is
   perpendicular to its leading edge, which depends on the row's aspect ratio,
   and a row's aspect ratio changes with its own copy.

   A gradient stop IS a straight line perpendicular to the gradient axis by
   construction, at any box size, and it resolves at exactly 100%. Same
   mechanism the homepage's card hover uses, for the same reason recorded there.

   The angle is the directional motion token, not a local number. It preserves
   the mark's slant while beginning from the reader's start edge. --wipe-p is registered with
   @property because an unregistered percentage inside a gradient stop is not
   animatable - the fill would snap, which is the reduced-motion behaviour
   anyway, so the degradation is the correct one rather than a broken state. */
@property --wipe-p {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

[dir="rtl"] i.ph.ph-paper-plane-tilt
{
    scale: -1 1;
}

/* --accent-deep (Liwa) fill, not --surface-sunken: the hover used to travel
   from Light Grey (the rest state above) to a barely-different Light Grey,
   which read as almost no hover at all now that the rest state itself is
   Light Grey. Site owner, 27 Aug 2026: wants the same Liwa wipe the homepage
   cards already hover with (.project-card-base's --wipe-fill: var(--accent-
   deep), --accent-deep resolving to Liwa on the main site - see sdeira-
   tokens.css). Same token, same travelling-gradient mechanism, not a new
   colour or a new animation. .pad--inverse's own dark-card wipe below keeps
   its own --card-dark-hover, unaffected - that variant was never Liwa. */
.row-item::before {
  content: "";
  position: absolute;
  z-index: 0;
  inset: 0;
  --wipe-p: 0%;
  --wipe-fill: var(--accent-deep);
  background-image: linear-gradient(var(--motion-wipe-gradient-angle-arrow),
    var(--wipe-fill) 0 var(--wipe-p),
    transparent var(--wipe-p) 100%);
  transition: --wipe-p var(--dur-slow) var(--ease-out);
  pointer-events: none;
}

.row-item:is(:hover, :focus-within)::before { --wipe-p: 100%; }

/* The copy travels white with the fill, the same tone-flip
   .project-card-base's own Liwa wipe already gives its children - Liwa's
   own measured contrast is for White text (9.35), not Ink, so the heading
   and meta must move with the fill instead of staying put on top of it.
   .pad--inverse .row-item h3/p below already sit at these same colours at
   rest, so this is a no-op there, not a second rule to reconcile. */
.row-item:is(:hover, :focus-within) :is(h3, p, .row-item-meta) {
  color: var(--brand-white);
}

.row-item .section-glass {
  transition: transform var(--dur-slow) var(--ease-out);
}

.row-item:is(:hover, :focus-within) .section-glass {
  transform: translate3d(-14px, -10px, 0);
}

[dir="ltr"] .row-item:is(:hover, :focus-within) .section-glass {
  transform: translate3d(14px, -10px, 0);
}

/* Everything the visitor reads sits above the wipe. */
.row-item > :not(.section-glass) { position: relative; z-index: 1; }

.row-item h3 {
  margin: 0;
  font-size: var(--type-label);
  line-height: var(--leading-label);
  color: var(--ink);
}

/* A row whose one destination is its entire content uses this variant. It
   stays a single semantic link, so pointer and keyboard users get the same
   complete target rather than a title-sized hit area. */
/* Also the base link contract for .icon-item--link (icon-grid's own single-
   link card, see the docstring above icon-grid's hover rules) - one rule
   for both since the two bodies are identical. */
:is(.row-item--link, .icon-item--link) {
  color: inherit;
  text-decoration: none;
}

:is(.row-item--link, .icon-item--link):focus-visible {
  outline: 3px solid var(--brand-razeen);
  outline-offset: 3px;
}

/* Other row families retain title-only links, stretched across the same card. */
.row-item h3 a {
  position: static;
  color: inherit;
  text-decoration: none;
}

.row-item h3 a::after {
  content: "";
  position: absolute;
  inset: 0;
}

.row-item p {
  grid-column: 1;
  margin: 0;
  color: var(--muted);
  font-size: var(--type-meta);
  line-height: var(--leading-body);
  /* RESERVE, DO NOT CLAMP - same contract .news-item's title/body already
     use. Default 0 leaves a bare route/download/contact row exactly as it
     always rendered; a row_list caller with real prose (a project or
     company summary) sets --row-sub-lines via `sub_lines` so every row in
     the group reserves the same height instead of each wrapping to
     whatever its own sentence length happens to produce. */
  min-block-size: calc(var(--row-sub-lines, 0) * 1lh);
}

.row-item-meta {
  grid-column: 1;
  display: flex;
  flex-wrap: wrap;
  gap: 10px 18px;
  color: var(--label);
  font-size: var(--type-meta);
}

.row-item-meta span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* Footer contact rows keep the icon at the reading-end edge in both
   directions: RTL gets this naturally; LTR mirrors the flex order. */
[dir="ltr"] .foot-contact,
.foot-contact {
  align-items: center;
}

[dir="ltr"] .foot-contact {
  flex-direction: row;
  justify-content: flex-start;
}

.row-item-meta i.ph {
  color: var(--icon);
  font-size: var(--ic-sm);
}

 .row-item:is(:hover, :focus-within) .row-item-meta span i.ph, .row-item:is(:hover, :focus-within) .row-item-meta i.ph {
    color: var(--icon-on-dark);
}

i.row-item-action {
  grid-column: 2;
  grid-row: 1 / -1;
  align-self: center;
  color: var(--ink);
  font-size: var(--ic-md);
  transition: transform var(--dur-base) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

/* var(--brand-white), not var(--link-hover): --link-hover resolves to Liwa
   on the main site, which the row's own hover fill now is too (above) - a
   Liwa arrow on a Liwa background is the same failure --accent on --accent
   always is. White is what the fill's own text already turns to. */
.row-item:is(:hover, :focus-within) i.row-item-action {
  color: var(--brand-white);
  transform: translateX(4px);
}

.pad--inverse .row-item { background: var(--card-dark); }
.pad--inverse .row-item::before { --wipe-fill: var(--card-dark-hover); }
.pad--inverse .row-item h3 { color: var(--brand-white); }
.pad--inverse i.row-item-action { color: var(--brand-white); }
.pad--inverse .row-item p,
.pad--inverse .row-item-meta { color: var(--muted-on-dark); }

/* Downloads: the action is a file glyph plus its type and size, which a
   visitor needs before deciding to click. */
.row-list--downloads i.row-item-action {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: var(--type-meta);
}

.row-list--downloads .row-item-action i.ph { font-size: var(--ic-md); }

/* Numbered variant: an unordered set of independent rows (each its own
   destination, no reading-order relationship between them - e.g. distinct
   awards) where the ordinal itself is the primary visual element instead of
   a meta line. Reuses the --figure/--figure-size numeral role directly, the
   same typographic role .step-list's marker and .stat already use, rather
   than inventing a badge or a list-style counter(). Unlike .step-list this
   stays a .row-item: no connecting rule, no sequence semantics - see the
   step-list note in the design system for why the two are not the same
   component. */
.row-item--numbered {
  grid-template-columns: auto minmax(0, 1fr) auto;
  padding-block: clamp(24px, 2.4vw, 32px);
}

.row-item-num {
  grid-column: 1;
  grid-row: 1 / -1;
  align-self: center;
  color: var(--figure);
  font-size: var(--figure-size);
  font-weight: 700;
  line-height: 1;
}

.row-item--numbered h3,
.row-item--numbered .row-item-meta { grid-column: 2; }

.row-item--numbered i.row-item-action { grid-column: 3; }

.pad--inverse .row-item-num { color: var(--figure-on-dark); }


/* =============================================================================
   7. C7 - .logo-wall : LOGO TILES
   -----------------------------------------------------------------------------
   A logo is the one thing on the site that is NOT a crop of the 16:9 master. It
   is placed with object-fit: contain inside its tile and never cropped -
   cropping a wordmark is damage, not a design choice.
   ========================================================================== */

.logo-wall {
  display: grid;
  grid-template-columns: repeat(var(--grid-cols, 4), minmax(0, 1fr));
  gap: clamp(14px, 1.6vw, 22px);
}

/* --surface-sunken, not --surface-raised: white-on-white on the site's
   default White floor (found on /en/companies/eskan/ during the sweep for
   other E1 instances - see CHANGELOG, 26-27 Aug 2026). Not the same case as
   .row-item-logo's deliberate White frame: that logo already sits inside a
   .row-item that carries its own Light Grey card, so its own small White
   sub-tile still reads as a frame against it. .logo-tile stands alone
   directly on the section floor, with nothing else to distinguish it. */
.logo-tile {
  position: relative;
  overflow: hidden;
  display: grid;
  /* An explicit 1fr track, not the bare implicit auto grid this used to be -
     a single grid item never stretches into an auto-sized track even with
     place-self:stretch on the item (the track sizes to the item's own
     content, so "stretch to the track" is a no-op). 1fr gives the child mark
     a real, definite box to fill. See the note on .logo-tile img. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  place-items: center;
  padding: clamp(20px, 2.2vw, 32px);
  aspect-ratio: var(--media-card);
  background: var(--surface-sunken);
  border-radius: var(--r);
}

.pad--sunken .logo-tile { background: var(--surface-raised); }

/* NO CORNER MARK ON A LOGO TILE, 31 Aug 2026: the shared p.43 diagonal-lines
   mark is drawn on photography and content cards across the site, but a
   .logo-tile holds someone else's brand mark, not Sdeira's own imagery -
   drawing the group's identity pattern behind a third party's logo reads as
   the group claiming or decorating that mark, not presenting it. Flagged in
   a preview comment on /companies/eskan/'s product-brands wall. .logo-tile
   is C7's only use (object-fit: contain logos, never a crop), so removing
   the ::after here does not touch any other component's corner mark. */

.logo-tile img,
.logo-tile figcaption {
  position: relative;
  z-index: 1;
}

/* FORCED SLOT, NOT A CAP, 1 Sep 2026: max-*-size alone left every mark at its
   own intrinsic pixel size, so a tall, tightly-cropped mark (68 Living,
   130x144) rendered far larger than a wide one with the same crop (Square,
   106x31) - flagged in a preview comment on this exact wall. The tile's own
   place-items:center does not stretch a grid item, so a percentage height on
   the img alone resolved against its own auto-sized grid area (i.e. against
   itself) and had no effect - align-self/justify-self:stretch is what gives
   the img the tile's own definite content box to size against. inline/block
   at 100% plus object-fit:contain then behaves exactly like
   .org-company-logo: every mark reads at one consistent apparent size
   regardless of its source file's own canvas, nothing is cropped. */
.logo-tile img {
  inline-size: 100%;
  block-size: 100%;
  align-self: stretch;
  justify-self: stretch;
  object-fit: contain;
}

/* A tile that names what it shows - certificates need the issuing body under
   the mark. */
.logo-tile figcaption {
  margin-block-start: 12px;
  color: var(--label);
  font-size: var(--type-meta);
  text-align: center;
}

.logo-tile--captioned {
  grid-template-rows: minmax(0, 1fr) auto;
  aspect-ratio: auto;
  place-items: center;
}

.pad--inverse .logo-tile { background: var(--card-dark); }

.pad--inverse .logo-tile figcaption { color: var(--label-on-dark); }


/* =============================================================================
   8. C8 - .people-grid : PORTRAITS
   -----------------------------------------------------------------------------
   1:1, the token file's --media-square. This is the site's portrait slot and
   the only one: there is no 4:5 delivery ratio, so a leadership photograph is
   composed for a square crop of the same 16:9 master everything else uses.
   ========================================================================== */

.people-grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-cols, 3), minmax(0, 1fr));
  gap: clamp(22px, 2.4vw, 34px);
}

.person {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.person-photo {
  overflow: hidden;
  border-radius: var(--r);
  aspect-ratio: var(--media-square);
  background: var(--surface-sunken);
}

.person-photo img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

.person h3 {
  margin: 0;
  font-size: var(--type-label);
  line-height: var(--leading-label);
  color: var(--ink);
}

.person-role {
  color: var(--label);
  font-size: var(--type-meta);
  /* Reserved: a two-line role must not shove the card below it out of step. */
  min-block-size: calc(2 * 1lh);
}

.person p {
  margin: 0;
  color: var(--muted);
}

/* No photograph yet. The token file's --pending is a neutral grey on purpose:
   a missing value is not an error. */
.person-photo--pending {
  display: grid;
  place-items: center;
  color: var(--pending);
  font-size: var(--ic-xl);
}

/* PEOPLE-GRID VARIANT: figure-rows. Not every leadership page reads as a row
   of peers - a Chairman and a CEO are a hierarchy, not a set, and a grid of
   equal-size cards says "set". This variant states that order visually
   instead: one person per row, top to bottom in the record's own order
   (rank, never alphabetical), each the site's one figure - .split-media /
   .split-ribbon, documented above under THE FIGURE - standing beside a
   reading card instead of a small overhanging .split-badge. Reuses .split's
   own two-column grid (see C1 above) with one override: a fixed-width photo
   column and a flexible card column that runs to the row's far edge, gap
   closed to zero so the two touch.

   THE PORTRAIT RATIO IS --media-square, not .split-media's own default
   --media-wide: a person is .person-photo's slot, square, the same one the
   card grid above already uses. Never widen it back to --media-wide for a
   person - that is the figure's landscape ratio, for a different subject.
   THE PHOTO NEVER RESIZES to fill the row - it keeps its fixed column and
   the card takes whatever width is left, which is the point: the card, not
   the photo, is what grows to close the row.

   THE CARD MATCHES THE PHOTO'S HEIGHT - `align-items: stretch` here, not
   `center`. Site owner, 28 Aug 2026: centring let each row's card size to
   its own copy length, so two rows of differing bio length rendered two
   differently sized cards and read as inconsistent, not as one
   component used twice. Stretching ties every card to the one fixed
   dimension every row already shares - the photo's own --media-square
   height - so card size is uniform across every row regardless of how much
   copy a given person has, without capping or truncating that copy. The
   content itself stays vertically centred inside the taller box
   (.split-copy's own `justify-content: center`, below), so a short bio
   does not look stranded at the top of an oversized card.
   REVISED same day: matching the photo's own height (420px) left a card
   mostly empty air around three lines of text. Reverted to `align-items:
   center` and moved the "uniform" requirement onto .split-copy itself
   instead - a fixed `min-block-size`, sized to the taller of the two
   published cards' natural content rather than to the unrelated photo
   column, plus a fixed `inline-size` (not just a `max-inline-size` cap) so
   width is uniform too, not just capped. */
.person-rows {
  display: flex;
  flex-direction: column;
  gap: clamp(56px, 6vw, 96px);
}

.person-rows .split {
  grid-template-columns: min(420px, 42%) minmax(0, 1fr);
  align-items: center;
  gap: 0;
}

.person-rows .split-media {
  aspect-ratio: var(--media-square);
}

/* THE PORTRAIT CARRIES NO WATERMARK. A leadership photograph is a formal,
   official portrait - the identity band's diagonal is right for a landscape
   or a project photograph, wrong for a person's face, real or still pending.
   Suppressed here rather than dimmed: the same call .split--logo already
   makes to keep the mark off a partner's logo. The mark moves to the card
   below instead, at .icon-item-band's own ambient tone - see that rule for
   why a card this size takes the quieter tone rather than the flat-surface
   one. */
.person-rows .split-ribbon {
  display: none;
}

/* THE PENDING PORTRAIT: a person icon on Razeen, not the brand emblem. Site
   owner, 28 Aug 2026: no logo needed here, and Razeen is exactly this slot's
   documented role - a 15% secondary, fill-only, no text-on-it, "the amber
   fill" already covering other icon tiles below (see --icon-warm-fill /
   --icon-value-fill's own comments). --ink (near-black) on Razeen (#DDB176)
   measures comfortably above the site's icon-legibility floor, unlike white
   on Razeen (1.98, why the token file forbids Razeen as running text).
   Square, matching the row's own --media-square figure ratio, not the
   pending marker below (which sizes off a 3-across grid tile instead). */
.split-media-pending {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border-radius: var(--r);
  background: var(--brand-razeen);
  color: var(--ink);
  font-size: clamp(3.5rem, 9vw, 5.5rem);
}

.split-media-pending i.ph {
  inline-size: 1em;
  block-size: 1em;
}

/* THE CARD TOUCHES THE PHOTO, IT DOES NOT FILL THE ROW. `justify-self:
   start` keeps it pinned to the photo's edge inside its own flexible grid
   track rather than stretching to the track's full width (grid's default).
   `inline-size` (not just a `max-inline-size` cap) so every card is the
   SAME width regardless of how little text a given person has - a max-width
   alone lets a short card shrink-to-fit and read narrower than its sibling,
   which is what the site owner flagged, 28 Aug 2026: the two published
   cards had visibly different widths as well as different heights. 680px is
   the same reading-measure cap .sec-head already uses for a section lead.
   Both are logical (start = the row's reading-start edge, whichever
   direction that is), so RTL needs no mirror.

   `min-block-size` makes height uniform the same way: fixed to fit the
   taller of the two published cards' own content (name + two-line role
   reservation + a two-line body) at this width, so neither card's
   height depends on how much copy it carries. A third person with a
   longer body still grows the card past this floor rather than being
   clipped - `min`, not a hard `block-size`.

   --surface-sunken, NOT the White this used to read here. The card was
   White on the reasoning that "white-on-the-page-ground is already a tonal
   step (--surface-raised #FFFFFF against --surface-base #FAF4EC)" - true
   when written, false now: --surface-base is var(--brand-white) today (a
   later redesign), so that tonal step is gone and this was white-on-white,
   confirmed on /en/about/leadership/ by screenshot during the sweep for
   other E1 instances (see CHANGELOG, 26-27 Aug 2026). */
.person-rows .split-copy {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  justify-self: start;
  gap: 12px;
  inline-size: min(100%, 680px);
  min-block-size: 260px;
  padding: clamp(28px, 3vw, 48px);
  background: var(--surface-sunken);
  border-radius: var(--r);
}

.pad--sunken .person-rows .split-copy { background: var(--surface-raised); }

/* THE READING CARD'S CORNER MARK: the p.43 lines.svg mark instead of
   .icon-item-band. Site owner, 28 Aug 2026: the skewed band read as a stray
   diagonal line here too, the same call already made for the /about/
   our-story rebrand cards (icon-grid--corner-mark, above) and khadamat's own
   icon-grid cards - one mark family sitewide rather than two. This card is
   not an .icon-item, so it gets its own ::after rather than reusing that
   class; the artwork, ratio and physical corner are identical throughout. */
.person-rows .split-copy::after {
  content: "";
  position: absolute;
  z-index: 0;
  right: 0;
  bottom: 0;
  inline-size: 34%;
  aspect-ratio: var(--photo-mark-p43-ratio);
  background: url("../images/patterns/lines.svg?v=d874099296964b63") no-repeat right bottom / 100% 100%;
  pointer-events: none;
}

[dir="rtl"] .person-rows .split-copy::after { right: auto; left: 0; transform: scaleX(-1); }

/* Content sits above the mark. */
.person-rows .split-copy > * {
  position: relative;
  z-index: 1;
}

/* THE NAME TAKES --type-headline: --type-card/--type-label tops out at
   1.375rem, sized for a repeated grid tile, which read too close to
   .person-role beneath it; --type-feature (2-3rem) was tried next and read
   at essentially --type-section's own size, a page H2. --type-headline is
   the token added to sit between the two for exactly this case - a caption
   headline beside its own dedicated figure. See its own comment in
   sdeira-tokens.css. */
.person-rows .split-copy h3 {
  margin: 0;
  font-size: var(--type-headline);
  line-height: var(--leading-headline);
  font-weight: 700;
  color: var(--ink);
}

.person-rows .split-copy .person-role {
  margin: 0;
  min-block-size: 0;
}

.person-rows .split-copy p:not(.person-role) {
  margin: 0;
  color: var(--muted);
}


/* =============================================================================
   8z. THE BRAND GLASS BLOCK - one copy panel over a clean photograph
   -----------------------------------------------------------------------------
   GLASS, NO OVERLAY. Site owner, 26 Aug 2026. The translucent layer comes OFF
   the photograph; the copy that used to sit on it moves into a block of brand
   Black glass instead, so the photograph reads clean and whole and the text is
   carried by the panel rather than by a tint spread over the picture.

   THIS IS NOT A SECOND IMPLEMENTATION. It is the interior-page half of the
   treatment the two homepages already ship, promoted here rather than
   re-derived: assets/css/sdeira-liwa.css defines exactly these declarations
   for the homepage hero, its closing Contact band and its first full-image
   project card, under its own "BRAND GLASS BLOCK" heading. Every value below
   is that file's, which is in turn the token file's - nothing here is new:

     fill    --scrim-glass     rgba(65,64,66,.74), sdeira-tokens.css section 11
     blur    --glass-blur      18px, the same blur the scrolled top bar takes
     radius  --r               the site's one radius
     text    --brand-white     section 11: body text on a dark layer over
                               photography is white ONLY

   .74 IS NOT A LOOSE NUMBER. Section 11's own table makes it the last alpha at
   which white body text still clears 4.5:1 against the worst case a photograph
   can present (a fully white pixel composites to rgb(114,114,115), 4.81:1).
   The blur only adds headroom by pulling that worst pixel toward its
   neighbours. Do not lower it, and do not raise it back toward an opaque slab:
   the owner saw the opaque version and rejected it as not glass.

   THE PANEL HUGS ITS COPY. The photograph is the wide background, so the block
   is capped and padded rather than stretched - the same correction the
   homepage needed when its copy column filled the hero's full height and left
   the picture visible only beside it. A component joins this rule by adding
   its own selector here, not by copying the five declarations.

   THE PLAIN SUB-PAGE HERO LEFT THIS GROUP, 27 Aug 2026. Site owner, from a
   real screenshot of /en/about/: the boxed panel behind the title now reads
   as its own dark shape sitting on the photo rather than a caption, and the
   panel wanted to be shorter than this group's shared padding allows. It is
   back to a flat scrim across the whole photograph - the very thing this
   section's own history records as rejected on 26 Aug - but that reversal is
   scoped to this one hero, confirmed explicitly: the homepage hero, the
   closing CTA band and .photo-band keep the glass panel above unchanged. See
   .subpage-hero--photo below. */

.photo-band > .wrap,
.cta-band > .wrap .cta-inner {
  background: var(--scrim-glass);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(112%);
  backdrop-filter: blur(var(--glass-blur)) saturate(112%);
  color: var(--brand-white);
  border-radius: var(--r);
  padding: clamp(36px, 5vw, 64px) clamp(28px, 4vw, 56px);
  max-inline-size: min(var(--max), 94%);
}

/* THE PLAIN SUB-PAGE HERO'S OWN TREATMENT. A flat scrim covers the whole
   photograph, so the panel no longer competes with the picture as a
   separate shape. .wrap carries no background of its own; the section's own
   `color: var(--brand-white)` (sdeira-subpage.css) already reaches the
   heading and breadcrumb, scrim or not. Painted on .subpage-hero-media's own
   ::after (not the section's) so it stays inside that element's stacking
   context, directly over the image it darkens and nothing else.

   --scrim-glass, NOT --scrim-hero, 27 Aug 2026: the site owner asked for the
   photo to read more clearly through the layer. Section 11 sets .86 as the
   floor for a flat, unblurred dark layer over photography, which is what
   --scrim-hero (.88) is calibrated for - but this hero is not unblurred any
   more: the photograph itself now carries filter: blur(var(--glass-blur))
   (below), the same 18px the token file's own worked example uses to justify
   --scrim-glass's lower .74 floor for a blurred surface. Same blur, same
   token, not a new number - .74 already has its contrast math done in
   sdeira-tokens.css section 11 (white text 4.81:1 against a fully white
   photo pixel, above the 4.5 floor). */
.subpage-hero-media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--scrim-glass);
  pointer-events: none;
}

/* THE P.43 CORNER MARKS, back on this hero 27 Aug 2026 in place of the
   identity bands retired the same day. Same pair, same files, same ratio the
   closing CTA band's .cta-glass already carries (--photo-mark-p43-ratio,
   lines.svg / lines-rotated-180.svg) - not a second mark. Only the sizing
   axis differs: .cta-glass sizes off --photo-mark-width-hero because its own
   box is a narrow, tall copy panel; this hero is short and full-bleed, so
   the mark is sized off its own block-size instead and the token still
   supplies the one ratio both boxes draw from. z-index 0 sits above the
   scrim (part of the media element's own -2 context) and below .wrap (1).

   :not(.subpage-hero--error) - the 4 error pages carry .subpage-hero--photo
   too (their own photo ground), and sdeira-error.css's own header comment is
   explicit: "No stripe or watermark decoration here, by request." Selecting
   .subpage-hero--photo bare would have put a watermark back on exactly the
   pages that rule names - caught before it shipped, not after. */
.subpage-hero--photo:not(.subpage-hero--error)::before,
.subpage-hero--photo:not(.subpage-hero--error)::after {
  content: "";
  position: absolute;
  z-index: 0;
  block-size: 62%;
  aspect-ratio: var(--photo-mark-p43-ratio);
  pointer-events: none;
}

.subpage-hero--photo:not(.subpage-hero--error)::before {
  inset: 0 auto auto 0;
  background: url("../images/patterns/lines-rotated-180.svg?v=bc182f344a4d87fd") no-repeat left top / 100% 100%;
}

.subpage-hero--photo:not(.subpage-hero--error)::after {
  inset: auto 0 0 auto;
  background: url("../images/patterns/lines.svg?v=d874099296964b63") no-repeat right bottom / 100% 100%;
}


/* =============================================================================
   9. C12 - .cta-band : THE CLOSING BAND
   -----------------------------------------------------------------------------
   THE SAME BAND THE HOMEPAGE CLOSES ON, so the site ends the same way on every
   page. Authored from the measured computed values of the homepage's own
   section#contact - not lifted from sdeira-v2.css, which defines the same
   selectors many times over and where source order decides the winner.

   The photograph is clean under a centred shared brand-glass copy panel:
   --scrim-glass, --glass-blur and --r. The section owns its photograph and
   identity bands; .cta-inner owns the readable panel and the action row.

   THE PHOTOGRAPH IS DATA, NOT A STYLESHEET. Each page passes its own image on
   --cta-image from its record, exactly as .company-card already passes its
   accent. Thirty-odd pages closing on the same band with a different
   photograph is one component with one definition; thirty page sheets each
   setting a background-image would be thirty definitions.

   ========================================================================== */

.cta-band {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  padding-block: var(--sec-pad);
  padding-inline: 0;
  background: var(--surface-inverse-soft);
  color: var(--brand-white);
}

/* A page with no --cta-image simply shows the Jais ground, which is a complete
   design on its own rather than a broken one. */
.cta-band::before {
  content: "";
  position: absolute;
  z-index: -2;
  inset: 0;
  background-image: var(--cta-image, none);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* NO SCRIM, site owner 26 Aug 2026. This band used to darken its photograph
   with a flat pure-black ::after at .76; the copy now sits in the shared brand
   glass block (section 8z) instead, so the picture reads clean and whole. The
   ::after no longer exists. */

/* The closing CTA carries the same three-line identity mark as the homepage.
   Keep the decoration in its own clipping frame so the skewed bands never
   alter the section's layout or leak past its photographic surface. */
.cta-glass {
  position: absolute;
  z-index: 0;
  inset: 0;
  overflow: hidden;
  opacity: .28;
  pointer-events: none;
}

/* PATTERN CLEAN-UP (Liwa migration C4, docs/content/liwa-migration.md; owner,
   21 & 26 Aug 2026). The site's own stripe language leaves completely -
   identity bands behind sections, ribbons on card photos; the p.43 line set
   (below) is the only mark that remains. PROMOTED here (26 Aug 2026 sub-page
   rollout) from assets/css/sdeira-liwa.css, which is homepage-only, so
   sub-pages kept the retired stripes. .cta-glass itself stays visible - see
   the mark drawn on its own ::before/::after just below - only its old
   skewed-band children (span, right after this block) are hidden. */
.section-glass,
.news-glass,
.ribbon-frame,
.split-ribbon { display: none !important; }

.cta-glass {
  opacity: 1;
}

/* One p.43 mark top-left to match the About photo's own composition, and a
   second - mirrored, not rotated a second time - at the opposite corner, so
   a section this wide reads as marked on both sides rather than one.
   UNIFIED 28 Aug 2026: --photo-mark-width-hero, the full-bleed section tier
   - kept in step with .hero-glass (sdeira-liwa.css) on the same token, one
   step larger than the content-scale --photo-mark-width every card/figure
   mark now uses, matching how much bigger this closing band already reads
   next to a card. The About photo moved to the content tier instead - see
   its own rule in sdeira-liwa.css. */
.cta-glass::before,
.cta-glass::after {
  content: "";
  position: absolute;
  inline-size: var(--photo-mark-width-hero);
  aspect-ratio: var(--photo-mark-p43-ratio);
  pointer-events: none;
}
.cta-glass::before {
  inset: 0 auto auto 0;
  background: url("../images/patterns/lines-rotated-180.svg?v=bc182f344a4d87fd") no-repeat left top / 100% 100%;
}
.cta-glass::after {
  inset: auto 0 0 auto;
  background: url("../images/patterns/lines.svg?v=d874099296964b63") no-repeat right bottom / 100% 100%;
}

.cta-glass span {
  display: none;
  position: absolute;
  top: var(--identity-band-top);
  block-size: var(--identity-band-height);
  inline-size: var(--identity-band-width);
  border: var(--identity-band-border) solid var(--identity-mark);
  border-radius: var(--r);
  background: color-mix(in srgb, var(--identity-mark) var(--identity-band-fill), transparent);
  transform: skewX(-24deg);
}

.cta-glass .r1 { inset-inline-start: 68%; }
.cta-glass .r2 { inset-inline-start: 78%; }
.cta-glass .r3 {
  inset-inline-start: 88%;
  border-color: color-mix(in srgb, var(--identity-mark) 55%, transparent);
}

@media (max-width: 920px) {
  .cta-glass { display: none; }
}

.cta-band > .wrap {
  position: relative;
  z-index: 1;
}

.cta-inner {
  text-align: center;
  /* The glass fill, blur, radius, padding and measure come from the shared
     brand glass block in section 8z; this centres the block within .wrap, the
     same way the homepage's own closing-band .cta-inner does (sdeira-liwa.css).
     text-align stays centre because the copy inside is the centred closing-CTA
     column, not the photo-band's start-aligned one. */
  margin-inline: auto;
}

.cta-inner .kicker {
  justify-content: center;
  margin-block-end: 30px;
  color: var(--brand-white);
}

.cta-inner h2 {
  margin: 0;
  color: var(--brand-white);
}

/* White at 82%, not --muted-on-dark. Section 11 of the token file is explicit
   that body-size text over photography is white ONLY; a translucent white is
   still white, a grey is not. */
/* THE SPACE ABOVE THE BUTTONS BELONGS TO THE BUTTONS, not to the paragraph
   above them. It used to be this rule's own margin-bottom, which is correct
   only while every band has a lead: the review of 21 August removed the lead
   from /contact's closing band and the two buttons went straight under the
   heading with nothing between them. Moved to .cta-actions below, at the same
   54px, so a band with a lead measures exactly as it did (18 + 54, verified on
   the homepage's own closing band, which shares this sheet) and a band without
   one now measures the same 54px from its heading. */
.cta-inner > p {
  max-inline-size: 56ch;
  margin: 18px auto 0;
  font-size: var(--type-lead);
  line-height: var(--leading-lead);
  color: color-mix(in srgb, var(--brand-white) 82%, transparent);
}

/* A ROW OF BUTTONS, not a grid of panels.
   This was `grid-template-columns: repeat(var(--cta-cols, 3), minmax(0, 1fr))`,
   which stretches each action to a full column whatever its label says. With
   three of them that reads as a card row; with two it gave 595px-wide orange
   slabs across the band, which is what was reported - "these are buttons at
   the end, not cards".

   A flex row sizes each one to its own label and lets them wrap, so the count
   in the record no longer decides how wide a button is. Centred, because the
   band's heading and lead above them are. */
.cta-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 16px;
}

/* See .cta-inner > p above: this is that rule's old margin-bottom, moved here
   so the gap holds whether or not the band carries a lead. Scoped to the
   closing band's own inner block on purpose - `.cta-actions` is also used
   inside `.split-copy`, where it sits in a named grid area and a top margin
   would push it out of that area's own rhythm. */
.cta-inner > .cta-actions {
  margin-block-start: 54px;
}

/* THE ACTION CARD. Not a button in a row - a card with a mark, a label and an
   arrow, which is what the homepage closes on. */
/* Button geometry, taken from the homepage's own .btn-primary rather than
   invented here: 48px tall, 22px of inline padding, one radius token, weight
   800. Measured on /en/ at 1400 before copying - 48px high, 0 22px, 4px
   radius, and a width that follows the label (172px for "Contact Us", 116px
   for "Explore").

   What changed from the panel this was: min-block-size 84 -> 48, the 14px 20px
   box padding -> 0 22px, weight 700 -> 800, and text-align start -> center,
   since a button sized to its own label has nothing to align against. The
   homepage is NOT affected by any of it: en/index.html does not load this
   sheet and carries its own .cta-action in sdeira-v2-en.css. */
.cta-action {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  min-block-size: 48px;
  padding: 0 22px;
  border-radius: var(--r);
  background: var(--accent-fill);
  color: var(--brand-white);
  font-size: var(--type-action);
  font-weight: 800;
  line-height: var(--leading-label);
  text-align: center;
}

/* THE DIAGONAL FILL WIPE - the fill/clip-path/timing are taken to the number
   from the homepage's own system-layer .cta-action (see sdeira-v2-en.css,
   "this is the same component the generated pages already draw"). This file
   used to carry its own geometry (a --wipe-slant/inset pair, a
   --surface-brand-deep fill) plus a second, later-added "canonical" block
   further down that only partially overrode it - same selector, different
   park distance, different fill. Two blocks fighting over one property is
   why the primary and ghost buttons stopped travelling identically and why
   this page read differently from the homepage that defines the reference.
   There is now exactly one block.

   THE WIPE MIRRORS BY DIRECTION THE SAME WAY .nav-cta ALREADY DOES (see
   sdeira-nav-controls.css, the header CTA's own canonical wipe, loaded last
   on every page precisely so one definition wins everywhere). That one
   flips both the parking transform AND the clip-path per [dir] - not the
   transform alone - which is what makes its diagonal read as a clean
   corner-to-corner fill in both languages instead of a slant that travels
   the "wrong" way relative to its own point. Matched here for the same
   reason: right-to-left on the Arabic/RTL default, left-to-right on an
   English page. */
.cta-action::before {
  content: "";
  display: block;
  position: absolute;
  z-index: -1;
  inset: -1px -40%;
  background: var(--accent-fill);
  clip-path: polygon(22% 0, 100% 0, 78% 100%, 0 100%);
  transform: translate3d(78%, 0, 0);
  transition: transform var(--dur-slow) var(--ease-out) 55ms;
  pointer-events: none;
}

[dir="ltr"] .cta-action::before {
  clip-path: polygon(78% 0, 0 0, 22% 100%, 100% 100%);
  transform: translate3d(-78%, 0, 0);
}

.cta-action:is(:hover, :focus-visible)::before { transform: none; }

/* THE SECOND ACTION IS NOT A SECOND PRIMARY. Every closing band rendered both
   of its actions in the Sweihan fill, because cta_band() never read the
   `level` the records have carried all along - so a page ended on two equally
   loud buttons and neither was the one to press.

   The secondary here is .btn-ghost's treatment, and that is the system's own
   answer rather than a new one: a transparent control with a white outline is
   the site's ONE sanctioned border, sanctioned precisely because this band
   sits over photography and an outline is what gives a transparent control a
   hit area there. See the docstring on components.buttons(), which states the
   same rule and reaches for .more instead on a light ground.

   Both levels wipe to the same fill, so the hover vocabulary stays one motion
   and one tone step: the ghost promotes to solid, and hovering the secondary
   never makes it look like the resting primary. */
.cta-action--ghost {
  border: 1px solid var(--brand-white);
  background: transparent;
}

.cta-action:is(:hover, :focus-visible) {
  border-color: transparent;
  background-color: var(--accent-fill);
}

/* --accent-fill READS AS THE DARK GROUND ITSELF ON THIS BAND. The docstring
   above states this component always sits over photography (the closing
   band); --accent-fill is each brand's DEEP tone, tuned for AA contrast
   under a solid fill on a LIGHT surface, not for a 1px outline over a photo
   already carrying a dark scrim - Khadamat's, for example, sits at #29523D
   against a scrim that reads near-black, so the ring all but disappears
   (site owner, 31 Aug 2026, flagged live). --accent-on-dark is the token
   the system already calibrates per brand for exactly this ground - Soft
   Sage for Khadamat, the same role variable .subpage-hero--carousel's own
   .em emphasis word reads two files up in khadamat-theme.css - so this
   swaps to it rather than inventing a new value. */
.cta-action--ghost:is(:hover, :focus-visible) {
  border-color: var(--accent-fill);
  background-color: transparent;
}

/* QUALIFIED WITH `i` ON PURPOSE. sdeira-subpage.css sets `i.ph { font-size:
   var(--ic-md) }` at (0,1,1); a bare .cta-action-mark is (0,1,0) and loses to
   it, which is a silent failure - the glyph just renders at the wrong size
   with nothing in the console. Every icon rule in this file is qualified for
   the same reason. */
i.cta-action-mark {
  flex: 0 0 auto;
  font-size: var(--ic-lg);
  color: var(--brand-white);
}

/* No `flex: 1`. On a panel that filled the row; on a button sized to its own
   label it would push the arrow to a far edge and re-create the slab. */
.cta-action span { flex: 0 1 auto; }

i.cta-arrow {
  flex: 0 0 auto;
  font-size: var(--ic-sm);
  transition: transform var(--dur-base) var(--ease-out);
}

.cta-action:is(:hover, :focus-visible) .cta-arrow { transform: translateX(4px); }

/* THE COMPANY WEBSITE LINK. components.company_link_button() reuses the plain
   .btn.btn-primary geometry from sdeira-subpage.css - height, padding, radius,
   the wipe - and adds only a leading identity glyph. `.btn-company-mark` is
   the Sdeira mark used as a mask, the same technique sdeira-assistant.css
   uses for the launcher and message avatar, so this is a reused icon rather
   than a new asset. Filled white to match the label, sized off `--ic-md` to
   read as a matched pair with the ph-arrow-square-out this button always
   closes on - the two glyphs bracket the label rather than either dominating
   it. `.btn` already sets `gap: 12px`, so the mark, label and trailing icon
   space themselves with no rule of this component's own. Site owner
   confirmed the scope 15 Aug 2026: this button only, not every .btn-primary. */
.btn-company-mark {
  display: inline-block;
  flex: 0 0 auto;
  inline-size: var(--ic-md);
  block-size: var(--ic-md);
  background: var(--brand-white);
  -webkit-mask: url("../images/brand/sdeira-mark.png?v=c1949ae876afaeca") center / contain no-repeat;
  mask: url("../images/brand/sdeira-mark.png?v=c1949ae876afaeca") center / contain no-repeat;
}

/* Surface variant: the same band on the light ground, for a page that already
   ends on a dark section and would otherwise show two dark bands in a row. The
   photograph and the scrim both come off; the cards keep their fill. */
.cta-band--surface { background: var(--surface-sunken); }
.cta-band--surface::before,
.cta-band--surface::after { content: none; }
/* "The photograph and the scrim both come off" is what the comment above has
   always said, and until 4 Sep 2026 only the photograph did. `.cta-band >
   .wrap .cta-inner` sets --scrim-glass unconditionally, so this variant put
   its own --ink heading on a dark panel at 1.75:1 - unreadable, and invisible
   as a bug because no record had ever asked for the variant. The variant was
   finished rather than replaced: its intent was already written down, so this
   removes the panel it says comes off instead of inventing a treatment.
   Specificity matches that rule's 0,3,0 deliberately; a bare
   `.cta-band--surface .cta-inner` is 0,2,0 and loses whatever the order. */
.cta-band--surface > .wrap .cta-inner {
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.cta-band--surface .cta-inner :is(h2, .kicker) { color: var(--ink); }
.cta-band--surface .cta-inner > p { color: var(--muted); }


/* =============================================================================
   9f. C13 - .photo-band : MID-PAGE PHOTOGRAPH, GLASS COPY BLOCK, NO ACTIONS
   -----------------------------------------------------------------------------
   .cta-band is documented as the band a page ENDS on, exactly once - see its
   own docstring and the C12 header above. A page that needs the same material
   (full-bleed photograph, white type) mid-page, with no action row, is not
   that band and must not borrow it a second time in one page. This is that
   component: same proven photograph mechanics, own name, no positional claim,
   and no `.cta-actions` row - a caller that wants a button anchors it in
   `body` copy instead, the same way any other section's lead prose behaves.
   Reuses `.kicker`/`h2` roles for its heading, unchanged from every other
   section's `sec_head`.

   NO SCRIM. Site owner, 26 Aug 2026. This band used to darken its own
   photograph with a flat pure-black `::after` at .76, and the copy sat
   directly on that tint. The tint is gone: the picture is shown as supplied,
   and the copy sits in the shared brand glass block instead - see section 8z
   above, which is the single definition of that panel and carries the
   contrast derivation. `.wrap` IS the panel here, because this component's
   markup has no inner copy wrapper to glass.

   THE BAND'S HEIGHT MOVED TO THE SECTION. It used to live on `.wrap` as a
   min-block-size, which is correct while `.wrap` is an invisible box and
   wrong the moment it becomes a visible panel: at 360-520px tall the block
   would have been a near-full-bleed slab, which is the opaque-panel reading
   the owner rejected. The section holds the height now and the panel hugs its
   copy inside it, so the photograph opens up above and below the block.

   LAYOUT still differs from .cta-band on purpose: the kicker/heading sit at
   the panel's block-start and the lead below them, both start-aligned (never
   centred) - the client's own reference treatment for this shape, title over
   description, not a stacked centred column. `space-between` went with the
   min-block-size it was distributing; the existing `gap` is what spaces the
   two now. */

/* `display: grid` + `align-content: center` is what seats the copy block in
   the middle of the panel now that it no longer stretches to fill it. `.wrap`
   carries `margin: 0 auto`, and auto inline margins opt an item out of
   stretch alignment in grid, so it needs an explicit `inline-size: 100%`
   below or it would shrink to fit its longest line and the panel width would
   follow the copy instead of the design. That trap is documented at length on
   .subpage-hero in sdeira-subpage.css; it is the same one. */
.photo-band {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  display: grid;
  align-content: center;
  min-block-size: clamp(360px, 38vw, 520px);
  padding-block: clamp(40px, 5vw, 64px);
  padding-inline: 0;
  background: var(--surface-inverse-soft);
  color: var(--brand-white);
}

.photo-band::before {
  content: "";
  position: absolute;
  z-index: -2;
  inset: 0;
  background-image: var(--photo-band-image, none);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* The glass panel's own fill, blur, radius, padding and measure come from the
   shared rule in section 8z; only layout is set here. */
.photo-band > .wrap {
  position: relative;
  z-index: 1;
  inline-size: 100%;
  display: flex;
  flex-direction: column;
  gap: 32px;
}

/* A glass-marked photo panel gets the full shared section breathing room so
   its identity lines never crowd the content at either edge. On the SECTION,
   not on `.wrap`: `.wrap` is the copy block itself now, and this is the
   photograph's breathing room around it, not the block's internal padding. */
.photo-band.section--glass { padding-block: var(--sec-pad); }

.photo-band-top .kicker {
  color: var(--brand-white);
}

.photo-band-top h2 {
  margin: 0;
  max-inline-size: 60ch;
  color: var(--brand-white);
}

/* 88%, not 82%. The old value was tuned against a .76 pure-black scrim; on the
   .74 glass block it would have been the weakest line on the panel. 88% is the
   homepage's own secondary-copy value on this exact surface (sdeira-liwa.css,
   `.cta p` / `.hero-content-copy p`), so the two read the same. */
.photo-band-lead {
  max-inline-size: 60ch;
  margin: 0;
  font-size: var(--type-lead);
  line-height: var(--leading-lead);
  color: color-mix(in srgb, var(--brand-white) 88%, transparent);
}
.photo-band-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sec-head-gap); }
.photo-band-column h3 { margin: 0 0 12px; color: var(--brand-white); font-size: var(--type-card); }
@media (max-width: 980px) { .photo-band-columns { grid-template-columns: 1fr; } }

/* ONE OPTIONAL LINK, styled like any other .action-link over imagery (white
   by default, per that rule's own comment) - see photo_band()'s docstring
   in scripts/components.py for why this is never a .cta-actions row. Sits
   below the lead with the same rhythm the panel's own `gap` already gives
   every other top-level child, so no extra margin is declared. */
.photo-band-action {
  align-self: flex-start;
}

/* The wide variant keeps the same photo, glass block, type roles and
   responsive rhythm; it only gives a long, sourced narrative the wider
   approved measure.

   THE PANEL DOES NOT WIDEN WITH IT, and that is deliberate. Every variant
   shares the one glass-block measure from section 8z, because a panel that
   grows to the page's full width stops being a block on a photograph and
   becomes the slab the owner rejected - at 1280 of a 1440 viewport the
   picture survives only as an 80px margin. The variant is still distinct
   inside the shared panel: the base lead is capped at 60ch and this one at
   --photo-band-copy-measure (72ch), so the wide narrative does read wider,
   and the taller min-block-size and larger gap below are untouched. */
.photo-band--wide .photo-band-top h2,
.photo-band--wide .photo-band-lead { max-inline-size: var(--photo-band-copy-measure); }
.photo-band--wide > .wrap,
.photo-band--columns > .wrap {
  gap: var(--photo-band-wide-gap);
}

/* Both wide variants keep their documented taller panel. It sits on the
   SECTION now, for the reason given on .photo-band above: the height is the
   photograph's, and the glass block hugs its copy inside it. */
.photo-band--wide,
.photo-band--columns {
  min-block-size: var(--photo-band-wide-min-block);
}
.photo-band--columns .photo-band-copy {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--photo-band-wide-gap);
  max-inline-size: 100%;
}
.photo-band--columns .photo-band-lead { max-inline-size: none; }

/* The chaptered growth story can use the same photographic section treatment
   while retaining its shared step-list cards as readable raised surfaces. */
.photo-band .sec-head h2,
.photo-band .sec-head > p,
.photo-band .sec-head .kicker {
  color: var(--brand-white);
}

.step-list-photo .step-chapter {
  background: var(--surface-raised);
}

/* A photo-band's figures stay one continuous white reading card at every
   breakpoint; the alternating mobile tint belongs to standalone strips only. */
.photo-band .stat-strip {
  background: var(--glass-card-fill);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(120%);
  backdrop-filter: blur(var(--glass-blur)) saturate(120%);
}

/* Every cell reads as the same glass surface on the photograph. nth-child(even)
   only was enough while cells sat on their own sunken fill; the odd cells now
   also carry the shared `.stat` card fill, and a half-white half-transparent
   strip on the photo reads as a rendering fault. */
.photo-band .stat-strip .stat { background: transparent; }
.photo-band .stat b,
.photo-band .stat > span,
.photo-band .stat-strip--facts .stat > span { color: var(--brand-white); }
.photo-band :is(.stat-figure-prefix, .stat-figure-suffix) { color: var(--muted-on-dark); }
.photo-band .stat i.ph,
.photo-band .stat-strip--facts-icons .stat > i.ph { color: var(--figure-on-dark); }
.photo-band .stat p { color: var(--muted-on-dark); }
.photo-band .stat time { color: var(--label-on-dark); }
/* Same on-dark correction as .pad--inverse above: the divider is decoration
   on a photograph, not the flat-ground identity mark, so it follows the
   figure colour the icon beside it already takes. */
.photo-band .stat + .stat::before { background: var(--figure-on-dark); }

/* Dark chapter variant for photographic history sections. It keeps the
   chaptered step geometry and swaps only the established on-dark roles.
   The glass fill goes on .step-chapter alone: .step-chapter-header used to
   carry the same fill a second time, and because the header is its own grid
   cell rather than an overlay, that painted a second, more opaque glass
   layer only under the chapter-label column - visibly lighter than the
   single layer showing through the items beside it, on one flat card that
   is supposed to read as one tone. */
.step-list--chapters-dark .step-chapter {
  background: var(--glass-card-fill);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(120%);
  backdrop-filter: blur(var(--glass-blur)) saturate(120%);
}

/* Must be stated, not just omitted: the base (light-surface) rule for
   .step-chapter-header paints its own var(--surface-raised) fill, and
   without this the header cell would fall back to that light tone instead
   of showing the single dark glass layer above through. */
.step-list--chapters-dark .step-chapter-header {
  background: transparent;
}

.step-list--chapters-dark .step-chapter-header h3,
.step-list--chapters-dark .step h4 {
  color: var(--brand-white);
}

.step-list--chapters-dark .step-chapter-header p,
.step-list--chapters-dark .step-mark {
  color: var(--figure-on-dark);
}

.step-list--chapters-dark .step p {
  color: var(--muted-on-dark);
}

.step-list--chapters-dark .step-chapter-band {
  background: var(--line-on-dark);
}

/* A compact lifecycle model can sit over photography without losing the
   shared step hierarchy: the established glass surface carries the photo
   through while the on-dark roles keep every stage readable. */
.step-list-photo .step-list--divided {
  background: var(--glass-card-fill);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(120%);
  backdrop-filter: blur(var(--glass-blur)) saturate(120%);
}

.step-list-photo .step-list--divided .step {
  background: transparent;
}

.step-list-photo .step-list--divided .step h3 {
  color: var(--brand-white);
}

.step-list-photo .step-list--divided .step p {
  min-block-size: 2lh;
  color: var(--muted-on-dark);
}

.step-list-photo .step-list--divided .step-mark {
  color: var(--figure-on-dark);
}

.step-list-photo .step-list--divided .step:nth-child(3n + 2)::before,
.step-list-photo .step-list--divided .step:nth-child(3n)::before {
  background: var(--line-on-dark);
}


/* =============================================================================
   9b. C9 - .spec-table : ROWS OF SPECIFICATION
   -----------------------------------------------------------------------------
   New, but not invented: the row rhythm is .news-item-meta's and the hairline is
   var(--line), so it sits in the same system as everything above it.

   ON A PHONE IT STACKS, and each cell carries its column heading as a data
   attribute so the stacked form is still readable. That is the one thing a
   table cannot do with CSS alone and the reason .spec-table has a JS module -
   the headings are read off <thead> at runtime rather than typed twice into the
   markup, which is how they drift.
   ========================================================================== */

.spec-table-scroll {
  overflow-x: auto;
}

.spec-table {
  inline-size: 100%;
  border-collapse: collapse;
  text-align: start;
}

.spec-table th,
.spec-table td {
  padding: clamp(12px, 1.4vw, 18px) clamp(14px, 1.2vw, 20px);
  vertical-align: top;
}

/* Zebra, not rules. Alternating the row ground is the tonal way to keep the
   eye on one line across a wide table, and it does the job a hairline was
   doing without drawing anything. */
.spec-table tbody tr:nth-child(odd) { background: var(--surface-raised); }
.spec-table tbody tr:first-child :is(th, td):first-child { border-start-start-radius: var(--r); }
.spec-table tbody tr:first-child :is(th, td):last-child { border-start-end-radius: var(--r); }
.spec-table tbody tr:last-child :is(th, td):first-child { border-end-start-radius: var(--r); }
.spec-table tbody tr:last-child :is(th, td):last-child { border-end-end-radius: var(--r); }

/* Site owner, 4 Sep 2026 (preview review on #packages, pad--sunken ground):
   the even row was transparent by design (see the zebra rule above), which
   let the section's own Light Grey ground show through - correct on a White
   section (the grey row reads against a White floor), but on a Light Grey
   section the transparent row and the floor beneath it are the exact same
   colour, so "every second line reads as no colour at all". The token file
   has no third surface tone to reach for here (sdeira-tokens.css: "no third
   tone ... a component alternates against its ground"), and this row still
   needs to read as distinct from BOTH its White neighbour and the Light
   Grey floor at once, which neither existing tone does alone - so this
   mixes the two the system already has, halfway between them, the one
   value guaranteed visible against both. */
.pad--sunken .spec-table tbody tr:nth-child(even) {
  background: color-mix(in srgb, var(--surface-raised) 50%, var(--surface-sunken));
}

.spec-table thead th {
  color: var(--label);
  font-size: var(--type-meta);
  font-weight: 700;
  white-space: nowrap;
  /* Site owner, 4 Sep 2026 (preview review): a <th> is center-aligned by the
     browser's own default UA stylesheet, with nothing here overriding it -
     the one cell in this table that never got the same `text-align: start`
     the table rule above already sets. logical `start` (not `left`) so this
     follows RTL/Arabic without a mirrored override. */
  text-align: start;
}

.spec-table tbody th {
  color: var(--ink);
  font-size: var(--type-body);
  font-weight: 700;
  /* Same default-UA-center fix as thead th above - a <th> anywhere in this
     table needed this, not just the header row. */
  text-align: start;
}

.spec-table td { color: var(--muted); }

.pad--inverse .spec-table tbody tr:nth-child(odd) { background: var(--card-dark); }
.pad--inverse .spec-table thead th { color: var(--label-on-dark); }
.pad--inverse .spec-table tbody th { color: var(--brand-white); }
.pad--inverse .spec-table td { color: var(--muted-on-dark); }


/* =============================================================================
   9c. C10 - .gallery and .lightbox
   -----------------------------------------------------------------------------
   Every tile is var(--media-wide), the delivered master's own shape, so the
   lightbox shows the supplied file uncropped rather than a different crop from
   the one the grid showed.

   The lightbox is a native <dialog>. It gets Esc, focus trapping and the top
   layer from the platform instead of from us.
   ========================================================================== */

/* Full-bleed on purpose - see the docstring on components.gallery(). Native
   scrolling already covers touch (swipe) and a trackpad; pointer drag-to-
   scroll for a plain mouse is added in sdeira-sections.js, guarded there so a
   drag never also fires the tap-to-open lightbox handler beside it. */
.gallery-scroll {
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-inline: contain;
  scroll-snap-type: inline mandatory;
  scroll-padding-inline: var(--page-gutter);
  scrollbar-width: thin;
  scrollbar-color: var(--accent) var(--surface-sunken);
  cursor: grab;
  -webkit-user-select: none;
  user-select: none;
}

.gallery-scroll:active,
.gallery-scroll.is-dragging {
  cursor: grabbing;
}

/* Snapping while the pointer is actively dragging fights the drag itself -
   the scroller keeps snapping back toward the nearest tile mid-gesture. */
.gallery-scroll.is-dragging {
  scroll-snap-type: none;
}

.gallery-scroll::-webkit-scrollbar {
  height: 6px;
}

.gallery-scroll::-webkit-scrollbar-track {
  background: var(--surface-sunken);
}

.gallery-scroll::-webkit-scrollbar-thumb {
  background: var(--accent);
  border-radius: var(--r);
}

.gallery-scroll:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.gallery {
  display: flex;
  gap: clamp(16px, 2vw, 28px);
  padding-inline: var(--page-gutter);
  /* Content width, not 100%: a flex row without this shrinks to its
     container and the tiles wrap/compress instead of overflowing into the
     scroller, which is the one thing this layout needs them to do. */
  width: max-content;
}

.gallery-item {
  position: relative;
  display: block;
  overflow: hidden;
  padding: 0;
  border: 0;
  border-radius: var(--r);
  aspect-ratio: var(--media-wide);
  background: var(--surface-sunken);
  cursor: pointer;
  flex: 0 0 auto;
  /* Large enough to read as "explorable photography", not a thumbnail strip;
     capped so a wide desktop does not put one image edge to edge. */
  inline-size: min(72vw, 640px);
  scroll-snap-align: start;
}

.gallery-item img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  filter: saturate(.94) contrast(1.04);
  transition: transform var(--dur-slow) var(--ease-out),
    filter var(--dur-base) var(--ease-out);
  /* The browser's own native "ghost image" drag (dragstart on an <img>)
     fights the custom pointer drag-to-scroll below it - this leaves dragging
     to the scroller entirely. The tile is still a <button>, so click/tap and
     keyboard activation are both untouched. */
  pointer-events: none;
}

.gallery-item:is(:hover, :focus-visible) img {
  transform: scale(var(--zoom));
  filter: saturate(1) contrast(1.03);
}

.lightbox {
  inline-size: min(94vw, 1200px);
  max-block-size: 92vh;
  padding: 0;
  border: 0;
  border-radius: var(--r);
  background: var(--surface-inverse);
  color: var(--brand-white);
  overflow: hidden;
}

.lightbox::backdrop { background: var(--scrim-hero); }

.lightbox-figure {
  margin: 0;
  display: grid;
  gap: 0;
}

.lightbox-figure img {
  inline-size: 100%;
  block-size: auto;
  max-block-size: 78vh;
  object-fit: contain;
  background: var(--brand-black);
}

.lightbox-figure figcaption {
  padding: 14px 20px;
  color: var(--muted-on-dark);
  font-size: var(--type-meta);
}

.lightbox-bar {
  position: absolute;
  top: 0;
  right: 0;
  display: flex;
  gap: 4px;
  padding: 10px;
}

.lightbox-btn {
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border: 0;
  border-radius: var(--r);
  background: var(--scrim-control);
  color: var(--brand-white);
  font-size: var(--ic-md);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out);
}

.lightbox-btn:is(:hover, :focus-visible) { background: var(--brand-sweihan); }


/* =============================================================================
   9d. C11 - .map-panel
   -----------------------------------------------------------------------------
   A supplied map image at the master ratio, with pins positioned from record
   coordinates. NO THIRD-PARTY TILE PROVIDER and no embedded script: the only
   external request any generated page makes is the icon font it already loads.

   Pin coordinates travel as inline custom properties, exactly as the homepage's
   .company-card already carries its accent. They are data about one instance,
   not a reason for a stylesheet.
   ========================================================================== */

.map-panel {
  position: relative;
  overflow: hidden;
  border-radius: var(--r);
  background: var(--surface-sunken);
}

.map-stage {
  position: relative;
  aspect-ratio: var(--media-card);
}

.map-stage :is(img, iframe) {
  inline-size: 100%;
  block-size: 100%;
  border: 0;
  object-fit: cover;
}

.map-pin {
  position: absolute;
  top: var(--pin-y);
  left: var(--pin-x);
  transform: translate(-50%, -100%);
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  padding: 0;
  z-index: 2;
  border: 0;
  border-radius: var(--r);
  background: var(--scrim-control);
  color: var(--brand-white);
  font-size: var(--ic-lg);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out);
}

.map-pin:is(:hover, :focus-visible, [aria-expanded="true"]) {
  background: var(--accent-fill);
}

.map-card {
  position: absolute;
  inset-block-end: 16px;
  inset-inline-start: 16px;
  max-inline-size: min(340px, calc(100% - 32px));
  padding: 18px 20px;
  border-radius: var(--r);
  background: var(--scrim-control);
  color: var(--brand-white);
  z-index: 3;
}

.map-card[hidden] { display: none; }

.map-card h3 {
  margin: 0 0 6px;
  color: var(--brand-white);
  font-size: var(--type-label);
  line-height: var(--leading-label);
}

.map-card p {
  margin: 0;
  color: var(--brand-white);
  font-size: var(--type-meta);
}

.map-proximity {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(10px, 1.6vw, 18px);
  margin-block-start: var(--space-component);
  color: var(--label);
  font-size: var(--type-meta);
}

.map-proximity span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.map-proximity i.ph {
  color: var(--accent-strong);
  font-size: var(--icon-inline);
}


/* =============================================================================
   9e. C14 - .faq
   -----------------------------------------------------------------------------
   Native <details>/<summary>, zero JavaScript - the same mechanism the header's
   nav disclosure already uses, with the same caret and the same open state. An
   accordion built out of buttons and hidden divs would be a second answer to a
   question the site has already answered.
   ========================================================================== */

.faq {
  display: grid;
  gap: 8px;
  max-width: 860px;
  margin-inline: auto;
}

/* Each question is its own raised block with air around it, not a row in a
   ruled list. The open one steps a rung further so the answer reads as
   belonging to it. */
/* --surface-sunken at rest, not --surface-raised: white-on-white on the
   site's default White floor, the same E1 bug .stat/.news-card/.row-item/
   .icon-grid--amenities all had (see CHANGELOG, 26-27 Aug 2026) - found on
   /en/business/landbank/ during the sweep for other instances of it. The
   p.43 corner mark below is the same reuse those fixes made: no hover (a
   <details> element already has its own native open/close affordance, not
   a destination), background + mark only. */
.faq-item {
  position: relative;
  overflow: hidden;
  border-radius: var(--r);
  background: var(--surface-sunken);
  transition: background var(--dur-fast) var(--ease-out);
}

.pad--sunken .faq-item { background: var(--surface-raised); }

.faq-item[open] { background: var(--surface-sunken); }
.pad--sunken .faq-item[open] { background: var(--surface-raised); }

.faq-item::after {
  content: "";
  position: absolute;
  inset: auto 0 0 auto;
  z-index: 0;
  block-size: 90%;
  aspect-ratio: var(--photo-mark-p43-ratio);
  background: url("../images/patterns/lines.svg?v=d874099296964b63") no-repeat right bottom / 100% 100%;
  pointer-events: none;
}

[dir="rtl"] .faq-item::after {
    inset: auto auto 0 0;
    transform: scale(-1, 1);
}

.faq-item > summary,
.faq-item > :not(summary) {
  position: relative;
  z-index: 1;
}

.faq-item > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: clamp(16px, 1.8vw, 22px) clamp(18px, 2vw, 24px);
  color: var(--ink);
  font-size: var(--type-body);
  font-weight: 700;
  cursor: pointer;
  list-style: none;
}

.faq-item > summary::-webkit-details-marker { display: none; }

.faq-item > summary i.ph {
  flex: 0 0 auto;
  color: var(--icon);
  font-size: var(--ic-md);
  transition: transform var(--dur-base) var(--ease-out);
}

.faq-item[open] > summary i.ph { transform: rotate(180deg); }

.faq-item[open] > summary { color: var(--accent-text); }

/* THE ANSWER OPENS AND CLOSES WITH A HEIGHT + FADE TRANSITION, NOT A SNAP -
   site owner, 31 Aug 2026, on /business/landbank: the page grows noticeably
   as FAQ answers reveal, and an instant snap read as a jump rather than a
   transition. <details>/<summary> is kept exactly as it was (still zero
   JavaScript, still the header nav-disclosure's own mechanism) - only how
   the answer's own box responds to the [open] attribute changes.

   TWO propositions are animated together, not one:
   - HEIGHT via the grid-rows technique: .faq-answer is a one-row grid
     track sized 0fr at rest and 1fr when open - animatable, unlike
     height:auto. The UA stylesheet's own `details:not([open]) >
     :not(summary) { display: none }` is overridden below so the answer
     stays laid out (and therefore transitionable) at every state; the 0fr
     row is what actually hides it, at zero height with overflow clipped.
   - OPACITY + a small rise on .faq-answer-inner, so the revealed text
     visibly fades and settles into place instead of merely growing a box -
     the second half of "واضح وجميل" (clear AND beautiful) the site owner
     asked for, not just a taller container. --dur-slow (480ms), not
     --dur-base: the shorter duration read as barely perceptible on a
     preview showing a static screenshot mid-flip; --dur-slow is this
     system's own "make a size change actually register" duration
     (image-zoom, entrance-stagger - see sdeira-tokens.css). */
.faq-item:not([open]) > .faq-answer {
  display: grid;
}

.faq-answer {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-slow) var(--ease-out);
}

.faq-item[open] > .faq-answer { grid-template-rows: 1fr; }

.faq-answer-inner {
  overflow: hidden;
  min-block-size: 0;
  padding: 0 clamp(18px, 2vw, 24px) clamp(18px, 2vw, 26px);
  color: var(--muted);
  max-width: 68ch;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
}

.faq-item[open] > .faq-answer > .faq-answer-inner {
  opacity: 1;
  transform: translateY(0);
}

.faq-answer-inner p { margin: 0; }
.faq-answer-inner > * + * { margin-block-start: 14px; }

@media (prefers-reduced-motion: reduce) {
  .faq-answer,
  .faq-answer-inner {
    transition: none;
  }
}

.pad--inverse .faq-item { background: var(--card-dark); }
.pad--inverse .faq-item[open] { background: var(--card-dark-hover); }
.pad--inverse .faq-item > summary { color: var(--brand-white); }
.pad--inverse .faq-answer-inner { color: var(--muted-on-dark); }


/* =============================================================================
   10. C16 - .contact-details
   -----------------------------------------------------------------------------
   Derived from the footer's .foot-contact rows: same icon size, same gap, same
   label role. Promoted because the Contact page and every sector page repeat
   it, and a second implementation is how the phone number ends up formatted two
   ways on one site.
   ========================================================================== */

.contact-details {
  display: grid;
  gap: clamp(16px, 1.8vw, 24px);
}

.contact-line {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: 4px 14px;
}

.contact-line > i.ph {
  grid-row: 1 / span 2;
  align-self: center;
  color: var(--icon);
  font-size: var(--ic-md);
}

.contact-line dt {
  color: var(--label);
  font-size: var(--type-meta);
}

.contact-line dd {
  margin: 0;
  color: var(--ink);
}

.contact-line a {
  color: inherit;
  text-decoration: none;
}

.contact-line a:is(:hover, :focus-visible) { color: var(--link-hover); }

.pad--inverse .contact-line > i.ph { color: var(--icon-on-dark); }
.pad--inverse .contact-line dt { color: var(--label-on-dark); }
.pad--inverse .contact-line dd { color: var(--brand-white); }
.pad--inverse .contact-line a:is(:hover, :focus-visible) { color: var(--link-hover-on-dark); }


/* =============================================================================
   10b. C17 - .org-diagram : A HIERARCHY DRAWN FROM THE RECORD
   -----------------------------------------------------------------------------
   Moved here from page-companies.css on 2026-08-13, which was the site's only
   page-unique stylesheet and is now deleted. It was page-unique on the
   reasoning that the group structure appears on exactly one page; the Aryam
   communities are the second hierarchy, so the choice was to share the
   component or write the same drawing twice. This file is where a shared
   section component lives.

   THE CONNECTORS ARE CSS, NOT AN IMAGE OR AN SVG. A supplied diagram would be
   another place the structure lives, free to disagree with the page around it.
   Drawing it from the record means it cannot.
   ========================================================================== */

.org-diagram {
  display: grid;
  justify-items: center;
  inline-size: 100%;
  gap: 0;
}

/* Tier 1: the root of the hierarchy. */
.org-root {
  display: grid;
  place-items: center;
  min-inline-size: 220px;
  padding: 18px 28px;
  border-radius: var(--r);
  background: var(--surface-inverse);
  color: var(--brand-white);
  font-family: var(--font-display);
  font-size: var(--type-label);
  line-height: var(--leading-label);
  font-weight: 500;
  text-align: center;
}

/* The drop from the root to the tier below. A fixed height rather than a
   percentage, because the row below it grows with its content and a percentage
   would stretch the line with it. */
.org-drop {
  inline-size: 2px;
  block-size: 36px;
  background: var(--line);
}

.org-tier {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--grid-cols, 5), minmax(0, 1fr));
  gap: clamp(14px, 1.6vw, 24px);
  inline-size: 100%;
  /* Branches are different heights on purpose - on /companies two sectors have
     companies under them and two do not. Without this they stretch to match the
     tallest, which pushed the empty ones into tall blank boxes and left the
     labels at different heights. */
  align-items: start;
}

/* One tier has one connector. A half-segment on every branch left a visible
   break in each grid gutter, making one hierarchy read as fragments. */
.org-tier::before {
  content: "";
  position: absolute;
  z-index: 0;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 2px;
  background: var(--line);
}

.org-branch {
  position: relative;
  display: grid;
  justify-items: center;
  align-content: start;
  gap: 0;
}

/* Tier 2: a branch label - a sector on /companies, a community family on
   /projects/aryam. */
.org-sector {
  display: grid;
  place-items: center;
  inline-size: 100%;
  min-block-size: 64px;
  padding: 14px 18px;
  border-radius: var(--r);
  /* Raised, not outlined. The tier is legible from its tone and its position
     in the drawing; a rule around it adds a second, weaker statement of the
     same thing.
     --surface-sunken, not --surface-raised: white-on-white on the site's
     default White floor (found on /en/companies/ during the sweep for other
     E1 instances - see CHANGELOG, 26-27 Aug 2026). No p.43 corner mark here
     unlike the other cards in that sweep: at a 64px-tall centred-label box
     the mark has no room to read as anything but clutter behind the text -
     a different shape from the generously-padded icon/logo/accordion cards
     the mark was added to. */
  background: var(--surface-sunken);
  color: var(--ink);
  font-size: var(--type-action);
  font-weight: 700;
  text-align: center;
}

.pad--sunken .org-sector { background: var(--surface-raised); }

/* Tier 3: the leaves under a branch. A branch with none simply has an empty
   list, and the drop above it is suppressed rather than left hanging. */
.org-companies {
  display: grid;
  /* Was 8px - three logo tiles stacked that tight read as touching. 12px
     matches the same "tight cluster" step already used elsewhere in the
     system (--land-map-gap, --assistant-nudge-gap). */
  gap: 12px;
  inline-size: 100%;
  margin-block-start: 0;
}

.org-company {
  position: relative;
  isolation: isolate;
  display: grid;
  place-items: center;
  gap: 8px;
  padding: 12px 14px;
  border-radius: var(--r);
  /* WHITE, NOT THE SUNKEN GREY. Site owner's rule, 28 Aug 2026: a logo never
     sits on a brand colour or a tinted surface, only on white. --surface-base
     is the token file's white; do not write a literal here, and do not tint
     this back to a surface step to "separate" the tile - the diagram's own
     connectors already do the separating. */
  background: var(--surface-base);
  color: var(--ink);
  font-size: var(--type-meta);
  text-align: center;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}

/* Linked leaves use the same directional orange wipe as the shared primary
   action. The logo remains the visual anchor and reverses to white with it. */
a.org-company::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  --wipe-p: 0%;
  background-image: linear-gradient(var(--motion-wipe-gradient-angle),
    var(--accent-fill) 0 var(--wipe-p),
    transparent var(--wipe-p) 100%);
  transition: --wipe-p var(--dur-slow) var(--ease-out);
  pointer-events: none;
}

a.org-company:is(:hover, :focus-visible)::before { --wipe-p: 100%; }
.org-company > * { position: relative; z-index: 1; }

.org-company-mark {
  display: grid;
  place-items: center;
  inline-size: 100%;
  block-size: 2lh;
}

/* A mark is a mark, not a crop: every mark receives the same bounded slot, and
   is sized by that slot's HEIGHT. That is only uniform if the supplied files
   share a canvas ratio - where they do not, two marks at the same slot height
   render at two different sizes. The eight Aryam community files were padded
   to one 125x55 canvas for exactly this reason (see the note in aryam.toml);
   fix a mismatched set at the asset, not with a per-page exception here. */
.org-company-logo {
  inline-size: auto;
  block-size: 100%;
  min-inline-size: 0;
  min-block-size: 0;
  max-inline-size: 100%;
  max-block-size: 100%;
  object-fit: contain;
  transition: filter var(--dur-slow) var(--ease-out), opacity var(--dur-fast) var(--ease-out);
}

.org-company-name {
  font-size: var(--type-meta);
  line-height: var(--leading-meta);
}


/* THE THREE ZOAD EXCEPTIONS ARE GONE, 28 Aug 2026. All three existed for one
   reason: the supplied artwork was the full lockup, whose navy emblem panel is
   baked into the image. A `mix-blend-mode: multiply` hid that panel against the
   node surface, and a second white mark was layered over it on hover so the
   panel would not show through the orange wipe.

   The mark is now the WORDMARK ALONE, cut from the same approved file
   (ZOAD Logo / Blueberry Full Colour), navy ink on transparent. Site owner's
   rule, same date: a logo never sits on a brand colour, only on white. With no
   panel in the artwork there is nothing to blend away and nothing to mask, and
   Zoad takes the same treatment as every other mark on the site.

   Do not reintroduce a blend mode here. If a supplied logo carries a coloured
   panel, the panel is removed from the ASSET, which is what happened. */

a.org-company:is(:hover, :focus-visible) .org-company-logo {
  filter: brightness(0) invert(1);
}


a.org-company:is(:hover, :focus-visible) {
  color: var(--link-hover);
}

/* A leaf with no confirmed destination is not a link. It keeps its box so the
   structure is complete, and drops the affordance so nothing suggests a
   destination that does not exist. */
span.org-company { color: var(--muted); }

/* A leaf is --surface-sunken, which is also what .pad--sunken paints the
   section - on that ground the boxes vanish into it. Stepping them to
   --surface-base keeps the drawing's three tones one step apart in the same
   order (branch white, leaf base, ground sunken) rather than flattening two
   of them together. Same move as `.pad--raised :is(.row-item, ...)` in section
   1: elevation here is a tonal step, never a shadow. */
.pad--sunken .org-company { background: var(--surface-base); }
.pad--sunken a.org-company:is(:hover, :focus-visible) { background: var(--surface-base); }

/* The shorter drop between a branch and its leaves. The element is only
   emitted when there are leaves, so it needs no :has() guard. */
.org-drop--company { block-size: 24px; }


/* =============================================================================
   11. PENDING MARKERS
   -----------------------------------------------------------------------------
   The project forbids inventing a value. Where a real one has not arrived, the
   position is marked rather than filled, and the marker is deliberately neutral
   grey: a missing value is not an error, it is a value that has not been
   supplied yet.
   ========================================================================== */

.is-pending {
  color: var(--pending);
  font-style: normal;
}

/* TWO DARK GROUNDS, NOT ONE. `.pad--inverse` is the declared dark surface, but
   a photo band is dark too: its scrim sits at or below the documented .86
   minimum over a photograph, and a figure strip drawn on it composites to
   about #535355. A `--pending` marker there measured 1.5:1 against its own
   ground, beside two white sibling figures - present in the markup, and
   unreadable. Found on /business/property-development after an illustrative
   figure was replaced by the marker on 28 Aug 2026, which was the first time
   the two treatments met. */
:is(.pad--inverse, .photo-band) .is-pending { color: var(--pending-on-dark); }

/* THE BOXED "AWAITING DATA" PANEL IS GONE.
   It was the honest answer to missing content and the wrong one to look at:
   nine dashed grey boxes down a demo site read as an unfinished build rather
   than as a design. Unapproved body copy is now placeholder prose in the
   record, rendered by the real component at the real length - which is what
   the client asked for and what actually reviews. .is-pending stays for the
   case a boxed note never covered: a single missing VALUE inside a real
   sentence or a real figure slot, where there is nothing to simulate. */


/* =============================================================================
   12. RESPONSIVE AND DIRECTION
   -----------------------------------------------------------------------------
   ALL breakpoint work for this file is collected here, each rule saying what it
   changes. This is the discipline sdeira-v2.css lost: that file redefines .hero
   51 times across 107 media blocks and the winner is decided by source order.
   ========================================================================== */

/* ---- Direction ------------------------------------------------------------
   Every decorative offset above is PHYSICAL, because logical properties once
   pushed decoration on top of the text of an RTL page (docs/quality/design-qa.md).
   These are the mirrored halves. The ROTATION IS NOT MIRRORED anywhere: the
   Sdeira mark has one physical slant, and direction may move a stripe group to
   the opposite edge but must never flip its angle. */
[dir="rtl"] .split-ribbon::after { left: auto; right: 8%; }
/* The caption block hangs off the READING edge, so it swaps sides with the
   language. The radius does not need a pair any more - the block is a whole
   rounded panel now rather than a corner tab, so all four corners are the same
   and nothing about it is handed. */
[dir="rtl"] .split-badge { left: auto; left: -36px; }
[dir="rtl"] .stat + .stat::before {
  left: auto;
  right: calc(clamp(18px, 1.9vw, 30px) / -2);
}
[dir="rtl"] .step-list--divided .step::before { left: auto; right: 0; }
/* The nodes swap sides. The spine is centred so it needs no pair, and the
   node's own rotation is untouched - one slant, always. */

[dir="rtl"] .step-chapter-header p {
    text-align: right;
}

[dir="rtl"] .step-list--timeline .step::after {
  right: auto;
  left: calc(clamp(28px, 3vw, 46px) * -1 - 5px);
}
[dir="rtl"] .step-list--timeline .step:nth-child(even)::after {
  left: auto;
  right: calc(clamp(28px, 3vw, 46px) * -1 - 5px);
}
/* The glyph itself is already ph-arrow-left here (cta_band() in
   scripts/components.py swaps it per language, the way action-link's
   ph-arrow-up-left and row-item-action's own glyph do NOT - row-item-action
   always emits ph-arrow-right and relies on scaleX below instead). Mirroring
   an already-mirrored glyph a second time is what put a rightward arrow
   inside an Arabic button reading right-to-left - only the hover TRAVEL needs
   flipping here, not the glyph. */
[dir="rtl"] .cta-action:is(:hover, :focus-visible) .cta-arrow { transform: translateX(-4px); }
/* .pad--edge and .pad--field's own RTL mirror retired 27 Aug 2026 along with
   the positioned band it moved: solid-a.svg's ground (centred, inset: 0) has
   no left/right position of its own to mirror - same reason the homepage's
   .value-sections/.needs never needed this rule either. Leaving the old
   override in would have clipped two sides of that inset: 0 box back down
   to a 74%-wide strip in RTL. */
[dir="rtl"] .row-item:is(:hover, :focus-within) i.row-item-action { transform: translateX(-4px); }

/* The action glyph points along the reading direction. */
/* [dir="rtl"] i.row-item-action { transform: scaleX(-1); } */
/* [dir="rtl"] .row-item:is(:hover, :focus-within) i.row-item-action { transform: scaleX(-1) translateX(4px); } */

/* A square-out glyph is a departure marker, not a directional arrow. It keeps
   the same static orientation and has no travel in either writing direction. */
i.row-item-action.ph-arrow-square-out,
.row-item:is(:hover, :focus-within) i.row-item-action.ph-arrow-square-out,
[dir="rtl"] i.row-item-action.ph-arrow-square-out,
[dir="rtl"] .row-item:is(:hover, :focus-within) i.row-item-action.ph-arrow-square-out {
  transform: none;
}



/* One-off exception, not a system change: site owner asked (18 Aug 2026) for
   the departure icon on the "ضمن قطاع المدن الاقتصادية والمناطق الحرة" AD
   Ports Group button on /about/our-story to mirror on the Arabic page only -
   scoped to that page's own section id so it cannot match any other external
   button on the site (Eskan/Khadamat/Zoad/Your Laundry keep the shared static
   rule above). The English mirror keeps the default static icon. */
/*[dir="rtl"] i.ph.ph-arrow-square-out {
  transform: scaleX(-1);
}*/

/* ---- 1180px: four figures do not fit ------------------------------------- */
@media (max-width: 1180px) {
  /* Two columns of figures. The separator moves to the even children. */
  .stat-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Three figures become two plus one. Let the orphaned figure use the full
     row instead of leaving a visibly unfinished half-width cell. */
  .stat-strip:not(.stat-strip--facts):has(> .stat:nth-child(3):last-child) > .stat:last-child {
    grid-column: 1 / -1;
  }
  .stat + .stat::before { content: none; }
  .stat:nth-child(even)::before { content: ""; }
  /* Six logo tiles per row becomes three. */
  .logo-wall { grid-template-columns: repeat(min(var(--grid-cols, 4), 3), minmax(0, 1fr)); }
}

/* ---- 980px: the two-column layouts collapse ------------------------------ */
@media (max-width: 980px) {
  /* C17: several branches across become two, and the connector rule would then
     join branches that are no longer siblings on screen. It is dropped entirely
     and each branch keeps its own drop, which is honest at this width: the
     hierarchy is still readable, the horizontal join is not. */
  .org-tier { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .org-tier::before { content: none; }

  /* Copy above media, in both variants. --reverse's `order` is reset or the
     media would still lead on a phone, where the copy is what orients. */
  .split { grid-template-columns: minmax(0, 1fr); }
  /* .split--logo-wide is a flex row above 920px (desktop rule near its own
     definition); stack it as a plain flex column here instead of reinstating
     the retired grid-template-areas scheme. .split-copy is normal in-flow
     content again (no display: contents), so its own children keep their
     natural document order - heading, then paragraphs, then actions - with
     the logo dropped between the heading and the first paragraph via order,
     matching the desktop reading order. */
  .split--logo-wide {
    display: flex;
    flex-direction: column;
    gap: var(--sec-head-gap);
  }
  .split--logo-wide .split-media {
    order: 1;
  }
  .split--logo-wide .split-copy {
    display: contents;
  }
  .split--logo-wide .split-copy > h2 {
    order: 1;
  }
  /* The kicker is a <p>, so the generic p/actions rule below would sink it
     under the logo on phones; it leads the section on desktop, so it must
     lead here too (25 Sept change request). */
  .split--logo-wide .split-copy > .kicker {
    order: 0 !important;
    align-self: flex-start;
  }
  .split--logo-wide .split-copy > :is(p, .cta-actions) {
    order: 2;
  }
  .split--media-dominant {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "heading"
      "media"
      "body"
      "actions";
    row-gap: var(--sec-head-gap);
  }
  [dir="rtl"] .split--media-dominant {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "heading"
      "media"
      "body"
      "actions";
  }
  .split--media-dominant .split-media { justify-self: start; }
  .split--media-dominant .split-copy-body,
  .split--media-dominant .split-media { margin-block-start: 0; }
  .split--logo-body {
    grid-template-areas:
      "kicker"
      "heading"
      "body"
      "media";
  }
  .split--logo-body .split-media { justify-self: start; }
  .split--media-dominant-no-actions {
    grid-template-areas:
      "heading"
      "media"
      "body";
  }
  [dir="rtl"] .split--media-dominant-no-actions {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "heading"
      "media"
      "body";
  }
  .split--heading-logo .split-heading {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--sec-head-gap);
  }
  .split--heading-logo .split-heading > .split-logo-badge--heading {
    grid-column: 1;
    grid-row: auto;
    align-self: start;
    justify-self: start;
  }
  .split--media-dominant-no-actions .split-copy-body,
  .split--media-dominant-no-actions .split-media { align-self: start; }
  /* Both boxes go from shrink-to-fit to definite here. At this width "media"
     is its own grid row (it shares one with the heading at the two-column
     width above, which is what gives it height there instead), and both the
     container (inline-size auto) and its img (inline-size/block-size auto,
     max-inline-size 100%) size from content - with no content loaded yet,
     that is 0x0 for both, independent of each other, until something breaks
     the circle. Measured this collapsing under a fast automated scroll (the
     visual-qa-fix headless capture path): the img's own load never started
     in that window - no entry in performance.getEntriesByType('resource') -
     while a slow manual scroll-and-settle on the same page loaded it fine, so
     this reads as scroll-speed-sensitive lazy-load timing rather than a
     defect only real visitors would never trigger either way. Fixing it here
     regardless: a stable floor is strictly better than a box that can render
     at 0x0 and jump when the image does arrive. The two logos this variant
     carries (AD Ports 338x55, Khadamat 548x136) are different ratios, so the
     container keeps aspect-ratio: auto above - inline-size/min-block-size are
     a floor, not a lock. */
  .split--logo-wide .split-media {
    justify-self: start;
    inline-size: min(100%, 240px);
    min-block-size: 40px;
  }
  .split--logo-wide .split-media img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: contain;
    order:1;
  }
  .split--logo-wide.split--logo .split-media {
    min-block-size: 0;
  }
  .split--logo-wide.split--logo .split-media img {
    inline-size: auto;
    block-size: auto;
    min-block-size: 0;
    max-inline-size: 100%;
    max-block-size: 100%;
    object-fit: contain;
  }
  .split--reverse .split-media { order: 0; }
  /* The figure now spans the whole column, so its caption block's outward
     overhang would hang into - and past - the page gutter. It pulls in flush
     with the reading edge and keeps only the vertical overhang, which has the
     row gap under it to fall into. */
  .split-badge,
  .split-logo-badge { inset-inline-start: 0; max-inline-size: min(86%, 340px); }
  [dir="rtl"] .split-badge { left: auto; right: 0; }
  /* The person variant's own column pair collapses the same way the base
     .split above it does - photo above card, full width, in document order -
     just restated here because the unscoped .person-rows .split rule that
     sets its fixed photo column would otherwise keep winning past this
     breakpoint on specificity alone. */
  .person-rows .split { grid-template-columns: minmax(0, 1fr); }
  /* The two peer columns stack; the lead sits under the heading like any
     other .sec-head, so no separate alignment rule survives the collapse. */
  .sec-head--split { grid-template-columns: minmax(0, 1fr); align-items: start; }
  /* Three icons per row becomes two. */
  .icon-grid { grid-template-columns: repeat(min(var(--grid-cols, 3), 2), minmax(0, 1fr)); }
  .icon-grid--balanced-five .icon-item,
  .icon-grid--balanced-five .icon-item:nth-child(4),
  .icon-grid--balanced-five .icon-item:nth-child(5) { grid-column: auto; }
  .icon-grid--balanced-five-stretch .icon-item,
  .icon-grid--balanced-five-stretch .icon-item:nth-child(4),
  .icon-grid--balanced-five-stretch .icon-item:nth-child(5) { grid-column: auto; }
  /* A fixed-count card list drops to two; the auto-fill default is untouched. */
  .news-list--cols { grid-template-columns: repeat(min(var(--grid-cols), 2), minmax(0, 1fr)); }
  /* .gallery is a flex scroller, not a grid - see components.gallery() - so
     it needs no column-count override here. Its item width is already fluid
     (min(72vw, 640px)). */
  .people-grid { grid-template-columns: repeat(min(var(--grid-cols, 3), 2), minmax(0, 1fr)); }
  .step-list--compact.step-list--divided { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Three action cards do not fit at this width; two do. */
}

/* ---- 860px: the timeline stops zig-zagging -------------------------------- */
@media (max-width: 860px) {
  /* Two half-width columns of prose stop working here, well before the rest of
     the page needs to change. The spine moves to the start edge and every
     station sits after it. */
  .step-list { grid-template-columns: minmax(0, 1fr); }
  /* A one-column sequence has no false vertical divider. Its surface stays
     pure white throughout, including every responsive state. */
  .step-list--divided .step:nth-child(3n + 2)::before,
  .step-list--divided .step:nth-child(3n)::before { content: none; }
  .step-list--divided .step:nth-child(even) { background: var(--surface-raised); }
  .step-list-photo .step-list--divided .step:nth-child(even) { background: transparent; }
  .step-list--timeline::before { left: 5px; }
  .step-list--timeline .step,
  .step-list--timeline .step:nth-child(even) {
    inline-size: auto;
    margin-inline-start: clamp(26px, 6vw, 40px);
  }
  .step-list--timeline .step::after,
  .step-list--timeline .step:nth-child(even)::after {
    right: auto;
    left: calc(clamp(26px, 6vw, 40px) * -1 - 5px);
  }
}

@media (max-width: 860px) {
  [dir="rtl"] .step-list--timeline::before { left: auto; right: 5px; }
  [dir="rtl"] .step-list--timeline .step::after,
  [dir="rtl"] .step-list--timeline .step:nth-child(even)::after {
    left: auto;
    right: calc(clamp(26px, 6vw, 40px) * -1 - 5px);
  }
}

/* ---- 640px: single column throughout ------------------------------------- */
@media (max-width: 640px) {
  /* C17: one branch per row, and a shorter drop between tiers so the whole
     drawing is not mostly line. */
  .org-tier { grid-template-columns: minmax(0, 1fr); }
  .org-drop { block-size: 20px; }

  .stat-strip { grid-template-columns: minmax(0, 1fr); }
  .stat:nth-child(even)::before { content: none; }
  .stat:nth-child(even) { background: var(--surface-sunken); }
  .pad--inverse .stat:nth-child(even) { background: var(--card-dark); }
  .icon-grid { grid-template-columns: minmax(0, 1fr); }
  .people-grid { grid-template-columns: minmax(0, 1fr); }
  .news-list--cols { grid-template-columns: minmax(0, 1fr); }
  .step-chapter { grid-template-columns: minmax(0, 1fr); }
  .step-chapter-header {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    padding: 20px;
  }
  .step-chapter-header p { justify-self: end; }
  .step-chapter-band {
    inset-inline-end: auto;
    top: auto;
    bottom: calc(var(--step-separator-thickness) / -2);
    left: 50%;
    transform: translateX(-50%) rotate(var(--brand-stripe-angle));
  }
  .step-list--chapters .step {
    align-content: start;
    padding: 22px 18px;
    text-align: center;
  }
  /* Same note as the 980px block above: .gallery no longer needs this. */
  /* The location card would cover the map at this width, so it sits under it. */
  .map-card {
    position: static;
    max-inline-size: none;
    border-start-start-radius: 0;
    border-start-end-radius: 0;
  }
  .map-proximity {
    padding-inline-start: calc(
      var(--assistant-panel-offset-inline-mobile) +
      var(--assistant-launcher-target-mobile)
    );
  }
  /* A table cannot reflow, so it stacks: each cell carries its column heading,
     read off <thead> by sdeira-sections.js rather than typed twice. */
  .spec-table thead { display: none; }
  .spec-table tr { display: grid; padding-block: 10px; }
  .spec-table th,
  .spec-table td { padding-block: 4px; }
  .spec-table tbody tr { padding: 14px 16px; border-radius: var(--r); }
  .spec-table td::before {
    content: attr(data-label) ": ";
    color: var(--label);
    font-size: var(--type-meta);
    font-weight: 700;
  }
  .logo-wall { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The row's action glyph moves under the title rather than squeezing it. */
  .row-item { grid-template-columns: minmax(0, 1fr); }
  .row-list--square { grid-template-columns: minmax(0, 1fr); }
  .row-list--square .row-item { aspect-ratio: auto; }
  i.row-item-action { grid-column: 2; grid-row: auto; justify-self: start; }
  /* Numbered variant collapses to the same single column; the ordinal sits
     above the title instead of beside it. */
  .row-item--numbered { grid-template-columns: minmax(0, 1fr); }
  .row-item-num,
  .row-item--numbered h3,
  .row-item--numbered .row-item-logo,
  .row-item--numbered .row-item-meta,
  .row-item--numbered i.row-item-action { grid-column: 1; grid-row: auto; }
  .row-item-num { justify-self: start; }
  .row-list--company-lines.row-list--company-branded .row-item {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 14px;
  }
  .row-list--company-lines.row-list--company-branded .row-item > :is(.row-item-logo, .row-item-copy, .row-item-action-wrap) {
    grid-column: 1;
  }
  .row-list--company-lines.row-list--company-branded .row-item-logo {
    justify-self: start;
  }
  /* Narrow: one per line, each still only as wide as it needs to be. */
  .cta-actions { flex-direction: column;  }
  .split-copy .cta-actions { align-items: flex-start; }
  /* The step marker keeps its width but the copy gets the rest. */
  .step { gap: 4px 14px; }
}

/* =============================================================================
   E7 - SECTOR CARD
   -----------------------------------------------------------------------------
   The five businesses are one navigational family. The homepage and the
   businesses overview use this markup; the overview only supplies the grid
   context around it. A sector card remains one full-card destination in every
   pointer, keyboard and touch state.
   ========================================================================== */

.sector-grid--content {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: clamp(6px, .55vw, 12px);
}

.sector-grid--content .sector-card {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: flex-end;
  min-inline-size: 0;
  min-block-size: clamp(210px, 24svh, 260px);
  overflow: hidden;
  border-radius: var(--r);
  background: var(--surface-inverse);
  color: var(--brand-white);
}

.sector-grid--content .sector-card-media,
.sector-grid--content .sector-card-img,
.sector-grid--content .sector-card-veil {
  position: absolute;
  inset: 0;
}

.sector-grid--content .sector-card-media { z-index: -1; }

.sector-grid--content .sector-card-img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  filter: saturate(.9) brightness(.9);
  transition: transform var(--dur-slow) var(--ease-out);
}

.sector-grid--content .sector-card-veil {
  background: var(--scrim-hero);
  transition: opacity var(--dur-base) var(--ease-out);
}

.sector-grid--content .sector-card-media::after {
  content: "";
  position: absolute;
  inset: -1px -40%;
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--surface-brand-deep) 76%, transparent) 0%,
    color-mix(in srgb, var(--surface-brand-deep) 90%, transparent) 100%);
  clip-path: polygon(22% 0, 100% 0, 78% 100%, 0 100%);
  transform: translate3d(78%, 0, 0);
  transition: transform var(--dur-slow) var(--ease-out);
}

.sector-grid--content .sector-card-body {
  inline-size: 100%;
  padding: clamp(18px, 2.1vw, 30px);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}

.sector-grid--content .sector-card-mark {
  display: grid;
  place-items: center;
  inline-size: var(--icon-default);
  block-size: var(--icon-default);
  border-radius: var(--r);
  background: color-mix(in srgb, var(--brand-white) 14%, transparent);
  color: var(--brand-white);
}

.sector-grid--content .sector-card-mark i.ph { font-size: var(--icon-control); }

.sector-grid--content .sector-card-title,
.sector-grid--content .sector-card-title a {
  margin: 0;
  color: inherit;
  font-size: var(--type-card);
  line-height: var(--leading-card);
  text-decoration: none;
}

.sector-grid--content .sector-card-sub {
  min-block-size: calc(var(--card-sub-lines) * 1lh);
  color: var(--muted-on-dark);
  font-size: var(--type-body);
  line-height: var(--leading-body);
}

.sector-grid--content .sector-card-action {
  position: static;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: auto;
  color: var(--brand-white);
  font-size: var(--type-action);
  text-decoration: none;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-base) var(--ease-out);
}

/* This pseudo link is why pointer users can activate the complete card while
   the visible action remains a normal, keyboard-focusable anchor. */
.sector-grid--content .sector-card-action::before {
  content: "";
  position: absolute;
  inset: 0;
}

.sector-grid--content .sector-card:is(:hover, :focus-within) .sector-card-media::after { transform: none; }
.sector-grid--content .sector-card:is(:hover, :focus-within) .sector-card-action { opacity: 1; transform: none; }
.sector-grid--content .sector-card:focus-within { outline: 3px solid var(--accent-on-dark); outline-offset: -3px; }

@media (hover: none), (pointer: coarse) {
  .sector-grid--content .sector-card-action { opacity: 1; transform: none; }
}

@media (max-width: 980px) {
  .sector-grid--content { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 640px) {
  .sector-grid--content { grid-template-columns: minmax(0, 1fr); }
  .sector-grid--content .sector-card { min-block-size: clamp(196px, 58vw, 242px); }
}

@media (prefers-reduced-motion: reduce) {
  .sector-grid--content :is(.sector-card-img, .sector-card-veil, .sector-card-media::after, .sector-card-action) { transition: none; }
}

@media (max-width: 640px) {
  /* A nine-stage history leaves one orphaned milestone in a two-column phone
     grid. At this established handset breakpoint it becomes a single reading
     sequence, while the desktop and tablet scans retain their 3 → 2 rhythm. */
  .step-list--compact.step-list--divided { grid-template-columns: minmax(0, 1fr); }
  .pad--inverse .stat:nth-child(even) { background: var(--card-dark-hover); }
}

/* =============================================================================
   E7 - BUSINESS DIRECT ROUTES
   -----------------------------------------------------------------------------
   The business overview uses the homepage's .needs-route markup rather than a
   second business-card family. This named list variant keeps five routes easy
   to scan while retaining the same media, raised copy surface and identity
   chevron.
   ========================================================================== */

.business-needs-routes {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sec-head-gap);
  margin-block-start: var(--sec-head-gap);
}

.business-needs-routes .business-needs-route {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(clamp(330px, 40vw, 470px), auto);
  align-items: center;
  min-block-size: clamp(330px, 40vw, 470px);
  color: var(--ink);
  text-decoration: none;
  isolation: isolate;
}

.business-needs-routes .needs-route-media {
  grid-area: 1 / 1;
  justify-self: end;
  align-self: center;
  position: relative;
  z-index: 1;
  display: block;
  overflow: hidden;
  inline-size: min(72%, 800px);
  /* Not var(--media-wide): site owner reviewed the rendered 4:3 route card,
     16 Aug 2026, and asked for 16:9 back on this component specifically -
     see the matching comment in sdeira-v2.css / sdeira-v2-en.css. */
  aspect-ratio: 16 / 9;
  background: var(--surface-sunken);
  border-radius: var(--r);
}

.business-needs-routes .business-needs-route:nth-child(even) .needs-route-media { justify-self: start; }

.business-needs-routes .needs-route-media img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-out);
}

/* The route image uses the same on-photo identity band as every shared figure.
   Its side follows the alternating media position; the mark's angle never
   mirrors. */
.business-needs-routes .business-needs-route:nth-child(odd) .needs-route-ribbon::after {
  left: auto;
  right: 8%;
}

.business-needs-routes .business-needs-route:nth-child(even) .needs-route-ribbon::after {
  left: 8%;
  right: auto;
}

[dir="rtl"] .business-needs-routes .business-needs-route:nth-child(odd) .needs-route-ribbon::after {
  left: 8%;
  right: auto;
}

[dir="rtl"] .business-needs-routes .business-needs-route:nth-child(even) .needs-route-ribbon::after {
  left: auto;
  right: 8%;
}

.business-needs-routes .needs-route-copy {
  grid-area: 1 / 1;
  justify-self: start;
  direction: inherit;
  position: relative;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  align-self: center;
  inline-size: min(48%, 520px);
  padding: clamp(28px, 3.4vw, 46px);
  overflow: hidden;
  border-radius: var(--r);
  background: var(--surface-raised);
  transition: background-color var(--dur-base) var(--ease-out);
}

.business-needs-routes .business-needs-route:nth-child(even) .needs-route-copy { justify-self: end; }

/* The same directional fill as the homepage route cards. It starts from the
   reader's start edge through the shared motion angle, while the card's
   geometry remains fixed. */
.needs-route .needs-route-copy::after,
.business-needs-routes .needs-route-copy::after {
  content: "";
  position: absolute;
  z-index: 0;
  inset: 0;
  clip-path: none;
  transform: none;
  --wipe-p: 0%;
  --wipe-fill: var(--surface-brand-deep);
  background-color: transparent;
  background-image: linear-gradient(var(--route-card-wipe-angle), var(--wipe-fill) 0 var(--wipe-p), transparent var(--wipe-p) 100%);
  transition: --wipe-p var(--dur-slow) var(--ease-out);
  pointer-events: none;
}


.section-pattern {
    position: relative;
    isolation: isolate;
}

.section-pattern::after {
    content: "";
    position: absolute;
    z-index: -1;
    inset: 0;
    background: url("/wp-content/themes/sdeira/assets/images/patterns/solid-a.svg") no-repeat center / cover;
    opacity: 0.05;
    pointer-events: none;
}

.needs-route:is(:hover, :focus-within) .needs-route-copy::after {
  --wipe-p: 100%;
}

.business-needs-routes .needs-route-copy > :not(.card-chevron) { position: relative; z-index: 2; }

.business-needs-routes .needs-route-copy > strong {
  margin: 0;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--type-card);
  font-weight: 400;
  line-height: var(--leading-card);
  text-wrap: balance;
}

/* Community route cards retain their semantic H3, while using the same visual
   role as the route component's strong label. */
.business-needs-routes .needs-route-copy > h3 {
  margin: 0;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--type-card);
  font-weight: 400;
  line-height: var(--leading-card);
  text-wrap: balance;
}

.business-needs-routes .needs-route-copy > em {
  margin: 18px 0 0;
  color: var(--muted);
  font-size: var(--type-body);
  font-style: normal;
  line-height: var(--leading-body);
  text-wrap: balance;
}

.business-needs-routes .needs-route-copy > p {
  margin: 18px 0 0;
  color: var(--muted);
  font-size: var(--type-body);
  font-style: normal;
  line-height: var(--leading-body);
  text-wrap: balance;
}

.business-needs-routes .needs-route-action {
  align-self: flex-start;
  margin-block-start: clamp(18px, 2vw, 26px);
  color: var(--ink);
}

.business-needs-routes .card-chevron {
  --chev-tint: color-mix(in srgb, var(--brand-razeen) 34%, var(--brand-white));
  --chev-line: var(--brand-sweihan);
  position: absolute;
  z-index: 1;
  /* `inset: auto` FIRST - same fix as .project-card-base .card-chevron
     above: clears the blanket `.card-chevron.card-chevron { inset: 0 }`
     reset before this family's own left/bottom apply, or top/right stay
     pinned at 0 underneath them and the sticker stretches across the card
     instead of anchoring a small corner mark. */
  inset: auto;
  /* FLUSH, NOT OVERHUNG - same fix as .needs-route/.project-card-base
     above (see their own comments): this host is also overflow:hidden,
     and the previous -7%/-8% negative offsets pushed the box past the
     corner so the clip sliced off the artwork's own tapered point,
     reading as a shifted/cut mark rather than a clean corner taper.
     Site owner, 29 Aug 2026. */
  left: 0;
  bottom: 0;
  /* HEIGHT-DRIVEN, NOT WIDTH-DRIVEN - same fix as .project-card-base and
     .needs-route above, needed for the same reason: this panel measures
     520x211px live (ratio ~2.5:1). A width-driven box sized taller than
     the panel's own 211px and was silently clipped by overflow:hidden,
     showing only the middle of the diagonal mark as a stray line through
     the card's own text. Site owner, 28 Aug 2026, from a screenshot of
     /en/business/'s "Five connected businesses" cards. */
  block-size: min(80%, 220px);
  inline-size: auto;
  aspect-ratio: 434 / 281;
  opacity: 1;
  pointer-events: none;
  transition: transform var(--dur-slow) var(--ease-out);
}

.business-needs-routes .card-chevron svg { inline-size: 100%; block-size: 100%; overflow: visible; }

/* The homepage mirrors both the chevron and its wipe in LTR. Keep those two
   pieces coupled: moving only the gradient makes the colour arrive from a
   corner with no mark to originate it.
   Back to a plain [dir="ltr"] selector, matching .project-card-base's own
   mirror above: the `html[dir="ltr"]` prefix here was a workaround for a
   phantom blanket rule at line ~458 (`html[dir="ltr"] .card-chevron
   .card-chevron { left: 0; right: 0; }`) that out-specified this plain
   selector and pinned the sticker to the wrong corner - its diagonal
   strokes touching "Explore business" (flagged from a screenshot of
   /en/business/'s "Five connected businesses" cards, 28 Aug 2026). That
   blanket rule is now removed outright (see its site at line ~458) rather
   than patched around a second time, so no family needs an `html` prefix
   to win against it any more. */
[dir="ltr"] .business-needs-routes .card-chevron {
  left: auto;
  right: 0;
}

[dir="ltr"] .business-needs-routes .card-chevron svg { transform: scaleX(-1); }

.business-needs-routes .business-needs-route .needs-route-media img { transform: none; }
.business-needs-routes .business-needs-route .needs-route-copy:is(:hover, :focus-within) .card-chevron,
.business-needs-routes .business-needs-route:focus-visible .needs-route-copy .card-chevron {
  --chev-line: var(--brand-white);
  --chev-tint: rgba(255, 255, 255, .30);
}
.needs-route:hover .needs-route-copy,
.needs-route:hover .needs-route-copy strong,
.needs-route:hover .needs-route-copy em,
.needs-route:hover .needs-route-action {
    color: var(--brand-white);
}
.business-needs-routes .business-needs-route .needs-route-copy:is(:hover, :focus-within),
.business-needs-routes .business-needs-route:focus-visible .needs-route-copy { --copy-active: 1; }
.business-needs-routes .business-needs-route .needs-route-copy:is(:hover, :focus-within) .card-chevron,
.business-needs-routes .business-needs-route:focus-visible .needs-route-copy .card-chevron {
  transform: scale(1.1);
  transform-origin: 0 100%;
}
[dir="ltr"] .business-needs-routes .business-needs-route .needs-route-copy:is(:hover, :focus-within) .card-chevron,
[dir="ltr"] .business-needs-routes .business-needs-route:focus-visible .needs-route-copy .card-chevron {
  transform: scale(1.1);
  transform-origin: 100% 100%;
}
.business-needs-routes .business-needs-route .needs-route-copy:is(:hover, :focus-within)::after,
.business-needs-routes .business-needs-route:focus-visible .needs-route-copy::after { --wipe-p: 100%; }
.business-needs-routes .needs-route-copy > :not(.card-chevron) { transition: color var(--dur-slow) var(--ease-out); }
.business-needs-routes .business-needs-route .needs-route-copy:is(:hover, :focus-within) > :not(.card-chevron),
.business-needs-routes .business-needs-route:focus-visible .needs-route-copy > :not(.card-chevron) { color: var(--brand-white); }
.business-needs-routes .business-needs-route .needs-route-copy:is(:hover, :focus-within) .needs-route-action::after,
.business-needs-routes .business-needs-route:focus-visible .needs-route-copy .needs-route-action::after { transform: scaleX(1); }
.business-needs-routes .business-needs-route .needs-route-copy:is(:hover, :focus-within) .needs-route-action > i.ph,
.business-needs-routes .business-needs-route:focus-visible .needs-route-copy .needs-route-action > i.ph { transform: rotate(0deg); }
.business-needs-routes .business-needs-route:focus-visible { outline: 2px solid var(--brand-black); outline-offset: 4px; }

@media (max-width: 700px) {
  .business-needs-routes .business-needs-route { display: flex; flex-direction: column; min-block-size: 0; }
  .business-needs-routes .needs-route-media { inline-size: 100%; aspect-ratio: 16 / 9; }
  .business-needs-routes .needs-route-copy { inline-size: calc(100% - 36px); margin: -42px auto 0; }
}

@media (prefers-reduced-motion: reduce) {
  a.org-company::before { transition: none; }
  .needs-route .needs-route-copy::after { transition: none; }
  .business-needs-routes :is(.needs-route-media img, .card-chevron, .needs-route-copy),
  .business-needs-routes .needs-route-copy::after { transition: none; }
}


/* =============================================================================
   13. REDUCED MOTION
   -----------------------------------------------------------------------------
   Per-selector, not a blanket * reset: a blanket rule also kills the transitions
   that carry meaning (a focus ring appearing, a menu opening) and it silently
   covers future selectors that were never considered. Add a line here whenever
   a transition is added above.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .org-company,
  .row-item,
  .row-item::before,
  .row-item .section-glass,
  .faq-item,
  i.row-item-action {
    transition: none;
  }

  /* Snap rather than travel. The stop still resolves to 100%, so the row is
     fully filled either way - only the journey is dropped. */
  .row-item:is(:hover, :focus-within)::before { --wipe-p: 100%; }
  .cta-action::before,
  .cta-arrow { transition: none; }
  .cta-action:is(:hover, :focus-visible)::before { transform: none; }
  .cta-action:is(:hover, :focus-visible) .cta-arrow { transform: none; }
  [dir="rtl"] .cta-action:is(:hover, :focus-visible) .cta-arrow { transform: none; }

  .row-item:is(:hover, :focus-within) i.row-item-action { transform: none; }
  .gallery-item img,
  .faq-item > summary i.ph,
  .lightbox-btn,
  .map-pin { transition: none; }
  .gallery-item:is(:hover, :focus-visible) img { transform: none; }
  [dir="rtl"] .row-item:is(:hover, :focus-within) i.row-item-action { transform: scaleX(-1); }

  :is(.icon-item--link, .icon-grid--interactive .icon-item),
  :is(.icon-item--link, .icon-grid--interactive .icon-item)::before,
  :is(.icon-item--link, .icon-grid--interactive .icon-item)
    :is(.icon-item-mark, h3, p, .icon-item-action),
  .icon-item-band { transition: none; }
  :is(.icon-item--link:is(:hover, :focus-visible), .icon-grid--interactive .icon-item:hover)::before {
    --wipe-p: 100%;
  }
}

/* ==========================================================================
   THE CARD SCROLLER - the client's "press releases on a carousel"

   It is the gallery's scroller with the canonical card inside it instead of a
   photograph tile, so .gallery-scroll carries the snap, the styled scrollbar,
   the focus ring and the drag-to-scroll behaviour already bound to it in
   sdeira-sections.js. Only two things differ from a gallery, and both are here:
   the row lays out cards rather than fixed-ratio tiles, and each card gets a
   readable width instead of an aspect ratio.

   Nothing auto-advances. A press-release strip that slides on a timer takes the
   reader's place in the list away from them.
   ========================================================================== */
.card-scroller {
  display: flex;
  gap: clamp(16px, 2vw, 28px);
  padding-inline: var(--page-gutter);
  /* Content width, not 100% - the same reason .gallery needs it: a flex row
     otherwise shrinks to its container and the cards compress instead of
     overflowing into the scroller. */
  width: max-content;
}

/* The grid rules do not apply to a flex row; the card needs its own width. */
.card-scroller .news-item {
  flex: 0 0 auto;
  inline-size: min(88vw, 380px);
  scroll-snap-align: start;
}

/* The scroller owns the horizontal padding, so the card must not also be
   inset by the wrap it is no longer inside. */
.card-scroller-scroll {
  padding-block: 4px;
}

/* ==========================================================================
   SHARED FOOTER - one geometry for homepage and generated sub-pages
   ========================================================================== */
@media (min-width: 981px) {
  footer .foot-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    align-items: start;
  }

  footer .foot-brand { grid-row: span 2; }
}

@media (max-width: 980px) {
  footer .foot-brand { grid-row: auto; }
}

.foot-brand .foot-socials { margin-block-start: 20px; }

.foot-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 16px;
}

/* The Legal group heading, added 13 Aug 2026 per the site-alignment
   instructions: the three links already existed, but nothing labelled them as
   a group. Stacks above .foot-legal rather than joining .foot-col - the
   bottom bar's copyright/legal row is a distinct, lighter-weight strip, not a
   fourth sitemap column, so it borrows the column heading's type role
   (--type-caption, matching the legal links themselves) rather than the
   column heading's own (--type-label). */
.foot-legal-group {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.foot-legal-heading {
  font-family: var(--font-display);
  /* color: var(--brand-razeen); */
  font-size: var(--type-caption);
  font-weight: 600;
}

.foot-legal {
  display: flex;
  flex-wrap: wrap;
  gap: 18px 24px;
  align-items: center;
}

.foot-legal a {
  color: var(--muted-on-dark);
  font-size: var(--type-caption);
  font-weight: 400;
  transition: color var(--dur-fast) var(--ease-out);
}

.foot-legal a:is(:hover, :focus-visible) { color: var(--link-hover-on-dark); }

/* Shared footer surfaces, type roles and control states. This final block is
   loaded after both homepage and sub-page sheets so neither family can fork the
   component with a local size or fill. */
footer {
  background: var(--scrim-footer);
  color: color-mix(in srgb, var(--brand-white) 96%, transparent);
  font-weight: 600;
  padding-block: 80px max(36px, calc(96px + env(safe-area-inset-bottom)));
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

footer > .wrap { position: relative; z-index: 1; }

.foot-brand .logo {
  display: block;
  inline-size: min(220px, 100%);
  max-inline-size: 100%;
  aspect-ratio: 360 / 92;
  margin-bottom: 20px;
}

.foot-brand .logo img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  max-inline-size: 100%;
  object-fit: contain;
  /* The lockup's real aspect ratio rarely matches this box's fixed 360/92
     (Khadamat's own logo is 1200/446 = 2.69, far off it), so `contain`
     always letterboxes - `object-position` decides which edge the mark
     sits flush against inside that empty space. It must track the reading
     direction, not stay fixed to "right": on an RTL page the mark reads
     right-to-left same as "Khadamat"/the paragraph below it, so flush-right
     is correct there, but the same fixed value on an LTR page pushes the
     mark away from the LEFT edge the English paragraph starts on instead -
     caught on the Khadamat English footer, where the gap was visible
     (~90px) against the "Your step-by-step..." line beneath it. */
  object-position: right center;
}

[dir="ltr"] .foot-brand .logo img {
  object-position: left center;
}

.foot-brand p {
  font-weight: 400;
  font-size: var(--type-body);
  line-height: var(--leading-body);
  max-inline-size: 300px;
  text-wrap: balance;
}

.foot-col :is(h3, h4) {
  font-family: var(--font-display);
  color: var(--brand-razeen);
  font-size: var(--type-label);
  line-height: var(--leading-label);
  margin-bottom: 22px;
  font-weight: 700;
}

.foot-col a,
.foot-col .foot-pending {
  display: block;
  margin-bottom: 13px;
  font-size: var(--type-action);
  font-weight: 400;
}

.foot-col a { color: var(--brand-white); }

.foot-socials a {
  border: 0;
  border-radius: var(--r);
  color: var(--icon-on-dark);
  background: color-mix(in srgb, var(--brand-white) 8%, transparent);
}

@media (max-width: 560px) {
  footer .foot-grid { grid-template-columns: 1fr; }
}

@media (max-width: 560px) {
  .foot-bottom {
    justify-content: flex-start;
  }
}

/* Subsidiary cards are the shared Companies / Subsidiaries family used by the
   homepage and business overview. The diagonal fill is the same registered
   wipe and corner mark as the homepage card, not a page-local hover effect. */
.company-grid--content {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(16px, 2vw, 24px);
}

.company-grid--content .company-card {
  position: relative;
  isolation: isolate;
  min-width: 0;
  min-height: 310px;
  padding: clamp(24px, 2.5vw, 36px) clamp(32px, 3.4vw, 46px);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  overflow: hidden;
  background: var(--surface-raised);
}

/* THE SECTION'S OWN GROUND WAS REMOVED (plain White now, site owner 31 Aug
   2026: no colour behind the whole "Four ways an owner can work with Eskan"
   section), so --surface-raised on the card would disappear again the same
   way it did against the retired .pad--wedge tint. Card and page canvas are
   the same White token by name (--surface-raised === --surface-base's
   value), so a raised card on a plain page floor still needs its own step to
   read as a surface. .company-grid--content has no other caller on the
   site, so this is unconditional rather than ground-scoped. */
.company-grid--content .company-card { background: var(--surface-sunken); }

.company-grid--content .company-card::after {
  content: "";
  position: absolute;
  z-index: 0;
  inset: 0;
  --wipe-angle: var(--route-card-wipe-angle);
  --wipe-p: 0%;
  --wipe-op: 72%;
  --wipe-fill: var(--surface-brand-deep);
  background-image: linear-gradient(
    var(--wipe-angle),
    color-mix(in srgb, var(--wipe-fill) var(--wipe-op), transparent) 0 var(--wipe-p),
    transparent var(--wipe-p) 100%);
  transition: --wipe-p var(--dur-slow) var(--ease-out),
    --wipe-op var(--dur-slow) var(--ease-out);
  pointer-events: none;
}

.company-grid--content .company-card::before {
  display: none;
}

.company-grid--content .company-card > :not(.card-chevron, .company-num) {
  position: relative;
  z-index: 2;
}

.company-grid--content .company-card .company-num {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.company-grid--content .company-logo {
  position: relative;
  z-index: 1;
  inline-size: 100%;
  min-block-size: 78px;
  margin-block-end: 24px;
  display: flex;
  align-items: flex-start;
  gap: 14px;
  align-self: flex-end;
  color: var(--company-accent);
}

.company-grid--content .company-logo svg {
  inline-size: 54px;
  block-size: 48px;
  flex: 0 0 auto;
}

.company-grid--content .company-logo--eskan {
  align-items: center;
  color: var(--brand-black);
}

.company-grid--content .company-logo--eskan-wordmark {
  display: grid;
  gap: 0;
  color: var(--brand-black);
  font-family: var(--font-display);
  font-weight: 600;
  line-height: .85;
}

.company-grid--content .company-logo--eskan-wordmark b {
  font-size: var(--type-feature);
  font-weight: 600;
}

.company-grid--content .company-logo--eskan-wordmark small,
.company-grid--content .company-card h3 small {
  display: block;
  margin-block-start: 7px;
  color: var(--brand-jais);
  font-size: var(--type-action);
  font-weight: 400;
  line-height: 1;
}

.company-grid--content .company-logo--image img {
  inline-size: min(190px, 88%);
  block-size: 76px;
  object-fit: contain;
  object-position: left center;
}

.company-grid--content .company-logo--zoad img {
  inline-size: min(154px, 82%);
  block-size: 82px;
}

.company-grid--content .company-card h3 {
  margin: 0;
  min-block-size: calc(2em + 7px);
  color: var(--ink);
  font-size: var(--type-card);
  line-height: var(--leading-card);
}

.company-grid--content .company-card p {
  flex: 1 1 auto;
  min-block-size: 3.4em;
  margin: 0;
  color: var(--muted);
  font-size: var(--type-body);
  line-height: var(--leading-body);
  text-wrap: balance;
}

.company-grid--content .company-card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 18px;
  margin-block-start: 24px;
  color: var(--label);
  font-size: var(--type-meta);
}

.company-grid--content .company-card-meta span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.company-grid--content .company-card-meta i.ph {
  color: var(--icon);
  font-size: var(--ic-sm);
}

.company-grid--content .company-card .action-link {
  margin-block-start: 18px;
  color: var(--ink);
  font-weight: 700;
}

.company-grid--content .company-card .card-chevron {
  position: absolute;
  z-index: 1;
  /* `inset: auto` FIRST - same fix as .project-card-base/.business-needs
     -routes above: clears the blanket `.card-chevron.card-chevron { inset:
     0 }` reset before this family's own edges apply, or the two unset
     sides stay pinned at 0 and the sticker stretches across the card. */
  inset: auto;
  inset-inline-start: -7%;
  inset-block-end: -8%;
  inline-size: min(40%, 220px);
  aspect-ratio: 434 / 440;
  opacity: 1;
  transition: transform var(--dur-base) var(--ease-out);
}


html[dir="rtl"] .company-card .card-chevron.card-chevron {
    inset: auto auto 0 0;
}

.company-grid--content .company-card .card-chevron svg {
  inline-size: 100%;
  block-size: 100%;
  overflow: visible;
}

.company-grid--content .company-card:is(:hover, :focus-within)::after { --wipe-p: 100%; }
.company-grid--content .company-card:is(:hover, :focus-within)::after { --wipe-op: 100%; }
.company-grid--content .company-card > :not(.action-link):not(.card-chevron):not(.company-logo) {
  transition: color var(--dur-slow) var(--ease-out);
}
.company-grid--content .company-card:is(:hover, :focus-within)
  > :not(.action-link):not(.card-chevron):not(.company-logo) { color: var(--brand-white); }
.company-grid--content .company-card:is(:hover, :focus-within) .company-card-meta i.ph { color: var(--icon-on-dark); }
.company-grid--content .company-card .company-logo img {
  transition: filter var(--dur-slow) var(--ease-out);
}
.company-grid--content .company-card:is(:hover, :focus-within) .company-logo img {
  filter: brightness(0) invert(1);
}
/* The full Zoad lockup has an opaque emblem; filtering destroys its lettering.
   The 25 Sept change request replaces the reconstructed white wordmark with the
   identity's White Full Colour mark (zoad-logo-hover.webp) on hover. */

.company-nav-link:is(:hover, :focus-visible, .active) .company-nav-mark.company-nav-mark--zoad img {
  content: url("../images/companies/zoad-logo-hover.svg");
    filter: none;
  mix-blend-mode: normal;
}

.company-card:is(:hover, :focus-within) .company-logo.company-logo--zoad img{
  content: url("../images/companies/zoad-logo-dual-white.svg");
    filter: none;
  mix-blend-mode: normal;
}
.company-grid--content .company-card .company-logo--eskan svg {
  transition: filter var(--dur-slow) var(--ease-out);
}
.company-grid--content .company-card:is(:hover, :focus-within) .company-logo--eskan svg {
  filter: brightness(0) invert(1);
}
.company-grid--content .company-card:is(:hover, :focus-within) .company-logo--eskan-wordmark,
.company-grid--content .company-card:is(:hover, :focus-within) .company-logo--eskan-wordmark small,
.company-grid--content .company-card:is(:hover, :focus-within) > h3 small { color: var(--brand-white); }
.company-grid--content .company-card:is(:hover, :focus-within) .card-chevron {
  --chev-line: var(--brand-white);
  --chev-tint: rgba(255, 255, 255, .30);
}
/* THE ZOAD EXCEPTION IS GONE HERE TOO, 28 Aug 2026. Same cause as the two
   homepage sheets and the org diagram: the supplied artwork carried its own
   ground, so the shared brightness(0) invert(1) would have whitened the
   whole canvas into a rectangle and a second white file was cross-faded over
   it on hover. The mark is the wordmark now, navy ink on transparent, and
   takes the shared treatment. Site owner's rule: a logo sits on white, never
   on a brand colour, and a coloured panel comes off the ASSET. */
.company-grid--content .company-card:is(:hover, :focus-within) .company-logo svg {
  --company-accent: var(--brand-white);
  --company-accent-2: rgba(255, 255, 255, .72);
}
.company-grid--content .company-card:is(:hover, :focus-within) .card-chevron { transform: translate3d(14px, -10px, 0); }
.company-grid--content .company-card:is(:hover, :focus-within) .action-link { color: var(--brand-white); }
.company-grid--content .company-card:focus-within { outline: 3px solid var(--accent-on-dark); outline-offset: -3px; }

[dir="ltr"] .company-grid--content .company-card .card-chevron { inset-inline-start: auto; inset-inline-end: -7%; }
[dir="ltr"] .company-grid--content .company-card:is(:hover, :focus-within) .card-chevron { transform: translate3d(-14px, -10px, 0); }
[dir="ltr"] .company-grid--content .company-card::after { --wipe-angle: 317deg; }
[dir="ltr"] .company-grid--content .company-card .card-chevron svg { transform: scaleX(-1); }
[dir="rtl"] .company-grid--content .company-logo--image img { object-position: right center; }

@media (hover: none), (pointer: coarse) {
  .company-grid--content .company-card .action-link { color: var(--ink); }
}

@media (max-width: 760px) {
  .company-grid--content { grid-template-columns: minmax(0, 1fr); gap: clamp(14px, 4vw, 24px); }
}

@media (prefers-reduced-motion: reduce) {
  .company-grid--content .company-card::after,
  .company-grid--content .company-card .card-chevron { transition: none; }
  .company-grid--content .company-card > :not(.action-link):not(.card-chevron):not(.company-logo),
  .company-grid--content .company-card .company-logo img,
  .company-grid--content .company-card .company-logo--eskan svg { transition: none; }
}

/* The group model is a hierarchy, not a numbered sequence. It reuses the
   org-diagram connectors and surfaces; the record's two-column grid is carried
   by the existing --grid-cols contract. */
.org-diagram--model .org-companies {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}

@media (max-width: 760px) {
  .org-diagram--model .org-tier {
    grid-template-columns: minmax(0, 1fr);
  }

  .org-diagram--model .org-tier::before {
    content: "";
    inset-inline-start: 50%;
    inset-inline-end: auto;
    inline-size: 2px;
    block-size: 100%;
  }
}

/* =============================================================================
   OPTIONAL PER-ITEM BACKGROUND CONTROLS (shared blocks)
   -----------------------------------------------------------------------------
   Stat Strip, Icon Grid, Step List and Logo Wall each expose an optional
   "item background colour" control. The control writes a CSS custom property
   onto the component's grid container inline; these scoped rules hand it to
   the individual items with !important so it wins over the shared surface
   rules (.pad--raised/.pad--sunken/.pad--inverse) exactly when the control is
   set. When the control is unset the attribute selector does not match and
   the reference defaults (below) apply unchanged.
   ========================================================================== */
.stat-strip[style*="--stat-bg"] .stat { background: var(--stat-bg) !important; }
.icon-grid[style*="--icon-item-bg"] .icon-item { background: var(--icon-item-bg) !important; }
.step-list[style*="--step-bg"] .step { background: var(--step-bg) !important; }
.logo-wall[style*="--logo-tile-bg"] .logo-tile { background: var(--logo-tile-bg) !important; }

/* ============================================================================
   PER-CARD HOVER STYLE + OPTIONAL CUSTOM PATTERN
   ----------------------------------------------------------------------------
   companies-v2 / needs-v2 / card-grid cards may each set their own hover
   fill (default | eskan | khadamat | zoad | your-laundry) and an optional
   pattern image.

   Hover: the card carries .sdeira-card-hover--{style}, and the rules below
   re-declare --wipe-fill on that card's wipe pseudo-element. "default" adds
   no class, so the card keeps its original red hover (--surface-brand-deep
   on the needs/company cards, --accent-deep on the project cards) unchanged.
   The wipe animation itself is untouched.

   Pattern: a card with an uploaded pattern sets an inline
   --card-pattern-image. The rules below swap ONLY the background-image of
   that card's existing decorative pattern layers (the copy/card chevron mark,
   the media photo-marks and the company watermark) - position, size, repeat,
   opacity, clipping/masking and layout are all untouched, and no new
   full-card layer is created. The [style*="--card-pattern-image"] attribute
   selector matches only cards that carry the variable, so default cards are
   pixel-identical and the default
   pattern URL is never stored per card.

   Placed last so these overrides win over the base wipe/pattern rules. */
.sdeira-card-hover--eskan::after,
.sdeira-card-hover--eskan .needs-route-copy::after,
.sdeira-card-hover--eskan .project-card-copy::after { --wipe-fill: var(--brand-eskan-teal-deep); }
.sdeira-card-hover--khadamat::after,
.sdeira-card-hover--khadamat .needs-route-copy::after,
.sdeira-card-hover--khadamat .project-card-copy::after { --wipe-fill: var(--brand-khadamat-green-deep); }
.sdeira-card-hover--zoad::after,
.sdeira-card-hover--zoad .needs-route-copy::after,
.sdeira-card-hover--zoad .project-card-copy::after { --wipe-fill: var(--brand-zoad-blueberry); }
.sdeira-card-hover--your-laundry::after,
.sdeira-card-hover--your-laundry .needs-route-copy::after,
.sdeira-card-hover--your-laundry .project-card-copy::after { --wipe-fill: var(--brand-laundry-deep-tide); }

/* Swap only the pattern artwork on the card's existing decorative layers.
   One selected pattern replaces ALL of that card's pattern instances:

   - copy/card layer:        .card-chevron::after  (lines.svg, normal)
   - media, normal corner:   .needs-route-media::after (lines.svg)
   - company watermark:      .company-card::before (sdeira-mark.png)
   - media, rotated corner:  .needs-route-media::before, .project-card-media::before,
                             .project-card--feature .project-card-img::before
                             (lines-rotated-180.svg — the custom image is
                             rotated 180deg in place to match that artwork).

   Only background-image (and the rotation for the rotated-180 corners) is
   touched; position, size, repeat, opacity, clipping/masking and layout come
   from the existing rules. The [style*="--card-pattern-image"] selector
   matches only cards carrying the variable, so default cards are
   pixel-identical. */
[style*="--card-pattern-image"].company-card::before,
[style*="--card-pattern-image"] .card-chevron::after,
[style*="--card-pattern-image"] .needs-route-media::after {
  background-image: var(--card-pattern-image);
}

[style*="--card-pattern-image"] .needs-route-media::before,
[style*="--card-pattern-image"] .project-card-media::before,
[style*="--card-pattern-image"] .project-card--feature .project-card-img::before {
  background-image: var(--card-pattern-image);
  transform: rotate(180deg);
  transform-origin: center;
}

/* Keep the rotated artwork rotated through the existing hover grow. */
[style*="--card-pattern-image"] :is(.project-card, .project-card--secondary):is(:hover, :focus-within) .project-card-media::before,
[style*="--card-pattern-image"] .project-card--feature:is(:hover, :focus-within) .project-card-img::before {
  transform: rotate(180deg) scale(1.1);
  transform-origin: center;
}

/* ============================================================================
   ZOAD-SPECIFIC PATTERN TREATMENT
   ----------------------------------------------------------------------------
   A card with .sdeira-card-hover--zoad swaps its decorative pattern layers to
   the Zoad dot-field treatment (reference: the Zoad route cards). ONE mark on
   the photo: the dots at 200% from the photo's own bottom-right corner under a
   radial mask, rotated 180deg so the field reads from the same corner on every
   mark, at .35; the second media corner's mark is dropped. The copy/card
   chevron takes the same artwork at .16. An uploaded custom pattern
   (--card-pattern-image) is used in place of zoad-dots.svg with the SAME
   geometry, mask and rotation. background-image is set directly - never the
   `background` shorthand - so the per-card custom pattern variable is not
   reset. Only Zoad cards are affected; the other styles keep their generic
   behaviour above. */
.sdeira-card-hover--zoad .needs-route-media::before {
  inset: auto 0 0 auto;
  inline-size: 44%;
  aspect-ratio: 480 / 380;
  background-image: var(--card-pattern-image, url("../images/patterns/zoad-dots.svg"));
  background-repeat: no-repeat;
  background-position: 100% 100%;
  background-size: 200% 200%;
  -webkit-mask-image: radial-gradient(ellipse at 0% 0%, #28374A, transparent 78%);
  mask-image: radial-gradient(ellipse at 0% 0%, #28374A, transparent 78%);
  rotate: 180deg;
  transform: none;
  transform-origin: center;
  opacity: .35;
}
html[dir="rtl"] .sdeira-card-hover--zoad .needs-route-media::before {
  inset: auto auto 0 0;
  background-position: 0% 100%;
  -webkit-mask-image: radial-gradient(ellipse at 100% 0%, #28374A, transparent 78%);
  mask-image: radial-gradient(ellipse at 100% 0%, #28374A, transparent 78%);
  transform: none;
  transform-origin: center;
}
.sdeira-card-hover--zoad .needs-route-media::after {
  display: none;
}

.sdeira-card-hover--zoad .card-chevron.card-chevron {
  position: absolute;
  inset: auto 0 0 auto;
  inline-size: 44%;
  block-size: auto;
  aspect-ratio: 480 / 380;
  max-block-size: none;
}
.sdeira-card-hover--zoad .card-chevron::after {
  inset: 0;
  background-image: var(--card-pattern-image, url("../images/patterns/zoad-dots.svg"));
  background-repeat: no-repeat;
  background-position: 100% 100%;
  background-size: 200% 200%;
  -webkit-mask-image: radial-gradient(ellipse at 0% 0%, #28374A, transparent 75%);
  mask-image: radial-gradient(ellipse at 0% 0%, #28374A, transparent 75%);
  rotate: 180deg;
  transform: none;
  transform-origin: center;
  opacity: .16;
}
html[dir="rtl"] .sdeira-card-hover--zoad .card-chevron.card-chevron {
  inset: auto auto 0 0;
}
html[dir="rtl"] .sdeira-card-hover--zoad .card-chevron::after {
  -webkit-mask-image: radial-gradient(ellipse at 100% 0%, #28374A, transparent 75%);
  mask-image: radial-gradient(ellipse at 100% 0%, #28374A, transparent 75%);
  transform: none !important;
  transform-origin: center !important;
}



/* ============================================================================
   YOUR LAUNDRY-SPECIFIC PATTERN TREATMENT
   ----------------------------------------------------------------------------
   A card with .sdeira-card-hover--your-laundry swaps its decorative pattern
   to the Your Laundry treatment (reference: the Laundry route cards): the
   wave runs as ONE full-width 34px band along the card's bottom, repeat-x
   from the physical left, at .16, no mask, no rotation. The media corner
   marks are dropped (the Laundry brand carries no photo mark). An uploaded
   custom pattern (--card-pattern-image) replaces your-laundry-wave.svg with
   the SAME geometry/repeat/opacity. background-image is set directly - never
   the `background` shorthand - so the per-card custom pattern variable is
   not reset. Only Your Laundry cards are affected. */
.sdeira-card-hover--your-laundry .card-chevron.card-chevron {
  position: absolute;
  inset: 0 !important;
  left: 0;
  right: 0;
  inline-size: auto;
  block-size: auto;
  aspect-ratio: auto;
  max-block-size: none;
  /* The band must sit ABOVE the card copy (which is raised to z-index 1 so it
     clears the default corner chevron); otherwise the 34px wave is hidden
     behind the card's action link. This is a decorative overlay at .16, same
     role the Your Laundry card band plays in the reference. */
  z-index: 2;
}
.sdeira-card-hover--your-laundry .card-chevron::after {
  inset: auto 0 0 0;
  block-size: 34px;
  background-image: var(--card-pattern-image, url("../images/patterns/your-laundry-wave.svg"));
  background-repeat: repeat-x;
  background-position: left bottom;
  background-size: auto 100%;
  opacity: .16;
  transform: none;
  transform-origin: center;
  -webkit-mask-image: none;
  mask-image: none;
}
/* The band follows the shared corner-hover behavior like the other company
   cards: the box scales 1.1 about the bottom corner (the shared rule above)
   and the artwork steps to the approved promo treatment - the light wave at
   full opacity, replacing the resting .16 dark wave. The box is the whole
   card (`inset: 0`), so the scale stays anchored to the bottom edge it sits
   on and never drags the band off the card. */
.sdeira-card-hover--your-laundry:is(:hover, :focus-within) .card-chevron::after {
  background-image: var(--laundry-promo-wave);
  opacity: var(--laundry-promo-wave-opacity);
}
html[dir="rtl"] .sdeira-card-hover--your-laundry .card-chevron::after {
  /* The band is not mirrored: its physical base is repeat-x left bottom in
     both directions, so the shared RTL artwork mirror is released. */
  transform: none;
  background-position: left bottom;
}
.sdeira-card-hover--your-laundry .needs-route-media::before,
.sdeira-card-hover--your-laundry .needs-route-media::after {
  display: none;
}

/* Stat figures: isolate the +/- prefix and the unit suffix so a signed number
   (+165000 / -165000) never reorders under RTL. The number itself lives in a
   <bdi> from the renderer; these two spans get the same isolation. */
.stat-figure-prefix,
.stat-figure-suffix {
  unicode-bidi: isolate;
}

/* Row-list block-level row background colour. Scoped to the one block that
   carries --row-bg; empty blocks keep the current design exactly. */
.row-list[style*="--row-bg"] .row-item {
  background: var(--row-bg) !important;
}

/* Header company marks with a dedicated Hover logo: swap the two images on
   hover/focus instead of the shared white-filter inversion. Without a hover
   logo the mark keeps the existing white-filter behaviour. */
.company-nav-mark--swap {
  position: relative;
}
.company-nav-mark--swap .company-nav-mark-img--hover {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  opacity: 0;
}
.company-nav-link:is(:hover, :focus-visible, .active) .company-nav-mark--swap .company-nav-mark-img--default {
  opacity: 0;
}
.company-nav-link:is(:hover, :focus-visible, .active) .company-nav-mark--swap .company-nav-mark-img--hover {
  opacity: 1;
}
/* Bypass the shared white-filter for swap-pair marks (the hover logo is the
   intended hover artwork, so it must not be inverted on top of the swap). */
.company-nav-link:is(:hover, :focus-visible, .active) .company-nav-mark--swap img {
  filter: none !important;
}

/* Row-list per-block background colour. The attribute selector matches only
   the block that carries --row-bg, so the colour applies to that block's rows
   alone; typography, hover, borders and spacing are untouched. Empty => the
   default design. Mirrors the stat-strip/icon-grid scoped-override pattern. */
.row-list[style*="--row-bg"] .row-item {
  background: var(--row-bg) !important;
}
