@charset "UTF-8";
/* ==========================================================================
   BYARQ — البيارق
   Design system + motion. Hand-written, no framework.

   Principles this file enforces:
   · The ground is BRIGHT. Deep blue is punctuation, not the page.
   · Type is large but never heavy. Nothing above weight 500.
   · Negative space is the material. Sections breathe; content is narrow.
   · One geometric lattice, everywhere, barely visible.
   · The ground changes ONCE on the homepage — inside the landmark journey.
   ========================================================================== */

/* ---------- 1. FONTS ---------------------------------------------------- */

/* Latin display — Clash Grotesk (variable 200–700) */
@font-face {
  font-family: 'Clash Grotesk';
  src: url('../fonts/clash-grotesk-variable.woff2') format('woff2-variations'),
       url('../fonts/clash-grotesk-variable.woff2') format('woff2');
  font-weight: 200 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Latin body and UI — Inter (variable). A chosen face, not a fallback: it
   pairs cleanly with Clash Grotesk at small sizes and has the numerals this
   site needs. CLAUDE.md listed it as banned for a while, which made this
   file look like it was breaking a rule; the ban is lifted — see §8. */
@font-face {
  font-family: 'Inter';
  src: url('../fonts/inter-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Arabic display — Almarai Bold. Geometric and contemporary, not on the
   banned list. A single honestly-declared static weight (700): Zain was
   dropped because it was only ever a single static instance mislabelled as
   a 200–900 range, so "bold" headings quietly rendered at ~400 anyway —
   the actual cause of the "not bold enough" look. Arabic body stays
   Readex Pro. */
/* Almarai ships 300/400/700/800 — there is no 500. Headings ask for 500,
   which CSS font-matching resolves down to this 400 face; the 700 is kept
   for anything that genuinely needs bold. */
@font-face {
  font-family: 'Almarai';
  src: url('../fonts/almarai-arabic-regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1,
                 U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41,
                 U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC;
}
@font-face {
  font-family: 'Almarai';
  src: url('../fonts/almarai-arabic-bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1,
                 U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41,
                 U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC;
}
@font-face {
  font-family: 'Readex Pro';
  src: url('../fonts/readexpro-arabic.woff2') format('woff2');
  font-weight: 200 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1,
                 U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41,
                 U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC;
}
@font-face {
  font-family: 'Readex Pro';
  src: url('../fonts/readexpro-latin.woff2') format('woff2');
  font-weight: 200 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
                 U+2215, U+FEFF, U+FFFD;
}

/* ---------- 2. TOKENS --------------------------------------------------- */

:root {
  /* ----------------------------------------------------------------------
     BRAND, September 2026. Five colours, one job each. Everything below
     them is derived from them, and every derived value carries the ratio
     it was measured at. Nothing here is a guess - each number came from
     compositing the real stack and reading the pixel.

       Sandstone      every bright ground, and all light type on dark
       Midnight Teal  the structural dark: hero, dark sections, ink on light
       Earth Brown    the accent
       Oxblood        used once - the register section
       Carbon Black   overlays and scrims only
     ---------------------------------------------------------------------- */
  --sand:    #CFC8C0;   /* Sandstone     — brand                          */
  --teal:    #002426;   /* Midnight Teal — brand                          */
  --clay-ink:#673A1D;   /* Earth Brown   — brand. 5.8:1 on --sand         */
  --wine:    #340008;   /* Oxblood       — brand                          */
  --carbon:  #121212;   /* Carbon Black  — brand                          */

  /* Earth Brown lifted until it holds on a dark ground, exactly as the old
     --zen-ink was Zen blue darkened until it held on a light one. The new
     set has no light accent of its own, and the body tone cannot double as
     one: an accent has to differ from the text beside it. 5.6:1 on --teal,
     and 1.9:1 against --sand, so the two never read as the same colour.  */
  --clay:    #BD8B6B;

  /* Ground — Sandstone carries every bright surface in all three themes.
     It is the constant: what changes between themes is which brand colour
     owns the dark, and which one the register borrows.                   */
  --paper:      var(--sand);
  --paper-2:    #C3BCB5;   /* one step down, for panels on the ground      */
  --beige:      var(--sand);

  --hair-ink:   rgba(207, 200, 192, 0.18);
  --hair:       rgba(var(--ink-rgb), 0.16);
  /* Pill boundaries on the masterplan filter bar. --hair at .16
     measures 1.35:1 on Sandstone; a UI boundary wants 3:1. .52 is
     3.01:1, exactly on the floor, so this sits one step above at
     3.18:1 - the same figure as the warm grey proposed in SS11.5. */
  --mpf-line:   rgba(var(--ink-rgb), 0.54);
  --on-paper-mute:  #375053;  /*  5.2:1 on Sandstone — ground is constant  */

  /* Live channel — JS moves these, and only where the design says so.    */
  --bg:      var(--paper);
  --fg:      var(--on-paper);
  --fg-mute: var(--on-paper-mute);
  --accent:  var(--clay-ink);
  --rule:    var(--hair);

  /* Type ----------------------------------------------------------------
     Large, but never heavy. 500 is the ceiling.                          */
  /* Clash Grotesk carries the headings; Inter carries body and UI text.
     Arabic uses Readex Pro throughout — the Latin pair never sets Arabic. */
  --font-display:    'Clash Grotesk', ui-sans-serif, system-ui, sans-serif;
  --font-latin:      'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-arabic:     'Readex Pro', 'Inter', ui-sans-serif, system-ui, sans-serif;
  --font-display-ar: 'Almarai', 'Readex Pro', ui-sans-serif, system-ui, sans-serif;
  --font: var(--font-arabic);

  --fs-display: clamp(2.5rem,  5.6vw, 5.25rem);
  --fs-h1:      clamp(2rem,    4.2vw, 3.6rem);
  /* ONE section-heading size. Every h2 on the site uses it; the only
     exceptions are the three display moments - the hero's h1, the
     statement and the infrastructure opener - which take --fs-display.
     It was 2.6rem and the sections were landing at 42px, 56px and 81px
     depending on which override happened to reach them. */
  --fs-h2:      clamp(1.8rem,  2.9vw, 2.9rem);
  --fs-h3:      clamp(1.15rem, 1.7vw, 1.5rem);
  --fs-lead:    clamp(1.075rem, 1.4vw, 1.3rem);
  --fs-body:    1.0625rem;
  --fs-sm:      0.9375rem;
  --fs-xs:      0.75rem;
  /* display sizes the wide tiers override; declared here so every
     consumer resolves below 1440 as well. */
  /* control heights, so the header and the buttons grow with the type
     rather than staying at their 1280px proportions on a 2560 screen */
  --ui-h:       42px;
  --ui-h-sm:    34px;
  /* NOTE ON ZOOM. Browser zoom widens the CSS viewport and then draws each
     CSS pixel smaller, so a vw-sized thing keeps its PHYSICAL size at any
     zoom and a px- or rem-sized thing shrinks with it. Everything the eye
     reads as "the UI" is vw-driven from the 1800 tier up for that reason,
     and the ceilings sit far enough out not to bind until about 2900px -
     which is 1920 viewed at 66%. */
  --fs-fig:     clamp(3.6rem, 6.4vw, 7.2rem);
  --fs-step:    clamp(3.4rem, 5.6vw, 6.2rem);
  --fs-lmk:     clamp(20px, 1.8vw, 31px);
  --fs-lmk-d:   clamp(60px, 5.4vw, 93px);

  --lh-display: 1.06;
  --lh-head:    1.16;
  --lh-ar-display: 1.38;
  --lh-ar-head:    1.42;
  --lh-body:    1.75;

  --tr-label: 0.18em;   /* tracked micro-labels                            */

  /* Space — the material. Generous by default.                          */
  --s1: .5rem; --s2: 1rem; --s3: 1.5rem; --s4: 2.25rem;
  --s5: 3.5rem; --s6: 5.5rem; --s7: 8rem; --s8: 12rem;

  /* 151px top AND bottom on every .stage at 1080 - across the homepage
     that is most of a screen spent on nothing. 119px still reads as a
     deliberate break between sections. */
  --sp-section: clamp(5rem, 11vh, 9rem);
  --gutter:     clamp(1.5rem, 6vw, 7rem);
  --measure:    52ch;
  --maxw:       min(1560px, 88vw);

  /* The one diagonal: date-palm leaflet angle. Everything angled uses it. */
  --frond: 33deg;

  /* Apple-style fluid easing: a long, soft tail so nothing snaps. */
  --ease-out:   cubic-bezier(0.32, 0.72, 0, 1);
  --ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
  /* the two curves the fluid button runs on: --ease-lux settles hard at the
     end (the text roll), --ease-flow keeps momentum longer (the fill) */
  --ease-lux:   cubic-bezier(0.16, 1, 0.30, 1);
  --ease-flow:  cubic-bezier(0.18, 0.82, 0.22, 1);
  --t-fast: 280ms;
  --t-med:  620ms;
  --t-slow: 1100ms;

  --z-menu: 1000; --z-header: 900; --z-float: 800; --z-loader: 1200;
  /* The bar grows with its contents. A 62px lockup in an 88px bar leaves
     13px of air; the header tracks the viewport the same way the lockup
     does so the proportion holds from 1280 to 2560. */
  --header-h: clamp(88px, 5.2vw, 108px);

  /* Rounded-panel language: every surface that has its own ground gets
     the same corner. */
  --radius: 5px;   /* every square and rectangle on the site */
}

/* ---------- 3b. THE TWO PALETTES ----------------------------------------
   Two schemes, and what they swap is the HERO - the two surfaces a visitor
   meets before anything else. Everything downstream follows from that one
   choice, so the rest of the site stays put and the decision stays small.

     Teal   Midnight Teal veil  ->  Earth Brown scrim
     Earth  Earth Brown veil    ->  Midnight Teal scrim

   The veil colour also becomes the header bar, so the top of the page and
   the first surface under it are the same colour rather than two darks
   arguing. Headings take whichever of the two the hero did NOT use: a warm
   heading against a cool hero, a cool heading against a warm one, so the
   type never disappears into the palette it sits in.

   Oxblood is the secondary, storytelling colour - the register section's
   ground and the inline policy link, and nowhere else. Sandstone is the
   ground and Carbon the scrims in both.                                   */

/* ONE palette. The switcher is gone and Earth is locked.

   The veil is the brand's own Earth Brown, #673A1D exactly - not the warmed
   #70401F the switcher used. That lift existed to keep the render's light
   readable through a .84 veil; with the exact hex the veil runs a touch
   lower (.80) and the still is desaturated a touch less, which buys the same
   legibility without moving off the brand colour. The header bar takes the
   same hex, so the top of the page and the first surface under it are one
   colour rather than two browns nearly matching. */
:root {
  --hero-a:       #673A1D;                    /* veil: Earth Brown, exact  */
  --hero-a-rgb:   103, 58, 29;
  --hero-b-rgb:   0, 36, 38;                  /* scrim: Midnight Teal      */
  --veil-op:      .80;
  --still-blur:   8px;
  --still-sat:    .26;

  --ink:      #002426;  --ink-rgb: 0, 36, 38;
  --ink-800:  #002426;
  --ink-900:  var(--carbon);
  --ink-600:  #0E4143;  --ink-600-rgb: 14, 65, 67;
  --clay:     #BD8B6B;  --clay-rgb: 189, 139, 107;
  --clay-ink: #673A1D;
  --head:     #002426;                                /* cool on a warm hero */
  --on-paper: #3B2412;                                /* 8.8:1 on --paper  */
  --on-paper-mute: #5C4A3C;                           /* 5.1:1 on --paper  */
  --on-ink:      #CFC8C0;
  --on-ink-mute: #B9AEA2;

  /* the register's ground, and the header's border colour */
  --wine: #121212; --wine-rgb: 18, 18, 18;
  --hc-line: #CDA88E;
}

/* ---------- 2b. ULTRA-WIDE ----------------------------------------------
   Every clamp on this site tops out somewhere between 1340 and 1560px, which
   was fine while nobody opened it wider. On a 2560px monitor the whole page
   became a small layout centred in a lot of empty ground - the complaint was
   "everything looks shrunk", and that is exactly what it is.

   The answer is not to scale the page uniformly. It is to let the type scale
   and the content column keep growing past the point the clamps stop, so the
   page fills the width at a size the eye reads as the same, rather than at
   the same number of pixels. */
/* TWO SEPARATE PROBLEMS, and conflating them is what went wrong the first
   time. A wide screen (1440-1700) needs the page it always had, at the size
   it always was. An ultra-wide screen (2000+) needs the CONTENT COLUMN to
   grow so the page does not sit as an island - it does not need the type to
   grow with it. Pushing the whole scale up from 1700 made a 1920 monitor
   look like the ultra-wide mock-up: everything in your face.

   So the type barely moves and the column does most of the work. */
/* THE TYPE MOVES NOW TOO, and that is a reversal. The tiers above deliberately
   moved --maxw and almost nothing else, because the first attempt at this made
   a 1920 monitor look like the ultra-wide mock-up. What that version missed is
   that a column growing while the type stands still reads as the CONTENT
   shrinking - which is the note that came back from every wide screen: the
   headings, the figures and the landmark cards all looked small.

   So the display sizes climb in three deliberate steps and the body barely
   moves. The earlier mistake was pushing the WHOLE scale from 1700; this
   pushes the three display tokens and the lead, and leaves body/sm/xs where
   the reading measure wants them. */
@media (min-width: 1440px) {
  :root {
    --fs-display: clamp(2.5rem, 6.4vw, 6.4rem);
    --fs-h2:      clamp(1.8rem, 3.5vw, 3.6rem);
    --fs-lead:    clamp(1.075rem, 1.6vw, 1.5rem);
    --fs-h3:      clamp(1.15rem, 1.7vw, 1.7rem);
    --fs-sm:      1rem;
    --fs-xs:      0.8125rem;
    --ui-h:       46px;
    --ui-h-sm:    38px;
    --plate-cap:  34rem;
    --fs-fig:     clamp(3.6rem, 7.6vw, 9rem);
    --fs-step:    clamp(3.4rem, 6.6vw, 8rem);
    --fs-lmk:     clamp(20px, 2.3vw, 39px);
    --fs-lmk-d:   clamp(60px, 6.9vw, 117px);
  }
}
@media (min-width: 1800px) {
  :root {
    --maxw:    min(1760px, 92vw);
    --fs-body: 1.125rem;
    --fs-display: clamp(2.5rem, 6.4vw, 9.4rem);
    --fs-h2:      clamp(1.8rem, 3.5vw, 5.4rem);
    --fs-lead:    clamp(1.075rem, 1.6vw, 2.2rem);
    --fs-h3:      clamp(1.15rem, 1.7vw, 2.4rem);
    /* vw, not rem: see the note at the top of this block. Calibrated so the
       value at 1920 is the one that shipped - 19px, 16px, 56px, 44px - and
       then tracks the viewport instead of standing still. */
    --fs-sm:      clamp(15px, 0.99vw, 28px);
    --fs-xs:      clamp(12px, 0.833vw, 24px);
    --ui-h:       clamp(42px, 2.92vw, 82px);
    --ui-h-sm:    clamp(34px, 2.29vw, 64px);
    --plate-cap:  38rem;
    --header-h:   clamp(88px, 5.2vw, 164px);
    --fs-fig:     clamp(3.6rem, 7.6vw, 13.6rem);
    --fs-step:    clamp(3.4rem, 6.6vw, 11.8rem);
    --fs-lmk:     clamp(20px, 2.3vw, 62px);
    --fs-lmk-d:   clamp(60px, 6.9vw, 186px);
  }
}
@media (min-width: 2200px) {
  :root {
    /* 92vw left 260px of dead margin on each side of a 2560 screen and the
       middle read as "slightly smaller". 95vw closes most of it without
       letting the measure run away. */
    --maxw:    min(2320px, 95vw);
    --fs-body: 1.1875rem;
    --fs-sm:   1.0rem;
    --fs-display: clamp(2.5rem, 6.4vw, 11.5rem);
    --fs-h2:      clamp(1.8rem, 3.5vw, 6.3rem);
    --fs-lead:    clamp(1.075rem, 1.6vw, 2.6rem);
    --fs-h3:      clamp(1.15rem, 1.7vw, 3.1rem);
    --fs-sm:      clamp(15px, 0.99vw, 30px);
    --fs-xs:      clamp(12px, 0.833vw, 26px);
    --ui-h:       clamp(42px, 2.92vw, 88px);
    --ui-h-sm:    clamp(34px, 2.29vw, 70px);
    --plate-cap:  44rem;
    --header-h:   clamp(88px, 5.2vw, 180px);
    --fs-fig:     clamp(3.6rem, 7.6vw, 14.4rem);
    --fs-step:    clamp(3.4rem, 6.6vw, 13.2rem);
    --fs-lmk:     clamp(20px, 2.3vw, 68px);
    --fs-lmk-d:   clamp(60px, 6.9vw, 204px);
  }
}

/* ---------- 3. RESET / BASE --------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: auto; }

body {
  font-family: var(--font);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  font-weight: 400;
  color: var(--fg);
  background: var(--bg);
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
html[lang="en"] body, html[lang="en"] { --font: var(--font-latin); }

img, svg, video { display: block; max-width: 100%; }
img { height: auto; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
ul, ol { list-style: none; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 2px; }
::selection { background: var(--ink); color: var(--paper); }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.skip-link {
  position: fixed; inset-inline-start: var(--s2); top: -100px; z-index: 2000;
  background: var(--ink); color: var(--paper); padding: .8rem 1.3rem;
  font-size: var(--fs-sm); font-weight: 500; border-radius: 2px;
  transition: top var(--t-fast) var(--ease-out);
}
.skip-link:focus { top: var(--s2); }
/* ---------- 4. GROUND ---------------------------------------------------
   The site-wide geometric field was removed on 23 Sep 2026 - a replacement
   pattern is coming from the client. Nothing here draws a texture now; the
   only patterned surface left is the footer's frond shadow, which is a
   corner accent rather than a field. */

/* The scroll spine - an SVG frond rachis in the gutter that draws itself as
   you move down the page. The "follow" element, kept to a whisper. Its rules
   were collateral damage when the lattice section was replaced: without
   `position: fixed` it fell into the flow at its intrinsic height and put
   27,538px of empty page above the hero. */
.spine {
  position: fixed; z-index: 0; pointer-events: none;
  inset-block: 0; inset-inline-start: max(0.75rem, calc(var(--gutter) * 0.34));
  width: 26px; color: var(--fg);
  opacity: 0.5;
}
.spine svg { width: 100%; height: 100%; }
.spine path, .spine line { vector-effect: non-scaling-stroke; }
@media (max-width: 900px) { .spine { display: none; } }

/* Everything sits above the ground. */
.hdr, main, .ftr, .menu, .wa, .lb, .loader { position: relative; z-index: 1; }
.hdr { z-index: var(--z-header); }
/* ---------- 4b. THE FLAG FIELD -------------------------------------------
   The client's own triangular tessellation. The supplied file paints the
   ink in #111 and the cut-outs in #fff over a white ground, so used as an
   image it can only ever be black-on-white. It ships instead as an ALPHA
   tile (`flag-tile.svg`): the same polygons wrapped in an SVG <mask>, ink
   opaque and everything else transparent. The colour then comes from this
   layer's own background, so it follows the palette, and the strength comes
   from opacity rather than from the artwork.

   Trimmed at build time from 378 polygons to the 30 that actually touch the
   tile - 2.2 KB, and still seamless (verified by tiling it 3x3).

   Two mask layers composited with `intersect`: the tile, and a gradient
   that decides where the field is allowed to show. That is what keeps it a
   ground rather than a wallpaper - at full strength this pattern is far too
   loud for a page whose brief is restraint. */
.pfield {
  position: absolute; inset: 0; z-index: -1; pointer-events: none;
  display: block; overflow: hidden;
  background: var(--pat-col, var(--ink));
  opacity: var(--pat-op, .05);
  --pat-w: 132px;
  /* The default fade is two-sided. The first version only faded OUT at the
     bottom, so the field began at full strength on the section's top edge -
     a hard horizontal line where one section met the next, which is exactly
     the seam a handmade ground should not have. It has to arrive from
     nothing as well as leave to nothing. */
  /* A vignette, not a band. A flat field tiled edge to edge is wallpaper;
     a field present at the margins and clear where the type and pictures
     sit reads as the surface the page is printed on.

     Horizontal, not radial. A radial sized in percentages of a 2400px-tall
     section produces an ellipse so large that its whole falloff sits off
     screen - it computed correctly and looked like nothing had changed.
     Two linear gradients composited with `intersect` give a vignette whose
     falloff is where the content actually is: clear through the middle 32%
     where the column sits, solid at both margins, and off the section's own
     top and bottom edges so it never starts on a hard line. */
  --pat-fade:
    linear-gradient(to right,
      #000 0%, rgba(0,0,0,.55) 15%, transparent 34%,
      transparent 66%, rgba(0,0,0,.55) 85%, #000 100%),
    linear-gradient(to bottom,
      transparent 0%, #000 14%, #000 86%, transparent 100%);
  -webkit-mask-image: url("../img/pattern/flag-tile.svg"), var(--pat-fade);
  mask-image: url("../img/pattern/flag-tile.svg"), var(--pat-fade);
  -webkit-mask-size: var(--pat-w) calc(var(--pat-w) * 0.866026), 100% 100%, 100% 100%;
  mask-size: var(--pat-w) calc(var(--pat-w) * 0.866026), 100% 100%, 100% 100%;
  -webkit-mask-repeat: repeat, no-repeat, no-repeat;
  mask-repeat: repeat, no-repeat, no-repeat;
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
/* A section that carries one needs a stacking context, or a z-index:-1 child
   escapes behind the page's own ground and disappears. */
.ed-sec, .stage.mp, .figs-sec { isolation: isolate; }

/* ---------- 5. LAYOUT ---------------------------------------------------- */

.shell { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }
.shell--narrow { max-width: 1080px; }

.stage {
  position: relative;
  padding-block: var(--sp-section);
  isolation: isolate;
}
.stage--screen { min-height: 100svh; display: flex; flex-direction: column; justify-content: center; }

.lede { max-width: var(--measure); }
.tight { max-width: 42ch; }

/* Section opener. Every section starts the same way and every heading is
   centred — mixing left- and centre-aligned headings is what made the page
   feel unresolved. */
.opener {
  display: grid; gap: var(--s3); margin-bottom: var(--s6);
  justify-items: center; text-align: center;
}
.opener > * { margin-inline: auto; }
.label {
  display: flex; align-items: center; justify-content: center; gap: .75em;
  font-family: var(--font-latin);
  font-size: var(--fs-xs); font-weight: 500;
  letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--fg-mute);
}
html[lang="ar"] .label {
  font-family: var(--font-arabic);
  letter-spacing: 0; text-transform: none; font-size: calc(var(--fs-xs) + 1px);
}
.label::before {
  content: ''; inline-size: 2.2em; block-size: 1px;
  background: currentColor; opacity: .55; flex: none;
}

/* ---------- 6. TYPE ------------------------------------------------------ */

/* Headings carry the palette. They take whichever of the two hero colours
   the hero did NOT use, so the type never sinks into the scheme it sits in.
   --head falls back to the inherited colour, and every dark context below
   sets it to --on-ink, so this rule is safe on any ground. */
.t-display, .t-h1, .t-h2, .t-h3 { color: var(--head, inherit); }
.landmark-card h3 { color: var(--head, inherit); }
[data-hdr="ink"], .hero, .reg, .hgal--home.hgal--dark, .mp__frame, .lb, .menu__nav {
  --head: var(--on-ink);
}
.nets-sec { --head: var(--on-ink); }

.t-display, .t-h1, .t-h2, .t-h3 {
  font-family: var(--font-display);
  font-weight: 500;
  letter-spacing: 0.005em;
  text-wrap: balance;
}
/* Arabic never takes the Latin display face. Almarai only carries one loaded
   weight — 700 — so that's what every Arabic heading renders at. */
/* Arabic sits on a deeper body: ق ج ع ى ـم all drop well below the
   baseline, and 1.06 - which is right for Clash Grotesk - was slicing them
   off. This is a token override so every heading on the site inherits it
   rather than each one being patched. */
html[lang="ar"] {
  --lh-display: var(--lh-ar-display);
  --lh-head:    var(--lh-ar-head);
}
html[lang="ar"] .t-display, html[lang="ar"] .t-h1,
html[lang="ar"] .t-h2, html[lang="ar"] .t-h3 {
  font-family: var(--font-display-ar); letter-spacing: 0; font-weight: 500;
}

.t-display { font-size: var(--fs-display); line-height: var(--lh-display); }
.t-h1      { font-size: var(--fs-h1);      line-height: var(--lh-head); }
.t-h2      { font-size: var(--fs-h2);      line-height: var(--lh-head); }
.t-h3      { font-size: var(--fs-h3);      line-height: 1.3; font-weight: 500; }

.t-lead { font-size: var(--fs-lead); line-height: 1.62; color: var(--fg); max-width: var(--measure); font-weight: 400; }
.t-body { font-size: var(--fs-body); line-height: var(--lh-body); color: var(--fg-mute); max-width: var(--measure); }
.t-sm   { font-size: var(--fs-sm);   color: var(--fg-mute); }

/* Everything that reads as a heading takes the display face — Clash Grotesk
   in Latin, Almarai in Arabic — and the bold weight each face was chosen
   for. Collected here so the pairing has one home. */
.hero__h1, .menu__link, .landmark-card h3, .net__t, .cinfo__v, .figure__n, .blk h3 {
  font-family: var(--font-display);
  font-weight: 500;
}
html[lang="ar"] .hero__h1, html[lang="ar"] .menu__link, html[lang="ar"] .landmark-card h3,
html[lang="ar"] .net__t, html[lang="ar"] .cinfo__v, html[lang="ar"] .figure__n,
html[lang="ar"] .blk h3 { font-family: var(--font-display-ar); font-weight: 500; }
/* The landmark name carries its whole card now, so it takes the heavier of
   each face: 600 on Clash (variable 200-700) and Almarai's real 700, since
   Almarai has no intermediate weight to snap to. */
.landmark-card h3 { font-weight: 600; }
html[lang="ar"] .landmark-card h3 { font-weight: 700; }

.num {
  font-family: var(--font-display);
  font-weight: 300;
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  letter-spacing: 0.005em;
  direction: ltr; unicode-bidi: isolate;
}

/* ---------- 7. BUTTONS --------------------------------------------------- */

/* The site has no global box-sizing reset, so these were content-box and
   min-height was landing on the CONTENT box - the padding and border went on
   top of it. Every button was ~35px taller than the number next to it said:
   the hero CTA measured 68px against a stated 44, the masterplan CTA 74
   against 46. Scoped to the buttons rather than reset globally, because the
   rest of the layout was tuned around content-box and would all shift. */
.btn {
  --bfg: var(--fg); --bbg: transparent; --bhov: var(--paper); --bwipe: var(--ink);
  position: relative; display: inline-flex; align-items: center; gap: .85em;
  padding: 1.05rem 2rem; min-height: calc(var(--ui-h) + 10px);
  font-size: var(--fs-sm); font-weight: 500; letter-spacing: .02em;
  color: var(--bfg); background: var(--bbg);
  border: 1px solid currentColor; border-radius: 4px;
  overflow: hidden; isolation: isolate;
  transition: border-color var(--t-med) var(--ease-out);
}
.btn__i { inline-size: 1.05em; block-size: 1.05em; flex: none;
  transition: transform var(--t-med) var(--ease-out), color 900ms var(--ease-lux); }
[dir="rtl"] .btn__i { transform: scaleX(-1); }
.btn:hover .btn__i { transform: translateX(.28em); }
[dir="rtl"] .btn:hover .btn__i { transform: scaleX(-1) translateX(.28em); }

.btn--solid { --bfg: var(--paper); --bbg: var(--ink); --bhov: var(--ink); --bwipe: var(--paper); border-color: var(--ink); }
.btn--onink { --bfg: var(--on-ink); --bhov: var(--ink); --bwipe: var(--paper); }
.btn--sm { padding: .68rem 1.25rem; min-height: var(--ui-h); font-size: var(--fs-xs); letter-spacing: .06em; }

/* ---- the tapered rule --------------------------------------------------
   A divider that stops dead against nothing looks arbitrary; one that runs
   out at its ends belongs to the same family as the chamfer. The honest
   caveat: a literal 36deg cut across a 1px line spans 1.4px and is
   invisible, so the taper is expressed as a run-out whose LENGTH is the
   chamfer's own run (--cut-x). Same number, visible result.

   Applied with a class rather than by retrofitting every border, because a
   border cannot be masked on one side without masking the box's content. */
.rule-t { position: relative; border-top: 0 !important; }
.rule-t::before {
  content: ''; position: absolute; inset-inline: 0; top: 0; height: 1px;
  background: var(--rule); pointer-events: none;
  -webkit-mask: linear-gradient(to right, transparent 0,
    #000 var(--cut-x), #000 calc(100% - var(--cut-x)), transparent 100%);
  mask: linear-gradient(to right, transparent 0,
    #000 var(--cut-x), #000 calc(100% - var(--cut-x)), transparent 100%);
}
.rule-t.rule-t--last::after {
  content: ''; position: absolute; inset-inline: 0; bottom: 0; height: 1px;
  background: var(--rule); pointer-events: none;
  -webkit-mask: linear-gradient(to right, transparent 0,
    #000 var(--cut-x), #000 calc(100% - var(--cut-x)), transparent 100%);
  mask: linear-gradient(to right, transparent 0,
    #000 var(--cut-x), #000 calc(100% - var(--cut-x)), transparent 100%);
}

/* ---- the chamfer -------------------------------------------------------
   run 18px across, 25px down = 54.25deg from horizontal, which is the
   mark's own angle read off the other axis (the ring steps by 36deg and the
   staff leans at 35.7deg; 90 - 36 = 54). The diagonal measures 30.8px.

   clip-path cuts the border off with the corner, so the outline is drawn on
   ::after instead: a real 1px border supplies the three straight sides and
   is itself cut at the chamfer by the clip, and one linear-gradient draws
   the diagonal. The gradient box is exactly --cut-x by --cut-y, so its 50%
   stop passes through the box centre - which is the midpoint of the cut -
   and 125.75deg is the perpendicular to that cut, pointing inward. The band
   is pushed .2px past centre so the clip does not shave half of it off. */
:root {
  /* The header CTA's proportion, adopted everywhere. 11 across by 15.2 down
     is 54.1deg - the same angle as before, a shorter cut. 18/25 read as too
     long on a 52px button.

     The derived four live here too, NOT on .btn--cut. The shared ::after
     rule is also used by .hdr__cta, which is not a .btn--cut - when these
     sat on .btn--cut the header's `border: var(--bw) solid` resolved to
     nothing and the outline disappeared entirely. A token consumed by a
     shared rule has to be defined where every consumer can see it. */
  --cut-x: clamp(9px, 0.573vw, 17px); --cut-y: clamp(12.4px, 0.7917vw, 23.6px);
  --bw: clamp(1.6px, 0.104vw, 3px);
  --cut-ox: calc(var(--cut-x) * 1.45);
  --cut-oy: calc(var(--cut-y) * 1.45);
  --cut-px: calc(var(--cut-x) * -0.225);
  --cut-py: calc(var(--cut-y) * -0.225);
}

/* EVERY button, not just the ones marked .btn--cut. The chamfer was opt-in
   and five elements had opted in; the client's call is that the header
   button's geometry is the site's button geometry, so .btn and .hctl carry
   it and the modifier is now only a compatibility shim.

   The circles are deliberately excluded - the zoom controls, the lightbox
   arrows and the WhatsApp float are round because they are round, and SS8b's
   tier table says so. */
.btn, .hctl, .hgal__all, .btn--cut {
  --bline: currentColor;
  /* The gradient box is drawn 1.45x oversized about the cut's own midpoint.
     At exact size the band's ENDS fall outside the box and are never
     painted: a 2px band leaving the corner (18,0) perpendicular needs x up
     to 20.46 to reach y = 0, and the box stops at 18. That missing 2.46px
     at each end is the notch where the diagonal failed to meet the straight
     edges. Oversized, the band overshoots both corners and the button's own
     clip-path trims it - the overshoot lands exactly where the borders are,
     in the same colour, so the joint closes. The centre must not move:
     -0.225 x size positions a 1.45x box back over the same midpoint. */
  --cut-ox: calc(var(--cut-x) * 1.45);
  --cut-oy: calc(var(--cut-y) * 1.45);
  --cut-px: calc(var(--cut-x) * -0.225);
  --cut-py: calc(var(--cut-y) * -0.225);
  /* border-WIDTH zero, not just a transparent colour. An absolutely
     positioned child's containing block is its parent's PADDING box, so
     `inset: 0` on the ::after was landing one border-width inside the
     button - while the clip-path is measured on the BORDER box. The
     outline and the cut were drawn against two different rectangles, which
     is the offset that survived the last two fixes. The ::after carries
     the whole outline now, so the button needs no border of its own. */
  border-width: 0 !important;
  clip-path: polygon(var(--cut-x) 0, 100% 0, 100% 100%, 0 100%, 0 var(--cut-y));
}
/* Longhands, never the `background` shorthand. The shorthand resets
   background-origin, so a longhand set before it is silently discarded -
   which is how the whole gradient box ended up one border-width out of
   place the first time. */
.btn::after, .hctl::after, .hgal__all::after, .btn--cut::after, .hdr__cta::after {
  content: ''; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  /* --bline must be OPAQUE. The diagonal deliberately overshoots into the
     two borders so the joints close; with a translucent colour that overlap
     doubles the alpha and shows as a bright nick at each end of the cut.
     Transparency is applied once, here, to the finished outline - the
     element composites as one layer, so same-colour overlap is invisible. */
  opacity: var(--bop, 1);
  border: var(--bw) solid var(--bline);
  background-repeat: no-repeat;
  background-origin: border-box;
  background-clip: border-box;
  background-size: var(--cut-ox) var(--cut-oy);
  background-position: var(--cut-px) var(--cut-py);
  background-image: linear-gradient(125.75deg,
    transparent calc(50% - .5px), var(--bline) calc(50% - .5px),
    var(--bline) calc(50% + var(--bw)), transparent calc(50% + var(--bw)));
}
/* EVERY consumer of the cut, not just .btn--cut. This selector list must stay
   in step with the one that sets the LTR clip-path and the one that draws the
   ::after - three lists describing one set. Mirroring only the gradient left
   the band drawn on the end edge with the corner still cut on the start edge,
   so nothing trimmed its overshoot and it showed as a loose tick above the
   control. Visible on the language toggle in Arabic for several rounds. */
[dir="rtl"] .btn, [dir="rtl"] .hctl, [dir="rtl"] .hgal__all, [dir="rtl"] .btn--cut {
  clip-path: polygon(0 0, calc(100% - var(--cut-x)) 0, 100% var(--cut-y), 100% 100%, 0 100%);
}
[dir="rtl"] .btn::after, [dir="rtl"] .hctl::after, [dir="rtl"] .hgal__all::after,
[dir="rtl"] .btn--cut::after, [dir="rtl"] .hdr__cta::after {
  background-position: right var(--cut-px) top var(--cut-py);
  background-image: linear-gradient(234.25deg,
    transparent calc(50% - .5px), var(--bline) calc(50% - .5px),
    var(--bline) calc(50% + var(--bw)), transparent calc(50% + var(--bw)));
}
.btn--solid { --bline: transparent; }
.btn--solid::after { background: none; }
/* The language toggle is 34-50px tall depending on the tier, and the page's
   11/15.2 cut would eat half of it. Its own pair, at the same 54.1deg. */
/* THE TWO HEADER CONTROLS NOW DRAW ONE LINE. Measured side by side, they
   differed in all three things that make a border a border:

     colour   the toggle took `currentColor` - Sandstone, its own text - and
              the CTA took the client's own #CDA88E. Two colours, 9px apart.
     width    the CTA was lifted to 2px last round; the toggle was still on
              the page's clamp, 1.6px at most widths.
     chamfer  6.5/9 against the CTA's 7/9.7. Near enough to look like a
              mistake and not near enough to look deliberate.

   SS8b deliberately made these two UNEQUAL - the toggle is a utility
   control and was given a quieter line on purpose. The client has now asked
   for one treatment, so the line is shared and the DIFFERENCE IS CARRIED BY
   SIZE ALONE: the toggle stays 34px against the CTA's 42, which is what
   SS8b said settles the pair before colour has to.

   Contrast note, since this lowers it: #CDA88E measures 2.34:1 on the
   header glass and 3.89:1 over the hero, against the 3:1 a UI boundary
   wants. Sandstone was brighter. The hex is the client's, reported not
   altered - SS11.5. */
.hctl.lang {
  --cut-x: clamp(7px, 0.42vw, 12.4px);
  --cut-y: clamp(9.7px, 0.58vw, 17.1px);
  --bline: var(--hc-line);
  --bw: 2px;
}

/* ---------- 7b. FLUID BUTTON --------------------------------------------
   One behaviour for every button on the site: two organic shapes flood up
   from the inline-start edge while the label rolls over word by word - the
   outgoing copy leaves upward and blurs, the incoming copy arrives from
   below in the fill colour. The shapes and the masks are injected by the
   Fluid module rather than written into every button by hand, which also
   means they are rebuilt when the language changes and the words change.

   --bwipe is the flood colour and --bhov the colour the label lands in, so
   every existing button variant already carries the right palette.       */

.fbtn { position: relative; overflow: hidden; isolation: isolate; }
/* The rest position is a transform, not an inset. Percentages on `inset`
   resolve against the BUTTON, so on a small square control - the 42px
   language pill - a blob sized 215%/410% of that button had a rotated
   bounding box that still covered two thirds of it at rest, and the pill
   read as a filled circle with no label. Percentages on `translate`
   resolve against the blob itself, so it always parks fully clear of any
   button shape. It is also the cheaper animation: `left`/`bottom` are
   layout properties and were being animated every frame. */
.fbtn__blob {
  position: absolute; display: block; pointer-events: none;
  background: var(--bwipe);
  will-change: transform, border-radius;
  --bx: -14%;
}
[dir="rtl"] .fbtn__blob { --bx: 14%; }
.fbtn__blob--1 {
  inline-size: 215%; block-size: 410%;
  inset-inline-start: -55%; bottom: -150%;
  border-radius: 54% 46% 58% 42% / 46% 60% 40% 54%;
  transform: translate(var(--bx), 122%) rotate(19deg) scale(.88);
  transition: transform 2.55s var(--ease-flow), border-radius 2.6s var(--ease-lux);
  z-index: -2;
}
.fbtn__blob--2 {
  inline-size: 175%; block-size: 260%;
  inset-inline-start: -35%; bottom: -80%;
  background: color-mix(in oklab, var(--bwipe) 55%, transparent);
  border-radius: 43% 57% 64% 36% / 60% 40% 52% 48%;
  transform: translate(var(--bx), 132%) rotate(24deg) scale(.76);
  transition: transform 2.7s var(--ease-flow);
  z-index: -1;
}
.fbtn:hover .fbtn__blob--1, .fbtn:focus-visible .fbtn__blob--1 {
  transform: translate(0%, 0%) rotate(-4deg) scale(1.14);
  border-radius: 60% 40% 49% 51% / 45% 58% 42% 55%;
}
.fbtn:hover .fbtn__blob--2, .fbtn:focus-visible .fbtn__blob--2 {
  transform: translate(0%, 0%) rotate(-9deg) scale(1.21);
}

/* ---- the rolling label ---- */
/* flex: none on both. The language pill is sized from `min-inline-size`
   with the label allowed to push it wider; as shrinkable flex items the
   masks collapsed to zero width instead and the code vanished. */
.fbtn__words { display: inline-flex; align-items: center; gap: .32em; flex: 0 0 auto; }
.fbtn__mask {
  position: relative; display: grid; place-items: center;
  block-size: 1.6em; overflow: hidden; flex: 0 0 auto;
}
.fbtn__copy {
  /* no will-change: every word of every button would hold a layer for the
     whole session to serve a hover that may never come */
  grid-area: 1 / 1; display: block; white-space: nowrap;
}
.fbtn__copy--out {
  color: var(--bfg);
  transform: translate3d(0, 0, 0); opacity: 1; filter: blur(0);
  transition: transform 1.28s var(--ease-lux), opacity .8s ease, filter 1.1s var(--ease-lux);
}
.fbtn__copy--in {
  color: var(--bhov);
  transform: translate3d(0, 135%, 0); opacity: 0; filter: blur(1.8px);
  transition: transform 1.38s var(--ease-lux), opacity .8s ease, filter 1.05s var(--ease-lux);
}
/* the menu links roll too, on the same rails, without the flood */
.fbtn:hover       .fbtn__copy--out, .fbtn:focus-visible       .fbtn__copy--out,
.menu__link:hover .fbtn__copy--out, .menu__link:focus-visible .fbtn__copy--out {
  transform: translate3d(0, -135%, 0); opacity: 0; filter: blur(1.3px);
  transition-delay: calc(var(--i, 0) * 0.085s + 0.01s);
}
.fbtn:hover       .fbtn__copy--in, .fbtn:focus-visible       .fbtn__copy--in,
.menu__link:hover .fbtn__copy--in, .menu__link:focus-visible .fbtn__copy--in {
  transform: translate3d(0, 0, 0); opacity: 1; filter: blur(0);
  transition-delay: calc(var(--i, 0) * 0.085s + 0.01s);
}
/* on the way back every word leaves together - a staggered retreat reads
   as hesitation rather than polish */
.fbtn:not(:hover) .fbtn__copy,
.menu__link:not(:hover) .fbtn__copy { transition-delay: 0s; }

.fbtn:hover .btn__i, .fbtn:focus-visible .btn__i,
.fbtn:hover .hdr__cta-i, .fbtn:focus-visible .hdr__cta-i { color: var(--bhov); }

@media (prefers-reduced-motion: reduce) {
  .fbtn__blob, .fbtn__copy { transition-duration: .01ms !important; transition-delay: 0ms !important; }
}

.link {
  position: relative; display: inline-flex; align-items: center; gap: .5em;
  min-height: 44px; font-size: var(--fs-sm); font-weight: 500;
}
.link::after {
  content: ''; position: absolute; inset-inline-start: 0; bottom: .55em;
  inline-size: 100%; block-size: 1px; background: currentColor; opacity: .45;
  transform: scaleX(0); transform-origin: var(--o, left);
  transition: transform var(--t-med) var(--ease-inout);
}
[dir="rtl"] .link::after { --o: right; }
.link:hover::after, .link:focus-visible::after { transform: scaleX(1); }

/* ---------- 8. HEADER ---------------------------------------------------- */

/* Transparent over the hero, a deep-blue glass bar everywhere else.
   --hc is the content colour, --hc-inv the colour it inverts to on hover. */
.hdr {
  position: fixed; inset-inline: 0; top: 0; height: var(--header-h);
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  padding-inline: clamp(1.25rem, 2.6vw, 2.75rem);
  isolation: isolate;
  --hc: var(--beige);
  --hc-inv: var(--ink);
  color: var(--hc);
  transition: transform 720ms var(--ease-out), color 520ms var(--ease-out);
}
/* The bar itself, as its own layer so it can fade independently of the
   controls. Deep blue at low alpha over a blur: enough separation to hold
   the controls against a bright page without becoming a solid band. */
.hdr::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  /* .55 carried over from the deep blue read 3.24:1 for the bar's own
     label text - Midnight Teal is less luminous than the navy was, but it
     is also far less saturated, so at the same alpha over Sandstone it
     composites to a grey-green rather than a dark glass. .72 measures
     5.46:1 and still reads as translucent. */
  /* the veil's colour, not the generic dark: the bar and the first surface
     under it are then one colour rather than two darks arguing */
  background: rgba(var(--hero-a-rgb), .72);
  /* A fixed bar with backdrop-filter re-samples and re-blurs whatever is
     behind it on every scroll frame, and the cost climbs with the radius.
     16px plus a saturate pass was the single most expensive thing on the
     page while scrolling. 6px with a stronger tint reads the same at a
     glance and costs a fraction. */
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  border-bottom: 1px solid rgba(207, 200, 192, .10);
  opacity: 0;
  transition: opacity var(--t-med) var(--ease-out);
}
.hdr.on-paper { --hc: var(--ink); --hc-inv: var(--paper); }
/* Over anything that is not the hero the glass is on — and once it is, the
   ground under the controls is deep blue, so they stay light regardless of
   what the section behind declares. Must come after .on-paper to win. */
.hdr:not(.is-over-hero)::before { opacity: 1; }
.hdr:not(.is-over-hero) { --hc: var(--beige); --hc-inv: var(--ink); }

/* The bar is permanent: no hide-on-scroll. */

.hdr__start { display: flex; justify-content: flex-start; }
.hdr__end { display: flex; justify-content: flex-end; align-items: center; gap: .55rem; }

/* Before the hero reveal only the language control is present. */
.hdr__logo, .menu-btn, .hdr__cta {
  opacity: 0; visibility: hidden;
  transition: opacity 620ms var(--ease-out), visibility 0s linear 620ms;
}
.hdr.is-in .hdr__logo, .hdr.is-in .menu-btn, .hdr.is-in .hdr__cta {
  opacity: 1; visibility: visible;
  transition: opacity 620ms var(--ease-out), visibility 0s;
}
/* Only the wordmark steps aside, and only over the hero, where the hero
   carries the identity itself - the mark in the corner, the wordmark in the
   middle of the screen. The menu button, the CTA and the language switch
   stay put throughout. This must come AFTER the .is-in rule above: the two
   have the same specificity, so order is what decides. */
.hdr.is-over-hero .hdr__logo { opacity: 0; visibility: hidden; }

/* 1.75x, and it scales past the old fixed 24px so the bar does not look
   empty on a wide screen. The header grows with it - see --header-h. */
.hdr__logo { display: block; position: relative; height: clamp(30px, 2.2vw, 46px); }
@media (max-width: 760px) { .hdr__logo { height: 26px; } }
.hdr__logo svg { height: 100%; width: auto; }
.hdr__logo::after {
  content: ''; position: absolute; inset-inline: -10px; top: 50%;
  transform: translateY(-50%); height: 44px;
}

/* ---- the shared pill shape, for the two bordered controls -------------- */
.hctl {
  position: relative; isolation: isolate; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center; gap: .5em;
  block-size: var(--ui-h); min-inline-size: var(--ui-h); padding-inline: 1.05rem;
  /* DEAD, BOTH OF THEM, and misleading to read: the chamfer rule forces
     `border-width: 0 !important` on every .hctl because the outline lives on
     the ::after, and `border-radius` does nothing under a clip-path. Kept as
     a note rather than a declaration so nobody tunes a border here again and
     wonders why nothing moves. The line is --bline on the ::after. */
  border: 0;
  color: var(--hc);
  font-family: var(--font-latin);
  font-size: var(--fs-xs); font-weight: 500; letter-spacing: .06em;
  transition: color var(--t-med) var(--ease-out), border-color 520ms var(--ease-out);
}
/* the header pills take the same flood as every other button; --bwipe and
   --bhov are what the Fluid rules read, so they are mapped onto the
   header's own two colours */
.hctl { --bwipe: var(--hc); --bhov: var(--hc-inv); --bfg: var(--hc); }
html[lang="ar"] .hctl { font-family: var(--font-arabic); letter-spacing: 0; }

/* ---- the header's two controls are not equals ---------------------------
   They were: same 2px border, same colour, same shape, sitting 9px apart -
   so the eye had to read both to find out which one was the offer. Three
   things separate them now, none of them loud:

   - the CTA takes the chamfer, so it speaks the same language as every
     other conversion on the site. At 42px tall the page's 18/25 cut would
     eat 60% of the height, so it gets its own smaller pair at the same
     angle: 11 across, 15.2 down = 54.1deg.
   - the language button drops to a 1px border at 62% alpha. It is a utility
     control; it needs to be findable, not sold.
   - the gap between them goes from .55rem to 1rem, which is what stops them
     reading as a segmented pair.                                          */
.hdr__end { gap: 1rem; }
/* Smaller as well as quieter. Matching the CTA's height made the two read
   as a pair however the borders were tuned; 34px against 42px settles the
   question by proportion before colour has to. */
.hctl.lang {
  block-size: var(--ui-h-sm); min-inline-size: var(--ui-h-sm); padding-inline: .7rem;
  border-width: 1px;
  border-color: color-mix(in oklab, var(--hc-line) 62%, transparent);
}
.hdr__cta {
  --bline: var(--hc-line);
  /* ITS OWN PAIR, at the same 54.1 degrees. SS8b gave this button a smaller
     cut - because at 42-56px tall the page's 17/23.6 eats most of the height
     and the diagonal stops reading as a detail and starts being the shape.
     It lost that when the cut tokens moved to :root, and has been inheriting
     the page's ever since. tan(54.1) = 1.3816, so the two scale together or
     the family breaks silently. */
  --cut-x: clamp(7px, 0.42vw, 12.4px);
  --cut-y: clamp(9.7px, 0.58vw, 17.1px);
  /* A WHOLE PIXEL CANNOT SPLIT. At 1.6px the border lands on one device
     pixel along some edges and two along others at the same zoom, which is
     why the bottom line read as thinner than the top. */
  --bw: 2px;
  border-width: 0 !important;   /* see .btn--cut - inset:0 is the padding box */
  clip-path: polygon(var(--cut-x) 0, 100% 0, 100% 100%, 0 100%, 0 var(--cut-y));
}
[dir="rtl"] .hdr__cta {
  clip-path: polygon(0 0, calc(100% - var(--cut-x)) 0, 100% var(--cut-y), 100% 100%, 0 100%);
}

/* The CTA now sits before the language switcher and takes the same pill. */
.hdr__cta { font-weight: 500; }
html[lang="ar"] .hdr__cta { font-weight: 500; }
.hdr__cta-i { inline-size: 1em; block-size: 1em; flex: none; transition: transform var(--t-med) var(--ease-out); }
[dir="rtl"] .hdr__cta-i { transform: scaleX(-1); }
.hdr__cta:hover .hdr__cta-i { transform: translateX(.28em); }
[dir="rtl"] .hdr__cta:hover .hdr__cta-i { transform: scaleX(-1) translateX(.28em); }

/* menu trigger — three bars of stepped length, one stroke weight, aligned to
   the inline start so they mirror with the language. On hover every bar runs
   out to the longest and they finish flush. */
.menu-btn {
  display: inline-flex; flex-direction: column;
  align-items: flex-start; justify-content: center;
  gap: 5px; inline-size: 44px; block-size: 44px;
  padding: 0; border: 0; background: none; color: var(--hc);
  transition: color var(--t-med) var(--ease-out);
}
.menu-btn span {
  display: block; block-size: 2px; background: currentColor; border-radius: 1px;
  transition: inline-size 560ms var(--ease-out);
}
.menu-btn span:nth-child(1) { inline-size: 12px; }
.menu-btn span:nth-child(2) { inline-size: 20px; }
.menu-btn span:nth-child(3) { inline-size: 28px; }
.menu-btn:hover span, .menu-btn:focus-visible span { inline-size: 28px; }

/* Quieter and smaller than the CTA beside it — it is shown the whole time,
   not an occasional action. The invisible ::after keeps a 44px target. */
/* Same height as the CTA beside it so the two sit on one line — it stays
   the quieter of the pair through a narrower box and lower opacity, not a
   shorter one. Type size is untouched. */
/* --ui-h-sm, not 42px. This was the one control on the header with a fixed
   box, so it stood still while the tiers lifted everything around it and it
   shrank with the browser zoom while the rest held. */
.lang {
  block-size: var(--ui-h-sm); min-inline-size: var(--ui-h-sm);
  padding-inline: clamp(.72rem, 0.63vw, 1.4rem);
  font-size: calc(var(--fs-xs) - 1px); letter-spacing: .1em; opacity: .8;
}
/* ON ::before, NOT ::after. This is the invisible box that keeps the small
   toggle at a 44px target, and it predates the chamfer by months. Once every
   .hctl took the chamfer, the outline moved on to ::after as well - and this
   rule, being the more specific of the two, won the `inset` and pushed the
   whole outline 3-4px OUTSIDE the control, where .hctl's own
   `overflow: hidden` clipped it away. The toggle rendered as bare text with
   one stray fragment of the diagonal above it.

   It also stopped working as a target: the chamfer rule sets
   `pointer-events: none`, which the expander inherited. On ::before it is
   out of the outline's way and clickable again. */
.lang::before {
  content: ''; position: absolute; inset: -3px -4px; z-index: 1;
}
.lang:hover, .lang:focus-visible { opacity: 1; }

/* Only the language you'd switch TO is rendered — never both at once. */
/* Direct children only. `.lang span` matched every descendant span, and
   the roll builds the label out of spans - so the masks and both text
   copies inherited display:none and the pill rendered empty. */
.lang > span { display: none; }
.lang > span.is-on { display: inline-flex; }

@media (max-width: 940px) { .hdr__cta { display: none; } }
@media (max-width: 760px) {
  .hctl { block-size: 38px; padding-inline: .85rem; }
  .menu-btn { inline-size: 40px; block-size: 40px; gap: 4px; }
  .menu-btn span:nth-child(1) { inline-size: 10px; }
  .menu-btn span:nth-child(2) { inline-size: 17px; }
  .menu-btn span:nth-child(3) { inline-size: 24px; }
  .menu-btn:hover span, .menu-btn:focus-visible span { inline-size: 24px; }
  .lang { block-size: 38px; min-inline-size: 38px; padding-inline: .7rem; }
}

/* ---------- 9. MENU ------------------------------------------------------ */

/* The panel arrives on a curve: the clip rectangle grows downward while its
   bottom corner radius runs 50% -> 0, so the leading edge is a dome that
   flattens as it lands. Interpolating one inset() into another is what makes
   that a single smooth move rather than two stacked animations. */
.menu {
  position: fixed; inset: 0; z-index: var(--z-menu);
  color: var(--ink-800);
  /* minmax(0,1fr), NOT 1fr. A bare `1fr` is `minmax(auto, 1fr)`, so the
     nav could not shrink below its content and pushed the foot off the
     bottom of the screen the moment a fifth link was added - measured at
     1440x860: the foot ran to 902 in an 860 viewport. */
  display: grid; grid-template-rows: var(--header-h) minmax(0, 1fr) auto;
  isolation: isolate;
  clip-path: inset(0 0 100% 0 round 0 0 50% 50%);
  visibility: hidden;
  transition: clip-path 1180ms var(--ease-lux), visibility 0s linear 1180ms;
}
.menu.is-open {
  clip-path: inset(0 0 0 0 round 0 0 0% 0%);
  visibility: visible;
  transition: clip-path 1180ms var(--ease-lux), visibility 0s;
}

/* Ground: the palm plate sat well back, a soft panna wash over it, and the
   page still legible through both — the panel is a veil, not a lid. */
.menu__bg { position: absolute; inset: 0; z-index: -2; display: block; overflow: hidden; }
/* One picture, at full strength, on an opaque ground - the page behind is
   not part of this design and was showing through as hero fragments. */
.menu__bg { background: var(--beige); }
.menu__bg img {
  inline-size: 100%; block-size: 100%; object-fit: cover; opacity: 1;
  transform: scale(1.08);
  transition: transform 2400ms var(--ease-lux);
}
.menu.is-open .menu__bg img { transform: scale(1); }
/* the wash: 80% over the picture, so the palm reads at a fifth strength */
.menu::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: rgb(207 200 192 / 94%);
}

/* The close sits on the SAME edge as the button that opened it - the menu
   button is in .hdr__start, so this is flex-start, and the header's own
   padding rather than the page gutter so the two land on one vertical.
   Both mirror with the language together, so the hand never crosses the
   screen to undo what it just did. */
.menu__top {
  display: flex; align-items: center; justify-content: flex-start;
  padding-inline: clamp(1.25rem, 2.6vw, 2.75rem);
}
.menu__close { inline-size: 44px; block-size: 44px; display: grid; place-items: center; color: var(--ink-800); }
.menu__close svg { inline-size: 18px; block-size: 18px; transition: transform var(--t-med) var(--ease-inout); }
.menu__close:hover svg { transform: rotate(90deg); }

.menu__nav {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center; padding-inline: var(--gutter);
}
.menu__link {
  /* --bfg / --bhov are the two colours the shared roll reads */
  --bfg: var(--ink-800); --bhov: var(--clay-ink);
  display: flex; align-items: center; justify-content: center;
  /* THE TYPE HAS TO KNOW HOW TALL THE WINDOW IS. Sized on width alone, five
     links at 62px with 1.5 leading come to 693px and the nav only has 651 -
     and a short, wide window (a laptop with a tab bar and a bookmarks bar) is
     exactly where that bites. The svh term is solved from the rows: each link
     costs font-size x (1.25 + 2 x .3em padding) = 1.85, five of them have to
     fit `100svh - header - foot`, so the cap is about (100svh - 209px) / 9.25.
     Leading comes down to 1.25, which is what a five-item menu can afford. */
  font-size: clamp(1.45rem, min(5.6vw, calc(9.8svh - 21px)), 3.9rem);
  font-weight: 500;
  line-height: 1.25; color: var(--ink-800);
  padding-block: .3em;
}
.menu__link .txt { position: relative; display: inline-flex; }
.menu__link[aria-current="page"] { --bfg: var(--clay-ink); }
/* THE MASK IS WHAT SETS THE LINK'S HEIGHT, not the line-height - it is the
   tallest thing in the flex line, so dropping the leading to 1.25 changed
   the type and not the row (measured: line-box 78px, link still 139px).
   1.35em clears the 1.25 line box it now has to hold, and five links come to
   605px in a 651px nav where they came to 693.
   It must not go below the line box, or the glyphs are clipped at rest,
   before anything has moved. */
.menu__link .fbtn__mask { block-size: 1.35em; }

.menu__foot {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5);
  justify-content: space-between; align-items: flex-end;
  padding: var(--s4) var(--gutter) var(--s5);
  border-top: 1px solid var(--hair);
  /* --on-paper-mute lands at 4.15:1 on this wash; the foot needs its own
     darker tone to clear 4.5 */
  font-size: var(--fs-sm); color: #375053;
}
.menu__foot a { color: #375053; }
.menu__foot a:hover { color: var(--clay-ink); }

@media (prefers-reduced-motion: reduce) {
  .menu, .menu__bg img { transition-duration: .01ms !important; }
}

/* ---------- 10. LOADER --------------------------------------------------- */

.loader { display: none; }
.js .loader {
  position: fixed; inset: 0; z-index: var(--z-loader);
  background: var(--paper); display: grid; place-items: center; overflow: hidden;
}
.loader__inner { position: relative; display: grid; place-items: center; gap: var(--s3); }
.loader__mark { inline-size: clamp(56px, 9vw, 88px); color: var(--ink); }
/* Each banner sits faint until its share of the load has arrived. Opacity
   only - ten elements each animating a filter would be ten layers, and the
   fade does the work on its own. */
.loader__mark .lmk__b { opacity: .13; transition: opacity 320ms var(--ease-out); }
.loader__mark .lmk__b.is-lit { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .loader__mark .lmk__b { opacity: 1; transition: none; }
}
.loader__pct { font-family: var(--font-latin); font-size: var(--fs-xs); letter-spacing: .22em; color: var(--on-paper-mute); font-variant-numeric: tabular-nums; }
body.is-loading, body.is-locked { overflow: hidden; }

/* ---------- 11. MOTION PRIMITIVES ---------------------------------------- */

.sp-line { display: block; overflow: hidden; padding-block: .08em; margin-block: -.08em; }
.sp-word { display: inline-block; white-space: pre; }
.sp-char { display: inline-block; white-space: pre; }

[data-fx].is-animating, [data-fx].is-animating .sp-word,
[data-fx].is-animating .sp-char { will-change: transform, opacity, filter; }

/* Hidden states are scoped to html.js — no JS, no hiding. */
.js [data-fx="mask"] .sp-word  { transform: translateY(105%); }
.js [data-fx="rise"] .sp-word  { opacity: 0; transform: translateY(.5em); }
.js [data-fx="blur"] .sp-word  { opacity: 0; transform: translateY(.3em); filter: blur(10px); }
.js [data-fx="fade"]  { opacity: 0; transform: translateY(1.5rem); }
.js [data-fx="scale"] { opacity: 0; transform: scale(.965); }
.js [data-fx="wipe"]  { clip-path: inset(0 0 100% 0); }

.fx-draw path, .fx-draw line, .fx-draw circle, .fx-draw polyline {
  stroke-dasharray: var(--len, 1000); stroke-dashoffset: var(--len, 1000);
}

/* ---------- 12. HERO ----------------------------------------------------- */

/* The hero pins while the mark grows into an aperture on the film, then the
   next section travels up over it. The hero itself never moves.          */
/* The hero is a tall scroll track; its inner frame sticks to the viewport
   for the whole track. That gives the choreography room to breathe AND
   leaves the frame in place for the next section to travel up over it.
   No `overflow` here — it would create a scroll container and kill sticky. */
/* The track carries the choreography, then holds the film full-frame. The
   negative margin below lets the next section travel up OVER the still-stuck
   hero for the last viewport of the track — the parallax cover. */
.hero {
  position: relative; height: 360svh;
  margin-bottom: -100svh;
  background: var(--ink); color: var(--on-ink);
  z-index: 0;
}
.hero__sticky { position: sticky; top: 0; height: 100svh; overflow: clip; }

.hero__still, .hero__film { position: absolute; inset: 0; }
.hero__still img, .hero__film video { width: 100%; height: 100%; object-fit: cover; }

/* Deep blue veil over the still — 70%, with the still itself thrown out of
   focus behind it so the mark and headline sit on a clean field. */
/* The FIRST of the hero's two surfaces. It used to be --ink at .70, and at
   that strength the render's own blue night sky read straight through it -
   a colour that is not in the brand set, on the first screen of the site.
   The still is desaturated almost to grey first, so the wash is the only
   thing supplying hue, and .90 of a brand colour then owns the frame
   completely. */
.hero__veil {
  position: absolute; inset: 0; background: var(--hero-a);
  /* .90, not .86: at .86 the Earth palette bottomed out at 4.39:1 for the
     hero type against a worst-case bright patch of the still. .90 puts the
     floor at the brand colour itself in every palette. */
  opacity: var(--veil-op, .90);
}
/* scale() covers the soft edge blur() leaves at the frame. The blur and the
   desaturation are per palette: the teal veil wants the render flattened
   almost to grey so the colour is all its own, the brown one wants a
   softer, less bleached still underneath so the light in the render still
   reads through it. */
.hero__still img {
  filter: blur(var(--still-blur, 5px)) saturate(var(--still-sat, .12)) contrast(1.06);
  transform: scale(1.03);
}

/* The film is revealed through the mark. mask-size keeps the mark's own
   proportions while it grows; a clip-path in bounding-box units would
   stretch it to the viewport. */
.hero__film {
  --ap: 0%;
  opacity: 0;              /* handed over to, once the mark starts growing */
  /* ONE mask layer: the circle inside the wheel.

     The film is revealed through the mark's HOLLOW, not through the banners
     themselves. The banners stay beige and the footage appears in the
     middle of them, opening as the wheel opens - which is what the mark is
     for. Masking the banners as well turned the logo into a stencil and,
     once it was large, into an oval smear; neither read as the mark.

     The hollow's inscribed radius measures 0.2202 of the mark box's width.
     That is rasterised and radially scanned off mark-mask.svg, not
     eyeballed. `closest-side` makes the gradient's 100% equal half the
     box's short side, so that radius is 44.8% of it. Solid to 43%, gone by
     46.5%: the soft edge lands exactly on the banners' inner tips. */
  /* `circle`, and a SQUARE box. Two separate reasons the shape was going
     oval: a radial-gradient with no shape keyword defaults to an ELLIPSE,
     and the box was `var(--ap) auto`, so the mark's 83.03 x 81.69 ratio was
     stretching it. Both are gone: the box is square and the shape is stated,
     so the aperture is a true circle at every size and simply grows off the
     edges of the frame - the short edge first on a wide screen, which is
     what it should do. */
  /* 57%/61% of closest-side, which on a square box is r = 0.285 -> 0.305 of
     the box width. The old 42% came from the hollow's INSCRIBED radius
     (0.2202), and that was wrong to use: a radial scan hits the thin
     pointed slivers first, but the ring only becomes half-solid at 0.305
     and 75% solid at 0.33 - measured. The eye reads the solid ring as the
     inner edge, so a circle at 0.21 left an obvious gap all the way round.
     At 0.285 it overlaps the slivers and meets the ring. */
  -webkit-mask-image: radial-gradient(circle closest-side, #000 0 57%, rgba(0,0,0,0) 61%);
  mask-image: radial-gradient(circle closest-side, #000 0 57%, rgba(0,0,0,0) 61%);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: var(--ap) var(--ap); mask-size: var(--ap) var(--ap);
}
.hero__film.is-full { -webkit-mask-image: none; mask-image: none; }

/* A child of .hero__film, not a sibling of it — so the logo mask clips it
   and the film's own opacity carries it. It therefore only ever tints the
   video: the still and the deep-blue veil underneath are left alone, and
   during the reveal the tint appears inside the mark's aperture along with
   the footage rather than washing the whole hero. */
/* The heavy end sits under the caption, so the angle has to mirror with the
   reading direction rather than just swapping the two colour stops: 120deg
   puts the dark end at the left for English, 240deg — its mirror about the
   vertical axis — puts it at the right for Arabic. */
/* The SECOND surface, and a different brand colour from the first in every
   palette - the hand-off from the still to the film is a change of colour as
   well as a change of image. Heavy under the caption, open at the far
   corner, so the footage still reads. */
.hero__scrim {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(120deg, rgba(var(--hero-b-rgb), .78) 40%, rgba(var(--hero-b-rgb), .38) 91%);
}
[dir="rtl"] .hero__scrim {
  background: linear-gradient(240deg, rgba(var(--hero-b-rgb), .78) 40%, rgba(var(--hero-b-rgb), .38) 91%);
}

.hero__type {
  position: absolute; inset: 0; z-index: 3;
  display: grid; place-items: center; align-content: center;
  gap: 0; text-align: center; padding-inline: var(--gutter);
  /* TOP padding, not bottom. The box is the whole screen but the header
     covers the top of it, so a stack centred in the box sits half a header
     above the centre of the part you can actually see - which is what reads
     as "too much space underneath". Padding the top by the bar's own height
     centres it in the visible area. Padding rather than a transform, so
     every animated transform on these elements stays free. */
  padding-block-start: var(--header-h);
}
@media (max-height: 680px) { .hero__type { padding-block-end: 24px; } }
/* The brand mark holds the physical top-RIGHT corner in BOTH languages, so
   `right`, not `inset-inline-end`: it is a fixed piece of identity rather
   than something that mirrors with the reading direction. It sits clear of
   the header bar above it. It is also the element the Hero timeline flies
   to centre and grows into the aperture, so nothing else may move it. */
.hero__mark {
  position: absolute; z-index: 4;
  top: calc(var(--header-h) + clamp(.25rem, 1.6vh, 1rem) - var(--mark-off));
  /* the header's own padding-inline, so the mark sits on the same vertical
     as the controls above it rather than near them */
  right: calc(clamp(1.25rem, 2.6vw, 2.75rem) - var(--mark-off));
  /* down ~17%. It is a signature, not a second focal point - it was
     competing with the wordmark in the middle of the frame. */
  /* Doubled, and the ceiling lifted so it keeps growing. At 3.5vw capped
     at 48px the mark stopped growing at 1370px wide and looked like a
     sticker on anything larger. 6.4vw: 92px at 1440, 123px at 1920,
     160px at 2560. */
  /* LAID OUT LARGE, DRAWN SMALL. The mark grows to ~30x on scroll, and a
     transform: scale() rasterises the element at its LAYOUT size and
     stretches that texture - so an SVG laid out at 92px and scaled 30x is a
     92px bitmap blown up to 2760px, which is precisely the oversized-PNG
     look the client saw. (It is GPU-dependent, which is why it showed on
     their screen and not on ours.)

     So the element is laid out at --mark-draw and scaled DOWN to --mark-w
     at rest. Downscaling a large texture stays sharp; the growth is then
     mostly still a downscale, and only past ~12x does it start to stretch
     at all - by which point the mark has faded out and left the frame.

     The box is much larger than the visual, so its top/right are pulled back
     by half the difference: the box centre and the visual centre coincide,
     which is what keeps the corner placement and the measured travel to the
     film's centre both correct. */
  /* The resting width rides on font-size, not on a bare custom property.
     An unregistered custom property computes to its token stream, so
     getPropertyValue('--mark-w') hands JS the literal string
     "clamp(58px, 6.4vw, 160px)" and parseFloat returns NaN. font-size is
     inert here (the element has no text) and computes to a real px value
     that both CSS and script can read. */
  font-size: clamp(56px, 4.4vw, 104px);
  --mark-w: 1em;
  --mark-draw: 1100px;
  --mark-off: calc((var(--mark-draw) - var(--mark-w)) / 2);
  inline-size: var(--mark-draw);
  color: var(--beige); opacity: .92;
}
@media (max-width: 760px) { .hero__mark { font-size: 54px; --mark-draw: 620px; } }

/* The wordmark leads the centre stack. Two of them - the English lockup
   carries both scripts - swapped by language rather than redrawn. */
.hero__word { display: none; block-size: auto; color: var(--beige); }
html[lang="ar"] .hero__word--ar,
html[lang="en"] .hero__word--en { display: block; }
/* Down ~33% from where these started. The stack was filling the frame edge
   to edge and reading as shouting; the breathing room is the point. */
/* +6%. The wordmark and the headline were arriving as one block; the
   reading order should be identity -> proposition -> support, so the
   identity leads by a small margin rather than tying. */
/* Twice the old size, and then it STOPS. The first attempt used 35vw with a
   700px ceiling, which at 2560 filled a third of the screen, pushed the
   stack past the top of the frame and broke the vertical centring. The vw
   term is back to something that reaches the ceiling at about 1700 and
   holds - a hero wordmark does not need to keep growing forever, it needs
   to be twice what it was and then leave room for everything under it. */
.hero__word--ar { inline-size: clamp(200px, 24vw, 470px); aspect-ratio: 122.94 / 51.06; }
.hero__word--en { inline-size: clamp(160px, 19vw, 370px); aspect-ratio: 103.27 / 59.70; }

/* The film CTA, now part of the hero stack rather than floating over the
   footage. It takes the site-wide fluid button, so no bespoke hover. */
/* Deliberately NOT the equal of "Register interest": lighter weight, a
   border at well under full strength, and more air around the label. It is
   the second thing to do on this screen, not the first. */
/* A THICKER LINE, on the client's call. This button sits on a photograph
   under a veil, where 1.6px of a translucent sand colour is close to
   invisible against a bright frame. */
.hero__play { --bw: 2.4px; }
.hero__play {
  margin-top: 0;
  padding: .72rem 1.7rem; min-height: 44px;
  font-size: var(--fs-xs); font-weight: 400; letter-spacing: .06em;
  --bline: var(--on-ink); --bop: .42;
}
html[lang="ar"] .hero__play { letter-spacing: 0; }
/* No ring. A circle around a triangle is two shapes doing one job, and the
   ring was also the last round outline left on a button. The triangle grows
   to carry the weight the circle used to. */
.hero__play-mark {
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 18px; block-size: 18px; flex: none;
  margin-inline-start: -.35rem;
  transition: color 900ms var(--ease-lux);
}
.hero__play-mark svg { inline-size: 18px; block-size: 18px; }
[dir="rtl"] .hero__play-mark svg { transform: scaleX(-1); }
.fbtn:hover .hero__play-mark, .fbtn:focus-visible .hero__play-mark { color: var(--bhov); }

/* The headline wipes up inside this clip, so it is genuinely gone rather
   than merely faded — no ghost of it can survive above the mark. */
.hero__lines { overflow: hidden; position: relative; z-index: 1; padding-block: .1em; margin-block: -.1em; }
/* One uniform gap gave four elements equal weight and they read as a
   single block. The rhythm is set per step instead: 50 / 45 / 55 at full
   size, scaling down with the viewport rather than collapsing. */
/* THE STACK TAKES THE WIDTH IT HAS, rather than shrink-wrapping to its
   widest child. As a non-stretched grid item .hero__lines sized itself to
   whatever was widest inside it - measured 564px against a 1336px column -
   and every child then inherited that as its ceiling. A headline set to one
   line needed 579 and was simply cut off at 564: the last word went missing
   with nothing to say it had.

   Full width, children still centred, so nothing moves - but the headline
   now has the whole column to sit in. */
.hero__lines { inline-size: 100%; }
.hero__lines-in { display: grid; gap: 0; justify-items: center; inline-size: 100%; }
/* TIGHTER BY ABOUT A QUARTER. The three gaps came to roughly 150px at 1920,
   which pushed the film button onto the bottom edge of the screen - and in
   English, where the headline is longer and wraps, far enough down that it
   could fall past it. A stack of four things does not need that much air
   between them when the whole screen is already empty around it. */
.hero__lines-in > .hero__word { margin-block-end: clamp(20px, 2.55vw, 38px); }
.hero__lines-in > .hero__h1   { margin-block-end: clamp(18px, 2.3vw, 34px); }
.hero__lines-in > .hero__sub  { margin-block-end: clamp(22px, 2.8vw, 42px); }
.hero__h1 {
  color: var(--beige);
  /* the hero sets its own size rather than taking --fs-display: the display
     scale is right for a section that owns a whole screen of its own, and
     too loud for a stack that also carries a wordmark, a sentence and a
     button */
  font-size: clamp(1.7rem, 3.75vw, 3.5rem); line-height: var(--lh-display);
  /* ENGLISH RUNS LONGER THAN THE ARABIC and wraps where the Arabic does not,
     so it takes a step down. The Arabic line is the client's own and keeps
     its size. */
  font-weight: 500; max-width: 18ch;
}
html[lang="ar"] .hero__h1 { font-size: clamp(1.6rem, 3.5vw, 3.3rem); }
html[lang="en"] .hero__h1 { letter-spacing: 0.005em; }
/* Almarai's only loaded face is 700 — stated explicitly here too, since this
   rule sits later in the cascade than the shared heading block and would
   otherwise win with whatever weight is set above. */
html[lang="ar"] .hero__h1 { font-weight: 500; }
/* ONE LINE, ALWAYS. The headline wrapping to two was what pushed the film
   button onto the bottom edge - and a button half off the screen is a button
   with half of it clickable, which is exactly what the client reported. The
   line is short enough to hold at every width: measured, it is the widest at
   its largest size and still well inside the column. */
html[lang="en"] .hero__h1 {
  font-size: clamp(1.45rem, 3.05vw, 2.85rem);
  white-space: nowrap; text-wrap: nowrap;
  /* AND THE ch CAP COMES OFF. 18ch exists to stop the ARABIC line running
     the width of the screen, and at the English size it resolves to 564px -
     eight pixels short of what one line of "Where Beauty Comes Home" needs,
     so the last word was cut with nothing to say it had been. A line that
     cannot wrap cannot overrun anything either: its own content is its
     width, and the font-size clamp is what keeps it inside a phone. */
  max-width: none;
}

.hero__sub { color: var(--beige); opacity: .8; font-size: clamp(.92rem, 1.05vw, 1.04rem); max-width: 46ch; }

.hero__meta {
  position: absolute; inset-inline: var(--gutter); bottom: clamp(1.25rem, 3.5vh, 2.5rem);
  z-index: 3; display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5);
  justify-content: space-between; align-items: flex-end;
  font-size: var(--fs-xs); letter-spacing: .1em; color: var(--beige);
  /* .7 measured 4.24:1 against the Earth veil, which is the lighter of the
     two. .82 clears the floor on both. */
  opacity: .82;
}
/* Mouse icon, a thin rule, then the label — stacked, and the whole group
   breathes very gently so it reads as alive without competing for attention. */
.hero__scroll {
  display: flex; flex-direction: column; align-items: center; gap: .55em;
  animation: hs-breathe 2.8s var(--ease-inout) infinite;
}
.hero__scroll-mouse { inline-size: 13px; block-size: auto; }
.hero__scroll-wheel { animation: hs-wheel 2.2s var(--ease-inout) infinite; }
.hero__scroll-line { display: block; inline-size: 1px; block-size: 22px; background: currentColor; opacity: .55; }
@keyframes hs-breathe { 0%, 100% { opacity: .8; } 50% { opacity: .3; } }
@keyframes hs-wheel   { 0%, 100% { opacity: 1; transform: translateY(0); } 50% { opacity: .25; transform: translateY(3px); } }

/* On short viewports (landscape phones, small laptops) the centred stack -
   wordmark, tagline, sentence, play button - and the pinned meta row start
   to collide. Give the stack the room.

   This block sits AFTER the rules it overrides on purpose. It used to be
   declared above .hero__meta, and a media query buys no specificity: the
   plain `.hero__meta { display: flex }` below it simply won, so the row
   never hid and at 1024x600 the play button cleared it by two pixels. */
@media (max-height: 680px) {
  .hero__meta { display: none; }
  .hero__type { gap: var(--s3); }
  .hero__lines-in { gap: var(--s3); }
  .hero__sub { font-size: var(--fs-body); }
  .hero__word--ar { inline-size: clamp(200px, 32vw, 380px); }
  .hero__word--en { inline-size: clamp(155px, 25vw, 295px); }
}

/* The film's identity mark. Always the physical lower-left corner — plain
   `left`, not the logical `inset-inline-start` used everywhere else on the
   site — because this one stays put in both languages rather than mirroring
   with the reading direction. JS fades it in ~500ms after the film settles
   into full frame; it is not tied to scroll position. */
/* A centre-left card, held at the same physical edge in both languages
   (`left`, not `inset-inline-start`) as briefed. JS adds .is-in the moment
   the film's aperture has opened across ~90% of the frame — derived from
   the live viewport, so it lands at the same visual moment on any screen.

   `visibility` rides along with the fade so the card leaves the tab order
   entirely while it is out: it contains a real link, and a focusable
   control inside an invisible box is a keyboard trap. Showing flips it
   visible immediately; hiding waits out the fade first. */
/* Mirrors with the reading direction — middle-left in English, middle-right
   in Arabic — so it always sits on the heavy end of the scrim. */
.hero__caption {
  position: absolute; z-index: 3;
  inset-inline-start: var(--gutter); top: 50%;
  inline-size: min(34rem, calc(100vw - var(--gutter) * 2));
  color: var(--beige);
  opacity: 0; visibility: hidden;
  transform: translateY(calc(-50% + 10px));
  transition: opacity 520ms var(--ease-out), transform 520ms var(--ease-out),
              visibility 0s linear 520ms;
}
.hero__caption.is-in {
  opacity: 1; visibility: visible;
  transform: translateY(-50%);
  transition: opacity 520ms var(--ease-out), transform 520ms var(--ease-out),
              visibility 0s linear 0s;
}
.hero__caption-h {
  font-family: var(--font-display); font-size: clamp(2.25rem, 5vw, 4.6rem);
  font-weight: 500; letter-spacing: 0.005em; line-height: var(--lh-display);
}
html[lang="ar"] .hero__caption-h { font-family: var(--font-display-ar); font-weight: 500; letter-spacing: 0; }
.hero__caption-s {
  margin-top: .35em;
  font-family: var(--font-latin); font-size: calc(var(--fs-lead) * .85);
  letter-spacing: .01em; opacity: .88;
}
html[lang="ar"] .hero__caption-s { font-family: var(--font-arabic); letter-spacing: 0; }

/* ---------- FILM PAGE ---------------------------------------------------- */
/* One player, given the same rounded-panel treatment as every other surface
   that carries its own ground. */
.filmpage { margin: 0; }
.filmpage__v {
  display: block; width: 100%; height: auto; aspect-ratio: 16 / 9;
  border-radius: var(--radius); background: var(--ink);
}
.filmpage__cap {
  margin-top: var(--s3);
  font-size: var(--fs-xs); color: var(--fg-mute);
}

/* Everything after the hero rides up over it. */
.after-hero {
  position: relative; z-index: 2; background: var(--bg);
  /* rides up over the still-stuck hero, so it reads as a panel */
  border-radius: var(--radius) var(--radius) 0 0;
}

/* ---------- 12b. EDITORIAL STATEMENT ------------------------------------
   A khatam lattice ground — the octagon-and-cross strapwork of Islamic
   architecture, drawn as fine line work rather than a filled motif — then
   the headline, an asymmetric lead that breaks the centred column, and one
   anamorphic frame whose label card cuts across its bottom edge.        */

.ed-sec { position: relative; padding-bottom: calc(var(--sp-section) + var(--s6)); }

.ed-sec > .shell, .ed-sec > .ed__plate { position: relative; z-index: 1; }

.ed { position: relative; display: grid; justify-items: center; text-align: center; }
.ed__rule {
  inline-size: 1px; block-size: clamp(48px, 8vh, 96px);
  background: var(--rule); margin-bottom: var(--s4);
}
.ed__h { max-width: 20ch; }

/* Centred, to sit with the centred headline above it. The measure opens up
   with the screen: at 46ch against a 98px headline the sentence read as a
   caption rather than as the second half of the statement. */
.ed__lead { margin-top: var(--s5); max-width: 46ch; text-align: center; }
@media (min-width: 1440px) { .ed__lead { max-width: 52ch; margin-top: var(--s6); } }
/* The section states where the plan is and then gives you somewhere to go.
   It sits in the space the lead already had below it rather than making the
   stage taller - the statement is one screen and stays one screen. */
.ed__cta { margin-top: clamp(2rem, 5.5vh, 3.75rem); }
@media (min-width: 1800px) { .ed__lead { max-width: 56ch; } }
.ed__lead-w { display: inline-block; white-space: pre; }
/* the words carry a transform now, so they need their own layer while
   they move and none once they have landed */
.ed__lead-w { will-change: auto; }

/* NO HOLD. --ed-run was the scroll the reveal used to be spread over: 190svh
   originally, then 80. Both are wrong now that the reveal is triggered - it
   plays on its own clock in under two seconds, so every pixel of run after
   that was the section pinned to the screen with nothing happening on it.
   That dead hold is what reads as "why am I still scrolling this".

   At 0 the stage is exactly one screen, the sticky has no travel and the
   section simply scrolls. The hand-off lift still runs across the last
   third, so it is moving the whole time it is on screen rather than
   freezing and then releasing. */
.ed__stage { --ed-run: 0svh; block-size: calc(100svh + var(--ed-run)); }
.ed__stick {
  position: sticky; top: 0; block-size: 100svh;
  display: grid; align-content: center;
  will-change: transform;
}
@media (max-width: 760px) { .ed__stage { --ed-run: 0svh; } }

/* The reveal is a clip on the heading alone - text only, no image or
   gradient underneath, so the repaint stays small. */
.ed__h { clip-path: inset(0% 100% 0% 0%); }
[dir="rtl"] .ed__h { clip-path: inset(0% 0% 0% 100%); }

/* THE FIGURE IS THE PICTURE'S WIDTH NOW. The caption is positioned
   against this box, and while the box was the full 1425px and the picture
   inside it only 576, the caption sat stranded at the page's left edge
   with the photograph floating in the middle - which is what the client
   is looking at. Shrink-wrapped and centred, the caption lands half off
   the picture's own bottom corner again, which is the interlock this
   section was drawn around. */
.ed__plate {
  position: relative; margin: var(--s7) auto 0; --ep: 0;
  /* EDGE TO EDGE, on the client's call. `fit-content` sized the figure to
     whatever the frame inside it worked out to be, so the picture sat as a
     card with the section's ground either side of it. `.ed-sec` is a
     `.stage`, which carries no inline padding of its own, so 100% here is
     the full width of the page - no negative margin trick needed. */
  inline-size: 100%; max-inline-size: none;
}
/* Opens from a centred slot to full bleed, scrubbed so the reveal tracks the
   scroll exactly and completes only once the frame is properly in view. */
/* The expand is a transform, not a width.

   `inline-size` is a layout property: growing it re-ran layout and repainted
   the image and everything under it on every frame, and the cost grew with
   the box - which is exactly why it got rougher the wider it opened. An
   animated border-radius repainted the rounded clip on top of that. A
   uniform scale reproduces the same growth (the aspect ratio is locked
   either way) on the compositor, and the layout underneath never moves. */
.ed__plate-in {
  position: relative;
  /* THE FRAME TAKES THE PICTURE'S RATIO, not the other way round. The post
     is a 1080x1350 social card and the client wants all of it - so the
     plate is 4:5 and nothing is cut.

     WHICH MEANS IT CANNOT BE A FULL-BLEED BAND ANY MORE: a 4:5 picture at
     1425px wide is 1781px tall, two screens of it. It is sized by the
     height it is allowed and centred instead, and the width cap is what
     makes it fill the screen on a phone, where 4:5 fits comfortably.
     Reported rather than assumed - full bleed and no crop cannot both be
     true of a portrait source. */
  aspect-ratio: 1080 / 1350;
  /* ONE DEFINITE DIMENSION, NOT TWO. Setting both the height and the
     width makes `aspect-ratio` inert - it only derives the one you did
     not give it. With both set the frame measured 375x409 on a phone,
     an 0.917 box pretending to be 0.8. The width is the definite one
     and the ratio gives the height: `80svh * .8` is the width at which
     the picture is exactly 80svh tall, and `100%` takes over on a phone,
     where the screen is narrower than that - which is what makes it run
     edge to edge there and sit as a card on a desktop. */
  inline-size: min(100%, calc(80svh * .8));
  margin-inline: auto;
  overflow: hidden; background: var(--paper-2);
  /* `inline-size: 100%` and a second `margin-inline` used to sit HERE,
     after the block above, and a later declaration wins - so the frame
     stayed full width and 2:1 while every rule that was supposed to
     make it 4:5 computed and did nothing. Folded, not raced: SS8f.
     measured 1425x720 (1.98) where 4:5 was expected. */
  border-radius: var(--radius);
  transform-origin: 50% 50%;
  will-change: transform;
  backface-visibility: hidden;
}
/* THE FRAME FOLLOWS THE FILE. The client exported a 1672x941 landscape of
   the same post, so on anything above a phone the plate goes back to being
   the band this section was drawn around, and nothing is cropped either way
   - the 4:5 rule above still holds below 761px, where the picture element
   also hands over the portrait file.

   Width from the height it is allowed, as before: `68svh * 1672/941` is the
   width at which the picture is exactly 68svh tall, and `100%` takes over
   on a narrower window. 68 rather than 80 because a 16:9 box at 80svh is
   1.42x the viewport WIDE - it would be capped by `100%` at every size and
   the svh term would never do anything. */
@media (min-width: 761px) {
  .ed__plate-in {
    aspect-ratio: 1672 / 941;
    /* The width cap is gone with the figure's `fit-content`: the frame is
       the page now. Width is the definite dimension and the ratio gives the
       height, which is the order SS8f requires - set both and `aspect-ratio`
       goes inert.

       BUT A PICTURE MUST NEVER BE SIZED BY ITS WIDTH ALONE. At 16:9 full
       bleed, a 2560-wide window wants 1440px of height, which on a 1080-tall
       screen runs the plate through the sections above and below it. The cap
       takes over there and `object-fit: cover` crops the difference, rather
       than the width shrinking and taking the full bleed with it. */
    inline-size: 100%;
    max-block-size: calc(100svh - var(--header-h));
  }
}
/* The phone used to force 16/10 here, from when the plate was a landscape
   band and 2.4:1 was too wide to read at 375px. The frame takes the
   picture's own ratio at every size now, so an override is a crop. */
.ed__plate-in picture { display: block; block-size: 100%; }
.ed__plate img {
  width: 100%; height: 100%; object-fit: cover;
  /* NO OVERSCAN HERE ANY MORE. `scale(1.12)` is what gives a plate its slow
     drift, and it pays for that with 6% off every edge - which on this
     picture sliced the right-hand road callout in half. The frame now
     matches the source's ratio exactly, so at scale 1 with `cover` not a
     pixel is lost. The drift goes with it; the reveal is the movement. */
  transform: none; transform-origin: center;
}

/* THE WIPE. The frame used to arrive by growing from 42% to full size, a
   scrubbed `scale` - so the picture was tied to the wheel and finished only
   where you stopped. It is a horizontal clip now, on the client's call, and
   it is TRIGGERED rather than scrubbed for the reason SS8f sets out: a
   scrubbed reveal makes the reader operate the animation instead of reading
   it. Same contract as the statement headline and the service frames.

   `--wipe` runs 1 -> 0 and the two rules pick the side, so the picture is
   uncovered FROM THE READING EDGE in both languages - left to right in
   English, right to left in Arabic. Driving it from one property rather
   than two keyframed clip-paths is what keeps the direction in CSS, where
   `[dir]` can see it, instead of in JS where it would have to be branched.

   This is the one place SS5's "no clip-path over images" gives way, and
   deliberately: the ban is on SCRUBBED clip-path, which repaints for as long
   as the visitor keeps scrolling. This runs once, for 1.4s, and then the
   property is static for the life of the page. */
.ed__plate-in { clip-path: inset(0 calc(var(--wipe, 0) * 100%) 0 0); }
[dir="rtl"] .ed__plate-in { clip-path: inset(0 0 0 calc(var(--wipe, 0) * 100%)); }

/* the card sits half off the frame, so type and image interlock */
.ed__cap {
  position: absolute; z-index: 2; bottom: 0;
  opacity: 0;
  inset-inline-start: clamp(1.25rem, 6vw, 5rem);
  transform: translateY(42%);
  display: grid; gap: .35rem;
  padding: clamp(1rem, 1.8vw, 1.5rem) clamp(1.25rem, 2.4vw, 2rem);
  background: var(--paper); color: var(--on-paper);
  border-radius: calc(var(--radius) * .5);
  box-shadow: 0 24px 48px -28px rgba(var(--ink-rgb), .45);
  text-align: start;
}
.ed__cap-k {
  font-family: var(--font-latin); font-size: var(--fs-xs);
  letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--clay-ink);
}
html[lang="ar"] .ed__cap-k { font-family: var(--font-arabic); text-transform: none; letter-spacing: 0; }
.ed__cap-v {
  font-family: var(--font-display); font-size: var(--fs-h3);
  font-weight: 500; letter-spacing: 0.005em;
}
html[lang="ar"] .ed__cap-v { font-family: var(--font-display-ar); letter-spacing: 0; }

/* ---------- 13. NUMBERS -------------------------------------------------
   A deep-blue panel that runs almost the full width of the window and
   sticks while three cards arrive and settle. The drawn palm that used to
   sit beside them is gone — it was decoration competing with the figures.
   An aerial of the site sits far back under the blue instead, so the panel
   has depth without anything pulling focus off the numbers.             */

/* ---------- 13. FIGURES -------------------------------------------------
   Three facts, three rows, alternating sides, in normal flow.

   NOTHING PINS HERE, and that is the design. The homepage already holds
   seven sticky stages - hero, statement, infrastructure, the landmark map,
   the landmark cards, the gallery - across roughly 24 screens. An eighth in
   the same idiom is not a style problem, it is an attention problem. This
   section is the only major one on the page with no pin, which is exactly
   what stops it reading as a repeat of the two sections after it.

   The moment comes from the counter resolving as the row crosses centre,
   not from freezing the page to deliver it. It also costs LESS scroll than
   the pinned version it replaces: about 215svh against 240svh.            */

.figs-sec { position: relative; isolation: isolate; }
/* Bleed to bleed. The rows sit outside .shell entirely and take the full
   width less a gutter, so the photographs run as close to the edges as the
   page allows instead of stopping at the 1560px content column. */
.figs-wide {
  inline-size: 100%;
  padding-inline: clamp(1rem, 3vw, 3.5rem);
  margin-block-start: clamp(1.5rem, 4vh, 3rem);
}
/* The rows used to butt straight up against each other - each is a screen
   tall, so the only air between two figures was whatever their own content
   left over. An explicit gap is about 15% more. */
.figs { display: grid; gap: clamp(1.5rem, 4vh, 3.5rem); }
/* Each row owns a screen. At a shared gap the plates of two rows were in
   view at once and read as one crowded grid rather than three facts; a row
   that fills the viewport and centres its own content means the eye only
   ever has one figure and one photograph to deal with. The min-height does
   the spacing, so there is no gap between rows to fall out of step with it. */
.fig {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: center; align-content: center;
  min-block-size: calc(100svh - var(--header-h));
  padding-block: clamp(1.5rem, 4vh, 3rem);
}
@media (max-width: 899px) {
  .fig { min-block-size: 0; padding-block: clamp(2.5rem, 7vh, 4.5rem); }
}
@media (min-width: 900px) {
  /* The landmarks section next door splits 523 / 641 with its text inset
     36px inside its own column. Matching that here is what stops the
     figure reading as pushed into the corner while the card beside it
     sits comfortably in from the edge - the containers were already the
     same 1252px, it was the split and the inset that differed. */
  /* EQUAL OUTER MARGINS, which is what "aligned" means here. Pinning the
     text hard against the picture closed the hole in the middle and opened
     the same hole on the outside, so the pair read as a photograph at the
     page edge with a caption floating beside it. The row takes its own
     inset - the picture comes in off the edge - and the text centres in
     its column, so it moves out toward the corner and the picture moves in
     toward the text, each by about half the old dead space. */
  /* Fractional tracks could not give equal outer margins: the picture
     stretches to its track's edge while the text centres inside a wider
     one, so the picture sat 51px off the page and the text 180px off it.
     Content-sized tracks with justify-content: center make the two a single
     centred object - the outer margins are equal BY CONSTRUCTION at any
     width, and the gap is a value rather than a leftover. */
  .fig {
    /* The text track is sized by the text; the picture takes what is left.
       The row's own padding is the outer margin, so it is EQUAL on both
       sides by construction at any width, and the gap is a value rather
       than a leftover. `justify-content: center` with content-sized tracks
       was the first attempt and it was fragile: a lazy picture has no
       intrinsic width, so two of the three rows measured 0 wide until they
       loaded. A 1fr track cannot do that. */
    grid-template-columns: auto minmax(0, 1fr);
    gap: clamp(2rem, 5vw, 9.5rem);
    padding-inline: clamp(1rem, 3vw, 80px);
  }
  /* THE TEXT HUGS THE PICTURE, it does not sit at the page edge. Bleed to
     bleed made the text column ~980px wide on a 2560 screen while the
     figure and its label occupy about 400 of it, so the remaining 580px
     opened as a hole between the number and the photograph - which is the
     "space is very large" note, and it grows with the screen.

     justify-self collapses that hole to the gap: on an odd row the text is
     in the inline-start column and hugs its inline-end edge; on an even row
     `order` has put it in the inline-end column, so it hugs the other side.
     Grid mirrors both automatically, so RTL needs no rule of its own.

     NO max-inline-size here, deliberately. A fixed 34ch cap looked right
     and cut "1,600+" off by 171px at the new figure size, because the
     figure is display type and a ch is body type - the two do not track.
     With justify-self and no cap the item shrink-wraps to its widest child,
     which is the number itself or the 34ch description, whichever is wider.
     It cannot be too narrow for the figure, at any size. */
  .fig__txt {
    --fig-pad: clamp(0px, 2.4vw, 44px);
    padding-inline: var(--fig-pad);
    /* ONE WIDTH FOR ALL THREE ROWS, derived from the figure rather than
       from the body measure. Shrink-wrapping each row to its own content
       made them hug the picture correctly and start at three different x
       positions, which reads as ragged; a ch cap is body type and does not
       track display type, so 34ch cut "1,600+" off by 171px.

       3.1em of the figure's own size is the widest number this section
       holds plus headroom, and the floor keeps the description readable
       when the figure is short. */
    /* A DEFINITE width, not min(100%, ...). In an `auto` track a percentage
       is indefinite, so the track sized to the text's max-content instead -
       and the row whose description is longest ended up with a track 255px
       wider than the element inside it, which reads as that row having a
       different gap from the other two. */
    inline-size: max(34ch, calc(var(--fs-fig) * 3.1 + var(--fig-pad) * 2));
    max-inline-size: 42vw;
  }
  /* THE MIDDLE ROW WAS NARROWER. `order` swaps which cell each child lands
     in, but the column TRACKS keep their widths - so on the reversed row the
     picture was being handed the .82fr column meant for the text. The
     template has to reverse with the content. */
  .fig:nth-child(even) { grid-template-columns: minmax(0, 1fr) auto; }
  /* the middle row turns over, so the eye crosses the page rather than
     running down one edge */
  .fig:nth-child(even) .fig__txt   { order: 2; }
  .fig:nth-child(even) .fig__media { order: 1; }
}

/* ---- the figure ---- */
.fig__head { display: block; inline-size: fit-content; max-inline-size: 100%; }
.fig__n {
  display: block;
  font-family: var(--font-display); font-weight: 400;
  font-size: var(--fs-fig); line-height: .86;
  font-weight: 500;
  color: var(--accent); font-variant-numeric: tabular-nums;
  direction: ltr; unicode-bidi: isolate; letter-spacing: .005em;
}
/* This override existed because Clash Grotesk has no Arabic-Indic digits and
   ٠-٩ fell back to a face with taller metrics that then clipped. The digits
   are Latin in both languages now, so the figure keeps the display face and
   reads identically either way - which is the point of the change. */
html[lang="ar"] .fig__n { line-height: .9; }
.fig__u {
  display: block; margin-block-start: .55rem;
  font-family: var(--font-latin); font-size: calc(var(--fs-sm) * 1.1); font-weight: 400;
  letter-spacing: .16em; text-transform: uppercase; color: var(--fg-mute);
}
html[lang="ar"] .fig__u {
  font-family: var(--font-arabic); letter-spacing: 0; text-transform: none;
  font-size: calc(var(--fs-lead) * 1.1);
}
/* The separator is the tapered rule, and it is exactly as wide as the
   figure above it - .fig__head is fit-content, so the rule measures the
   number rather than the column. */
.fig__head::before {
  content: ''; display: block;
  margin-block-end: clamp(1.1rem, 2.4vh, 1.6rem);
  block-size: 1px; background: var(--accent); opacity: .5;
  -webkit-mask: linear-gradient(to right, transparent 0,
    #000 var(--cut-x), #000 calc(100% - var(--cut-x)), transparent 100%);
  mask: linear-gradient(to right, transparent 0,
    #000 var(--cut-x), #000 calc(100% - var(--cut-x)), transparent 100%);
}
.fig__k {
  /* IT LEADS THE FIGURE NOW, so the space goes underneath it. A label that
     names the number before the number arrives is how a survey entry reads;
     it was below, where it had to be read back up to. */
  display: block; margin-block-end: clamp(1.1rem, 2.4vh, 1.6rem);
  font-family: var(--font-display); font-size: clamp(1.43rem, 2.2vw, 2.1rem);
  font-weight: 500; line-height: 1.2; color: var(--head);
}
html[lang="ar"] .fig__k { font-family: var(--font-display-ar); line-height: 1.42; }
.fig__d {
  margin-block-start: .7rem;
  font-size: calc(var(--fs-lead) * 1.1); line-height: 1.6;
  color: var(--fg-mute); max-inline-size: 34ch;
}

/* ---- the plate ----
   Plain rectangles. The chamfer and the hairline came off on the client's
   call: at this size the corner cut read as a graphic device competing with
   the photograph rather than framing it. The shadow stays, and is a
   box-shadow again because there is no clipped alpha left for drop-shadow
   to follow. */
/* THE SHADOW BELONGS TO THE PICTURE, not to the box around it. On the
   wrapper it was cast by an element you cannot see - and once the height cap
   below started biting, the wrapper was wider and taller than the picture
   inside it, so the shadow read as a second pale rectangle offset down and
   to the right. A shadow can only be honest if the box casting it is the box
   being drawn. */
.fig__media { position: relative; }
.fig__media picture, .fig__media img { border-radius: 5px; }
.fig__media picture {
  box-shadow: 0 20px 34px rgba(var(--ink-rgb), .26), 0 4px 10px rgba(var(--ink-rgb), .14);
}
/* THE PICTURE MUST NOT FILL THE ROW. Its height came only from the width
   and the 16:10 ratio, so on a short wide window - 2560x1080, say - the
   derived height exceeded the row and the frame ran into the section above
   and below it. The cap crops through object-fit instead of squeezing the
   width, which would have taken the row's alignment with it. */
.fig__media picture {
  display: block; overflow: hidden;
  /* inline-size: 100% is load-bearing. `aspect-ratio` only DERIVES the width
     while the width is auto - so with a max-block-size and an auto width the
     cap made the picture narrower as well as shorter, and it stopped filling
     its column. Definite width, ratio gives the height, cap clamps it, and
     object-fit crops the difference. */
  inline-size: 100%;
  aspect-ratio: 16 / 10;
  max-block-size: calc(100svh - var(--header-h) - clamp(4rem, 11vh, 9rem));
}
.fig__media img {
  inline-size: 100%; block-size: 100%; object-fit: cover; display: block;
  transform: translate3d(0, var(--par, 0px), 0) scale(1.09);
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .fig__media img { transform: none; scale: 1; will-change: auto; }
}


/* ---------- 13b. INFRASTRUCTURE: full bleed, navigated -------------------
   The photograph IS the section's ground. The old shape was a frame beside a
   heading on a flat brown field, stepped by the scroll across four screens;
   this is one screen, five pictures, and a visitor moves through them.

   Three consequences worth stating, because each one removed a whole class
   of bug this section used to have:

   - nothing is scroll-driven, so there is no sticky run, no step length to
     tune against the window's height, and no index to keep in agreement
     with a cached offset;
   - the picture has no column to be limited by, so it cannot be starved of
     width or stranded in a track wider than itself;
   - the section costs ONE screen instead of 4.23.
*/
/* NOT a .stage. That class adds --sp-section top and bottom, which on a
   full-bleed section is 158px of padding outside a body that already
   manages its own - measured, the section came to 880px against a 720px
   screen and the rail fell below the fold. It carries `position` and
   `isolation` itself instead. */
/* THE SECTION IS TALLER THAN THE STAGE, AND THAT IS THE POINT. The layout
   fills the screen edge to edge - eyebrow on the top line, rail on the bottom
   one - so in a box exactly one screen tall it frames correctly at ONE scroll
   position and is cropped at every other. Measured at 1920x1080: 127px off
   that position the rail is already cut by 78px, and 200px off it the name
   goes too. The run gives the stage something to stick through, so the
   composition a visitor looks at is the composition as it was drawn. */
.nets-sec {
  position: relative; overflow: clip;
  --nets-run: clamp(300px, 46svh, 620px);
  min-block-size: calc(100svh + var(--nets-run));
  background: var(--carbon);
  color: var(--on-ink);
  --fg: var(--on-ink); --fg-mute: var(--on-ink-mute); --head: var(--on-ink);
  --rule: var(--hair-ink);
  /* NO RAKE. A straight bottom edge, like every other ordinary section. The
     page keeps one raked boundary - the register into the footer - and one
     is a device; two is a habit. */
}
/* `isolation` lives here rather than on the section, so the frames' negative
   z-index is scoped to the stage. `overflow: clip` and not `hidden` - hidden
   would make this a scroll container and break its own stickiness. */
.nets__stage {
  position: sticky; inset-block-start: 0;
  block-size: 100svh; overflow: clip; isolation: isolate;
}

/* ---- the pictures ----
   One cell, stacked. The live one is revealed OVER the one before it, so the
   box is covered at every instant and one picture becomes the next directly -
   the same contract the old frame swap had, and the reason there is no flash
   of ground between them. */
.nets__frames { position: absolute; inset: 0; z-index: -2; display: grid; }
.nets__frame {
  grid-area: 1 / 1; overflow: hidden;
  clip-path: inset(0% 0% 100% 0%);
}
.nets__frame.is-on { clip-path: inset(0% 0% 0% 0%); }
.nets__frame picture { display: block; block-size: 100%; }
.nets__frame img {
  inline-size: 100%; block-size: 100%; object-fit: cover; display: block;
}

/* TWO scrims, not one. A single overlay dark enough to carry display type at
   the floor flattens the whole photograph; one along the reading edge and one
   up from the bottom leave the middle of the picture alone, which is the part
   worth looking at. Measured against the real composited stack - see the
   figures in CLAUDE.md. */
.nets__scrim {
  position: absolute; inset: 0; z-index: -1; pointer-events: none;
  /* MEASURED, then raised. At .88 -> .62 -> .10 the reading edge let go a
     third of the way across and the display name fell to 1.2:1 over the
     paved-road render and 1.72:1 over the stormwater one - both of them
     bright, pale-street pictures. The first stop holds nearly opaque across
     the half of the frame the type actually occupies, and the floor gradient
     climbs with it; the middle third of every picture is still clear. */
  /* RE-WEIGHTED WHEN THE NAME MOVED UP. At the floor the name had the floor
     gradient under it; in the middle of the frame this one carries it alone,
     over the brightest part of the picture - measured 2.07:1 on the paved
     road render at the old weights, against a 3:1 floor. Three candidates
     were painted onto the real composited stack and read back before this
     one was chosen: it is the lightest that clears 4.5:1 on all five.
     Measured worst 5.69:1. */
  background-image:
    linear-gradient(to var(--scrim-dir, right),
      rgba(18, 18, 18, .95) 0%, rgba(18, 18, 18, .92) 30%,
      rgba(18, 18, 18, .74) 50%, rgba(18, 18, 18, .18) 70%,
      rgba(18, 18, 18, .08) 80%,
      /* A TIGHT EDGE VIGNETTE, not a wash. The forward arrow sits out here
         and measured 1.32:1 over the bright renders - invisible. Ramping
         only across the outer tenth gives it real ground without putting a
         second shape around it and without touching the middle of the
         picture, which is still clear. */
      rgba(18, 18, 18, .30) 88%, rgba(18, 18, 18, .74) 100%),
    /* SHORTER THAN IT WAS (.92 / .55 at 26% / clear at 56%). That height
       existed to carry the network name on the bottom line; the name is in
       the middle of the frame now and only the rail is left down here, so
       the gradient clears at a third of the height instead of over half and
       the lower part of every picture - which is where these subjects are -
       is visible again. */
    linear-gradient(to top,
      rgba(18, 18, 18, .90) 0%, rgba(18, 18, 18, .52) 15%,
      rgba(18, 18, 18, .00) 33%);
}
[dir="rtl"] .nets__scrim { --scrim-dir: left; }

/* ---- the body ---- */
.nets__body {
  position: relative; z-index: 1;
  block-size: 100%;
  display: grid; grid-template-rows: auto 1fr auto;
  gap: clamp(1rem, 3vh, 2.25rem);
  /* The floor reserve is the WHOLE rake, not a fraction of it. The cut takes
     --rake off one side of the bottom edge, and the rail runs the full width
     - so at .55 the outermost network name on the cut side was sliced
     through by the diagonal. */
  padding-block: calc(var(--header-h) + clamp(1.25rem, 3.5vh, 2.75rem))
                 clamp(1.75rem, 4.5vh, 3.25rem);
  padding-inline: var(--gutter);
}
/* The type stays on the side the scrim is dark on. Without a cap these
   blocks span the whole body, which puts their boxes over the bright middle
   of the picture - and a measurement taken on the box rather than on the
   glyphs then reads a contrast nobody actually experiences. */
.nets__eyebrow {
  align-self: start; justify-content: flex-start;
  max-inline-size: min(100%, 46ch);
  color: var(--on-ink); font-weight: 500;
}

/* ---- the live network ----
   NO COUNTER. There was an "01 / 05" above the name and it failed its
   contrast on three of the five pictures - 1.32:1 over the stormwater
   render - because it sits high enough that the floor gradient has let go.
   Fighting the gradient for it was the wrong answer: the rail below already
   numbers every network AND shows which one is live, so the counter was the
   same fact said twice. Removed rather than rescued. */
.nets__live {
  align-self: center;
  display: grid; gap: clamp(.35rem, 1.2vh, .9rem);
  /* NARROWER NOW THAN AT THE FLOOR. At the bottom the floor gradient carried
     the far end of a long name; in the middle of the frame only the
     reading-edge gradient does, and that has let go by about 52% of the
     width. 44vw keeps the whole box on the side the scrim is dark on - the
     measured figures are in CLAUDE.md. */
  max-inline-size: min(100%, 40vw);
}
/* The names are all in the DOM and only the live one shows - writing the name
   into one node from JS would mean rebuilding it on every language change and
   reading it back out of the DOM, which is the "ARAR" trap SS4 warns about. */
.nets__names { display: grid; container-type: inline-size; }
.nets__name {
  grid-area: 1 / 1; align-self: end;
  font-family: var(--font-display);
  /* capped against its OWN box, not the window - see the note on .step__t:
     the widest word in this list is "Stormwater" at 5.527em */
  /* A STEP DOWN FROM --fs-step. At the service-heading size the name filled
     half the frame and the photograph behind it had nowhere to be looked at;
     .82 of it still carries the screen and gives the picture its share. */
  font-size: min(calc(var(--fs-step) * .82), 15cqw);
  font-weight: 500; line-height: 1.04; letter-spacing: .005em;
  color: var(--on-ink);
  opacity: 0; transform: translateY(16px);
  transition: opacity 460ms var(--ease-out), transform 620ms var(--ease-lux);
}
html[lang="ar"] .nets__name { font-family: var(--font-display-ar); line-height: 1.3; letter-spacing: 0; }
.nets__name.is-on { opacity: 1; transform: none; }

/* ---- the rail ---- */
.nets__rail {
  align-self: end;
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: clamp(.5rem, 1.6vw, 1.75rem);
}
@media (max-width: 860px) {
  .nets__rail { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.nets__tab {
  appearance: none; background: none; cursor: pointer;
  display: grid; gap: .3rem; justify-items: start; text-align: start;
  padding: clamp(.6rem, 1.4vh, 1rem) 0 0;
  border: 0;
  color: var(--on-ink-mute);
  transition: color var(--t-med) var(--ease-out);
}
/* THE RULE ABOVE EACH TAB IS THE TIMER. The dwell has to be shown somewhere
   or an image that changes on its own reads as a glitch; drawing it on the
   line the design already has means no second indicator. The fill is a
   scaleX on a composited child - no width animation, which SS5 rules out. */
.nets__tab-bar {
  position: absolute; inset-block-start: 0; inset-inline: 0;
  block-size: 2px; background: rgba(207, 200, 192, .26);
  overflow: hidden;
}
.nets__tab { position: relative; }
.nets__tab-bar i {
  display: block; block-size: 100%; background: var(--on-ink);
  transform: scaleX(0); transform-origin: left center;
}
[dir="rtl"] .nets__tab-bar i { transform-origin: right center; }
.nets__tab:hover { color: var(--on-ink); }
.nets__tab:hover .nets__tab-bar { background: rgba(207, 200, 192, .6); }
.nets__tab:focus-visible { outline: 2px solid var(--on-ink); outline-offset: 3px; }
.nets__tab.is-on { color: var(--on-ink); }
.nets__tab-n {
  font-family: var(--font-latin); font-size: var(--fs-xs);
  letter-spacing: .14em; opacity: .62; font-variant-numeric: tabular-nums;
}
.nets__tab-t {
  font-size: var(--fs-sm); font-weight: 500; line-height: 1.3;
}
html[lang="ar"] .nets__tab-t { font-family: var(--font-arabic); }

/* ---- the navigation ----
   Round, like every other round control on the site. The lightbox arrows are
   circles and these do the same job; SS8b keeps a circle where the thing
   really is a circle. */
/* THE ARROW IS THE CONTROL. The ring around it was a second shape doing the
   same job - the same argument that took the circle off the hero's play mark.

   What a bare glyph on a photograph needs instead is its own legibility, and
   a drop-shadow gives it that on any ground: a dark halo under a light
   stroke reads against sky, against asphalt and against a lit street without
   the arrow itself having to be opaque white. It sits at .72 and comes to
   full on hover, so it is present without being loud - and because the halo
   travels with the glyph, a picture change cannot take it away. */
.nets__nav {
  position: absolute; z-index: 3; top: 50%; translate: 0 -50%;
  inline-size: var(--nav-d, 56px); block-size: var(--nav-d, 56px);
  display: grid; place-items: center;
  border: 0; background: none; padding: 0;
  color: var(--on-ink); cursor: pointer; opacity: .72;
  /* THREE SHADOWS, and they do the work a ring used to. A tight one draws a
     dark outline against the stroke, a short one gives it weight, and a wide
     soft one pools enough ground under the glyph that it reads on sky as
     well as on asphalt. The arrow never has to go opaque white, and nothing
     has to know which picture is showing. */
  filter: drop-shadow(0 0 1px rgba(18, 18, 18, .95))
          drop-shadow(0 1px 3px rgba(18, 18, 18, .9))
          drop-shadow(0 0 16px rgba(18, 18, 18, .8));
  transition: opacity var(--t-med) var(--ease-out),
              transform var(--t-med) var(--ease-out);
}
@media (min-width: 1800px) { .nets__nav { --nav-d: 64px; } }
@media (min-width: 2200px) { .nets__nav { --nav-d: 72px; } }
.nets__nav svg { inline-size: 52%; block-size: 52%; stroke-width: 1.6; }
.nets__nav:hover { opacity: 1; transform: scale(1.08); }
.nets__nav:focus-visible { outline: 2px solid var(--on-ink); outline-offset: 4px; border-radius: 4px; }
.nets__nav--prev { inset-inline-start: clamp(.75rem, 2.2vw, 2.5rem); }
.nets__nav--next { inset-inline-end: clamp(.75rem, 2.2vw, 2.5rem); }
/* the arrowheads are drawn pointing right, so RTL mirrors the whole glyph */
[dir="rtl"] .nets__nav svg { transform: scaleX(-1); }

/* ---- the entrance ----
   NOT A FADE. The site has its own vocabulary for arriving and this section
   speaks it: type is uncovered by a wipe running from the reading edge, a
   rule DRAWS out from that same edge, and everything settles on expo rather
   than stopping. The one thing that does cross-fade is the photograph, and
   only because scale + opacity are the two properties SS5 allows on a layer
   that size.

   The REST STATE lives here; the choreography is a GSAP timeline in `Steps`,
   because the stagger has to interleave five rules, five labels, a headline
   wipe and a picture settle, and a chain of transition-delays cannot express
   that without becoming a table of magic numbers.

   The wipes are `clip-path` on TEXT ONLY, which SS5 permits explicitly: a
   small area with nothing composited underneath. The picture is never
   clipped - it scales. */
/* ONE PHASE. The two-class split this replaces held the picture at opacity 0
   until a trigger fired, and there was no good moment for that trigger: early
   enough to avoid a screen of flat carbon arriving meant the settle was over
   before the section was on screen, which is exactly how it read.

   The picture rests DIM AND OVERSIZED rather than absent, so nothing is
   missing on the way in and there is still real distance left to travel when
   the stage locks. Opacity and scale only - the two properties SS5 allows on
   a layer this size. */
.nets-sec:not(.is-in) .nets__frames { opacity: .5; transform: scale(1.1); }
.nets-sec:not(.is-in) .nets__scrim  { opacity: .55; }
.nets-sec:not(.is-in) .nets__eyebrow,
.nets-sec:not(.is-in) .nets__live,
.nets-sec:not(.is-in) .nets__nav,
.nets-sec:not(.is-in) .nets__tab-n,
.nets-sec:not(.is-in) .nets__tab-t { opacity: 0; }
.nets-sec:not(.is-in) .nets__tab-bar { transform: scaleX(0); }
.nets__tab-bar { transform-origin: left center; }
[dir="rtl"] .nets__tab-bar { transform-origin: right center; }

@media (prefers-reduced-motion: reduce) {
  .nets__name { transition: opacity 200ms linear; transform: none; }
  .nets__nav, .nets__tab { transition: none; }
  .nets__nav:hover { transform: none; }
  /* Everything at rest, nothing withheld: a section that never animates must
     not depend on an animation to become visible. */
  .nets-sec .nets__frames, .nets-sec .nets__scrim, .nets-sec .nets__eyebrow,
  .nets-sec .nets__live, .nets-sec .nets__nav, .nets-sec .nets__tab-n,
  .nets-sec .nets__tab-t {
    opacity: 1 !important; transform: none !important;
    clip-path: none !important; transition: none;
  }
  .nets-sec .nets__nav { opacity: .72 !important; }
  .nets-sec .nets__tab-bar { transform: scaleX(1) !important; }
  .nets__tab-bar i { transform: scaleX(1); }
}


/* ---------- 13c. LAND PLATE ---------------------------------------------
   The Qassim landscape, full bleed, opening the masterplan page. It carries
   no type, so it needs no scrim and no measured contrast. Static by
   intent - a photograph this size moving on scroll buys nothing. */
.landplate { margin: 0; display: block; }
.landplate img {
  display: block; width: 100%; height: clamp(260px, 42vh, 520px);
  object-fit: cover; background: var(--ink);
}

/* ---------- 14. INFRASTRUCTURE ------------------------------------------- */
/* A technical cutaway with annotation. Vector belongs on a cutaway; it did
   not belong wrapped around a circular crop.                              */

.cutaway { position: relative; }
.cutaway__media { position: relative; overflow: hidden; border-radius: var(--radius); background: var(--paper-2); }
.cutaway__media img { width: 100%; height: auto; }
.cutaway__media::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(var(--ink-rgb), .55), rgba(var(--ink-rgb), .05) 55%);
}
.cutaway__svg { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 2; overflow: visible; }
.cutaway__svg line { stroke: var(--clay); stroke-width: 1; vector-effect: non-scaling-stroke; }
.cutaway__svg circle { fill: var(--clay); }

.nets { display: grid; gap: 0; margin-top: var(--s6); }
@media (min-width: 900px) { .nets { grid-template-columns: repeat(5, minmax(0,1fr)); } }
.net {
  padding: var(--s4) 0; border-top: 1px solid var(--rule);
  display: grid; gap: .6rem; align-content: start;
  opacity: .45; transition: opacity var(--t-med) var(--ease-out);
}
@media (min-width: 900px) {
  .net { padding-inline: var(--s3); }
  .net:first-child { padding-inline-start: 0; }
  .net + .net { border-inline-start: 1px solid var(--rule); }
}
.net.is-on { opacity: 1; }
.net__n { font-family: var(--font-latin); font-size: var(--fs-xs); letter-spacing: .14em; color: var(--accent); }
.net__t { font-size: var(--fs-h3); font-weight: 500; line-height: 1.3; }
.net__d { font-size: var(--fs-sm); color: var(--fg-mute); }

/* ---------- 15. EDITORIAL IMAGE LAYOUTS ---------------------------------- */
/* Images are placed, not tiled. Offset pairs, generous margins, captions
   set small and quiet underneath.                                        */

.plate { position: relative; overflow: hidden; border-radius: var(--radius); background: var(--paper-2); }
.plate > picture { display: block; block-size: 100%; }
.plate img { width: 100%; height: 100%; object-fit: cover; }
.plate--wide { aspect-ratio: 16/9; }
.plate--4x3  { aspect-ratio: 4/3; }
.plate--3x4  { aspect-ratio: 3/4; }
.plate--1x1  { aspect-ratio: 1; }

/* Plain rectangles, on the client's call. The chamfer came off the gallery
   prints for the same reason it came off the figures: at print size the cut
   corner read as a device competing with the photograph. Square corners and
   no radius - the series reads as a series because the frames are identical,
   which is the cheaper way to say it. */
.hgal__item .plate, .g .plate,
.hgal__item .plate > picture, .g .plate > picture,
.hgal__item .plate img, .g .plate img {
  /* 5px, the same soft corner the figures take - enough to stop the frame
     reading as a raw crop, not enough to argue with the chamfered controls.
     The shadow matches the figures too, so the two photographic sections
     read as one treatment. */
  border-radius: 5px;
  clip-path: none;
}
.hgal__item .plate, .g .plate {
  box-shadow: 0 20px 34px rgba(var(--ink-rgb), .26), 0 4px 10px rgba(var(--ink-rgb), .14);
}
.cap { margin-top: var(--s2); display: flex; gap: var(--s3); justify-content: space-between; font-size: var(--fs-xs); letter-spacing: .06em; color: var(--fg-mute); }

.duo { display: grid; gap: var(--s5); }
@media (min-width: 900px) {
  .duo { grid-template-columns: repeat(12, 1fr); gap: var(--s4); align-items: start; }
  .duo__a { grid-column: 1 / span 7; }
  .duo__b { grid-column: 9 / span 4; margin-block-start: var(--s8); }
}

.sticky-duo { display: grid; gap: var(--s5); align-items: start; }
@media (min-width: 1000px) { .sticky-duo { grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: var(--s7); } }
.sticky-duo__media { position: relative; aspect-ratio: 4/5; overflow: hidden; border-radius: 3px; }
@media (min-width: 1000px) { .sticky-duo__media { position: sticky; top: calc(var(--header-h) + var(--s4)); } }
.sticky-duo__media figure { position: absolute; inset: 0; }
.sticky-duo__media img { width: 100%; height: 100%; object-fit: cover; }
.sticky-duo__media figure + figure { opacity: 0; }
.sticky-duo__text { display: grid; gap: var(--s8); }
@media (max-width: 999px) { .sticky-duo__text { gap: var(--s6); } }
.blk { display: grid; gap: var(--s2); max-width: 40ch; }
.blk__n { font-family: var(--font-latin); font-size: var(--fs-xs); letter-spacing: .16em; color: var(--accent); }

/* ---------- 16. PROJECT CONTEXT (sticky map + landmark cards) ------------ */
/* Ported from the client-authored prototype. Their revision moves it onto
   the brand palette and swaps the per-frame viewBox rewrite for a single
   GPU-composited transform on #map-world, which is what kills the jitter.
   Three deliberate deviations: the site's own fonts replace the prototype's
   system stack; headings take the site-wide 500 rather than the prototype's
   700; and the prototype's `scroll-snap-type: y mandatory` on <html> is
   dropped, because it would snap every section on the site and fights
   Lenis's smooth scrolling. Each card is still a full viewport tall, so the
   observer continues to settle on exactly one at a time. */

.context { background: var(--paper); color: var(--on-paper); }

.project-context {
  --pc-navy:       var(--ink);
  --pc-navy-soft:  rgba(var(--ink-rgb), .72);   /* .62 in the source; raised to
                                              clear 4.5:1 on the beige */
  --pc-navy-faint: rgba(var(--ink-rgb), .14);
  --pc-sync:       1150ms;
  --pc-ease:       cubic-bezier(.65, 0, .35, 1);
  position: relative;
}

.context-grid { display: grid; grid-template-columns: 1.05fr 1fr; align-items: start; }
/* The map pane is sticky and starts at the very top of the grid, so on a
   wide screen the section heading and the frame's top edge ended up a few
   pixels apart. The grid needs a gutter of its own above it. */
@media (min-width: 861px) { .context-grid { margin-block-start: clamp(1.5rem, 4vh, 4rem); } }
@media (min-width: 1440px) { .context-grid { margin-block-start: clamp(3rem, 7vh, 7rem); } }
@media (max-width: 860px) { .context-grid { grid-template-columns: 1fr; } }

.map-pane {
  position: sticky; top: var(--header-h); height: calc(100vh - var(--header-h));
  display: flex; align-items: center; justify-content: center; padding: 40px;
}
@media (max-width: 860px) { .map-pane { position: relative; height: 44vh; padding: 16px; } }

.map-mask {
  position: relative; width: 100%; height: 100%; max-height: 820px;
  border-radius: var(--radius); overflow: hidden; background: var(--paper);
  box-shadow: 0 30px 60px -30px rgba(var(--ink-rgb), .35);
  outline: 1px solid var(--pc-navy-faint); outline-offset: -1px;
}
.map-mask svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* The whole illustration pans and zooms as one composited transform. */
#map-world {
  transform-origin: 0 0;
  transition: transform var(--pc-sync) var(--pc-ease);
  will-change: transform;
  backface-visibility: hidden;
}
/* Roads are line art at many scales; letting the rasteriser trade a little
   precision for speed is what keeps the pan smooth now the network is real
   OSM geometry rather than a dozen drawn shapes. */
#roads-major path, #roads-minor path { shape-rendering: optimizeSpeed; }

/* Hidden until the map flies to them; the marker that lands scales up and
   fades in on a soft overshoot, in a deep green that reads clearly against
   the beige ground and the grey road network. */
/* The positioning transform lives on the outer <g> as an SVG attribute, and
   a CSS `transform` would REPLACE it rather than compose with it — which is
   what collapsed every pin onto the canvas origin. The animation therefore
   targets an inner group, leaving the placement untouched. */
.map-mask svg [id^="pin-"] .pin-anim {
  opacity: 0; transform: scale(.35) translateY(-14px);
  transform-box: fill-box; transform-origin: center;
  transition: opacity .5s var(--pc-ease), transform .62s cubic-bezier(.22, 1.2, .36, 1);
}
.map-mask svg [id^="pin-"].is-visited .pin-anim { opacity: .38; transform: scale(.78) translateY(0); }
.map-mask svg [id^="pin-"].is-active  .pin-anim { opacity: 1;  transform: scale(1.18) translateY(0); }
.map-mask svg [id^="pin-"].is-active .pin-shape { fill: var(--ink); stroke: var(--carbon); }
.map-mask svg [id^="pin-"].is-active .pin-halo  { fill: rgba(var(--ink-rgb), .30); }
/* The pins are planted banners now, not teardrops. Two consequences:

   - The flag is asymmetric, so it mirrors with the reading direction or a
     map full of them reads as wind rather than as markers. The flip goes on
     .pin-flag, an inner <g>, because a CSS transform on an element REPLACES
     its transform attribute - the path carries its own placement.
   - The numbered disc inside the old teardrop is gone. The landmark cards
     carry no numbering, so it referenced nothing; it also left no room on a
     banner. TODO(client): confirm nobody was using those numbers.  */
/* THE FLAG DOES NOT MIRROR. It used to flip with the reading direction, on
   the argument that a map of markers all leaning one way reads as wind. The
   mark settles it: in the logo the banner flies to the right, so a pin that
   flies left in Arabic is the brand's own shape backwards. It stays right in
   both languages. */
.map-mask svg [id^="pin-"].is-active .pin-base { opacity: .42; }

/* A route belongs to its landmark: none of them exist until that card is
   reached, so the base state is invisible and .is-active/.is-visited are
   the only things that bring one in. */
.map-mask svg [id^="route-"] { opacity: 0; transition: stroke .4s ease, opacity .4s ease; }
.map-mask svg [id^="route-"].is-active  { stroke: var(--pc-navy); opacity: 1; }
.map-mask svg [id^="route-"].is-visited { stroke: #A69C8C; opacity: .7; }

.details-pane { padding-inline-start: var(--s6); }
@media (max-width: 860px) { .details-pane { padding-inline-start: 0; } }

/* The pane's own height is the scroll: one screen per stop. The stage
   inside it sticks, so every location arrives at exactly the same place on
   screen. Laid out one card per screen - which is what this was - the active
   card's text drifted up the viewport as you scrolled through its screen,
   and the eye had to follow it. Nothing moves now except the content. */
/* ONE SCREEN PER STOP WAS EIGHT SCREENS for one section - a third of the
   page for seven place names. It takes the same shape the services do: the
   sticky element's own screen plus a run per stop. `pick()` needs no change,
   because it derives the span from the pane's real height rather than from
   a stop count.

   Measured at 1920x1080: 8640px -> 4258px, 8.0 screens -> 3.9. */
.details-pane {
  position: relative;
  --landmark-run: clamp(380px, 42svh, 560px);
  min-height: calc(100svh + var(--stops, 7) * var(--landmark-run));
}
.details-sticky {
  position: sticky; top: 0; height: 100svh;
  display: grid; align-content: center;
}
/* every card in the same grid cell, so they stack and the box is as tall as
   the tallest one - no absolute positioning, no measuring */
/* One explicit column. Left to `auto` the track sized to the longest name's
   max-content, so the card's own `100%` resolved against THAT rather than
   against the pane and the box overflowed by 31px. */
.details-cards { display: grid; grid-template-columns: minmax(0, 1fr); }
.landmark-card {
  grid-area: 1 / 1; align-self: center; justify-self: center;
  /* Left-aligned in a column that keeps growing, the card ended up as a
     block of text with a third of the screen empty beside it. It centres
     in its column and grows with the screen, the same fix the figures
     took. */
  /* A DEFINITE width, not just a cap. justify-self shrink-wraps the box, so
     with only a max-width every card was as wide as its own name and the
     text jumped left and right as the cards swapped. One box, one start
     edge, centred in the column. */
  inline-size: min(100%, clamp(520px, 46vw, 1080px));
  opacity: 0; pointer-events: none;
  /* a short flip off the bottom edge rather than a slide: the card settles
     into place instead of arriving from somewhere. Transform and opacity
     only - nothing here costs a repaint. */
  transform: perspective(900px) rotateX(-7deg) translateY(12px);
  transform-origin: 50% 100%;
  transition: opacity 420ms var(--ease-out), transform 620ms var(--ease-lux);
}
.landmark-card.is-active {
  opacity: 1; pointer-events: auto;
  transform: perspective(900px) rotateX(0deg) translateY(0);
}
@media (max-width: 860px) {
  .details-pane { min-height: calc(86svh + var(--stops, 7) * var(--landmark-run)); }
  .details-sticky { height: 86svh; }
}

/* The distance leads, and it is the number that matters - so it is set at
   display size above the name, with no pill around it. Kilometres only: the
   drive times were derived at ~35km/h and never confirmed by the client. */
.landmark-distance {
  display: block; margin: 0 0 10px; padding: 0; border: 0; background: none;
  font-family: var(--font-display);
  /* A LABEL, not a second headline. It was the same display face at the
     same weight and nearly the same size as the name below it, so the two
     read as one block in two colours. Smaller, lighter, letterspaced: it
     now reads as the measurement it is, and the name carries the card. */
  font-size: var(--fs-lmk-d); font-weight: 500; letter-spacing: .06em;
  line-height: 1; letter-spacing: -.01em;
  /* INVERTED. The measurement carried the accent and the name carried the
     structural dark, which put the warm colour on the smaller of the two.
     The name is the card, so the name takes the accent. */
  color: var(--pc-navy);
  font-variant-numeric: tabular-nums;
  /* its own direction, so the figure never reorders - and therefore its own
     alignment, set physically, because logical properties inside an element
     that declares `direction` resolve against ITSELF, not the page */
  direction: ltr; unicode-bidi: isolate; text-align: left;
}
[dir="rtl"] .landmark-distance { text-align: right; }
/* The unit is a unit, not part of the figure. At the same size the two read
   as one string; at .38em it reads as the label on a measurement. */
.landmark-distance i {
  font-style: normal;
  font-size: .38em; font-weight: 500;
  letter-spacing: .1em; text-transform: uppercase;
  margin-inline-start: .35em;
  vertical-align: .22em;
}
html[lang="ar"] .landmark-distance i { letter-spacing: 0; text-transform: none; }
/* THE DISTANCE AND THE NAME NEEDED SOMETHING BETWEEN THEM. Two lines of
   different type stacked with nothing in between read as one unfinished
   block. It is the same tapered rule the figures use - the site's own
   divider, sized to the measurement above it rather than to the column. */
/* THE SS7 TRAP, AGAIN. .landmark-distance carries its own `direction: ltr`
   so the figure never reorders - which means every logical property inside
   it, this block's own placement included, resolves against LTR rather than
   against the page. A block box with a definite width sits at its
   containing block's inline start: left, in both languages. In English that
   is under the start of the number; in Arabic the number is on the right
   and the rule was stranded away from it.

   Pushed over physically, scoped to the page's direction - the same fix
   `text-align` on this element already needed. */
.landmark-distance::after {
  content: ''; display: block;
  inline-size: clamp(56px, 4.4vw, 104px);
  margin-block-start: clamp(.7rem, 1.6vh, 1.15rem);
  block-size: 1px; background: currentColor; opacity: .5;
  -webkit-mask: linear-gradient(to right, transparent 0,
    #000 var(--cut-x), #000 calc(100% - var(--cut-x)), transparent 100%);
  mask: linear-gradient(to right, transparent 0,
    #000 var(--cut-x), #000 calc(100% - var(--cut-x)), transparent 100%);
}
/* Latin digits in both languages now, so the number keeps Clash Grotesk.
   Almarai is appended rather than substituted, because the unit beside the
   figure is still Arabic and Clash has no glyph for it. */
[dir="rtl"] .landmark-distance::after { margin-left: auto; }
/* THE BOX TURNS, NOT THE MARKUP. In Arabic the unit belongs after the
   figure, which on a right-to-left line means physically to its LEFT.

   Reordering the markup does NOT do that, and the reason is worth keeping:
   inside an LTR box, Arabic text and a number next to it resolve into ONE
   right-to-left run, so whichever of the two is written first ends up on the
   right. Measured both ways - unit at x799 against a number ending at x802
   either way. The box's own direction is the only thing that moves it.

   Safe for the figure itself: digits are a left-to-right run whatever the
   base direction, so `4.35` cannot reorder. The two rules that were set
   PHYSICALLY because of the old ltr - the text-align and the divider's
   margin-left - both still resolve to the right-hand edge, so neither had
   to move. */
html[lang="ar"] .landmark-distance {
  font-family: 'Clash Grotesk', var(--font-display-ar);
  line-height: 1.12;
  direction: rtl;
}

.landmark-card h3 {
  /* The description is gone, so the name carries the card - but 104px was
     shouting. It is the section's own heading size plus a step, which keeps
     it the largest thing on the card without competing with the section
     heading above it. */
  font-size: var(--fs-lmk);
  font-weight: 600;
  color: var(--clay-ink); margin: 0;
  line-height: 1.08; letter-spacing: -.01em;
  margin-block-start: clamp(.7rem, 1.6vh, 1.15rem);
}
html[lang="ar"] .landmark-card h3 { letter-spacing: 0; line-height: 1.34; }
.landmark-card p {
  margin: 0; line-height: 1.6;
  color: var(--pc-navy-soft); font-size: var(--fs-body);
}

@media (prefers-reduced-motion: reduce) {
  #map-world, .landmark-card,
  .map-mask svg [id^="pin-"], .map-mask svg [id^="route-"] { transition-duration: .01ms !important; }
  /* the observer still drives the map; the cards just never hide */
  .landmark-card { opacity: 1 !important; pointer-events: auto !important; }
}

/* ---------- 17. MASTERPLAN ----------------------------------------------- */
/* Grows from an inset panel to full bleed as it enters the viewport. */

/* One screen, start to finish. .stage's own rhythm is built for sections you
   scroll THROUGH; this one is a thing you look AT, so it takes the viewport
   as its height and centres what is inside. Everything from the eyebrow to
   the button has to land inside 100svh or the plan cannot be read whole. */
.mp { position: relative; }
.stage.mp {
  /* THE FRAME TAKES WHAT IS LEFT, rather than a flat fraction of the screen.
     At 54svh it did not know about the header, the heading, the filter bar or
     the button, so on a short window the five of them came to more than the
     screen: measured 750px against 720, which put the button under the
     WhatsApp float. A row of minmax(0, 1fr) cannot overflow by construction. */
  /* PAST ONE SCREEN, DELIBERATELY. The frame sits in a minmax(0, 1fr) row,
     so it already takes everything the row has and raising its own cap did
     nothing - measured, the cap was 778px while the frame came out 564,
     because the ROW was the constraint. The section itself has to be taller.
     The extra also buys a real floor under the plan, so the gallery below
     does not begin the moment the drawing ends. */
  min-block-size: 118svh;
  display: grid; grid-template-rows: auto minmax(0, 1fr);
  align-content: stretch;
  padding-block: calc(var(--header-h) + clamp(.75rem, 2.5vh, 2rem))
                 clamp(3.5rem, 9vh, 7.5rem);
}
.stage.mp > .shell { margin-block-end: clamp(.4rem, 1vh, .9rem); }
/* This section is a single screen with one object on it, so a .t-h2 at 42px
   sat small against all that air while the 111px below it made the gap read
   as a mistake. The heading steps up toward the display size the other
   full-screen sections use, the air comes down, and the frame takes what
   both give back. */
.stage.mp .opener h2 { max-width: 20ch; }
/* .opener carries --s6 of margin, which is right when a section continues
   below it and far too much when the next thing is the object the heading
   names. 96px of it sat between the two. */
.stage.mp .opener { gap: .35rem; margin-bottom: clamp(.6rem, 1.4vh, 1rem); }

/* One screen, gutters on both sides.
   It used to grow to full bleed as it entered. Full bleed put the drawing
   edge to edge with nothing around it, and the bar, the frame and the way
   into the plan stopped fitting on one screen together - so the section had
   to be scrolled to be read, which is the opposite of what a plan wants.
   Now the panel is capped, the frame is sized off the viewport rather than
   an aspect ratio, and everything lands inside 100svh. */
/* THE GRID AND THE GAP WERE LOST IN A SLICE EDIT. They sat after the closing
   brace of a rule inside the 2200 media block - declarations with no selector
   around them, which the parser drops. So .mp__panel had no `display: grid`
   and no `gap` at ANY width, and the bar, the frame and the button fell
   together in normal flow. That is the spacing the client photographed, and
   it is the failure mode SS8c already records once: count the rules after a
   block replacement, because the braces stay balanced the whole time. */
.mp__panel {
  inline-size: min(1640px, 94%); margin-inline: auto;
  display: grid; grid-template-rows: auto minmax(0, 1fr) auto;
  gap: clamp(.65rem, 1.3vh, 1rem);
  min-block-size: 0;
}
@media (min-width: 1800px) { .mp__panel { inline-size: min(1900px, 95%); } }
@media (min-width: 2200px) { .mp__panel { inline-size: min(2320px, 96%); } }
.mp__frame {
  position: relative; overflow: hidden; border-radius: var(--radius);
  background: var(--ink); color: var(--on-ink);
}
/* Only the homepage panel is sized off the viewport. The masterplan page's
   own frame keeps its inline aspect-ratio - it is the whole page there. */
.mp__panel .mp__frame {
  /* fills its row on a short window, capped and centred on a tall one.
     54svh left the drawing sitting in a wide, shallow box - the plan is the
     one thing this section exists to show, so the cap rises to take whatever
     the row actually has. */
  block-size: 100%; min-block-size: 220px;
  max-block-size: clamp(260px, 72svh, 1040px);
  align-self: center;
}
@media (max-width: 760px) {
  .mp__panel { inline-size: 92%; }
  .mp__panel .mp__frame { block-size: clamp(260px, 46svh, 460px); }
}

/* ---- the filter bar ----
   Modelled on the tool the client is supplying. The chips are real: each is
   a <details> whose <summary> is the pill, so open/close, Escape and
   keyboard focus all come from the browser rather than from us. What they
   cannot do is filter, because there is no plot index behind them yet -
   which is exactly what the status pill on the frame says. */
.mpf {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  color: var(--on-paper); position: relative; z-index: 6;
}
.mpf__seg {
  display: inline-flex; align-items: center; padding: .25rem;
  border: 1px solid var(--mpf-line); border-radius: 3px;
}
.mpf__ic {
  inline-size: 34px; block-size: 34px; display: grid; place-items: center;
  border-radius: 2px; color: var(--on-paper-mute);
}
.mpf__ic.is-on { background: var(--ink); color: var(--on-ink); }

.mpf__drop { position: relative; }
.mpf__pill, .mpf__search {
  display: inline-flex; align-items: center; gap: .5rem;
  block-size: 44px; padding-inline: 1rem;
  border: 1px solid var(--mpf-line); border-radius: 2px;
  font-family: var(--font-latin); font-size: var(--fs-xs); letter-spacing: .02em;
  white-space: nowrap; color: inherit;
}
html[lang="ar"] .mpf__pill, html[lang="ar"] .mpf__search { font-family: var(--font-arabic); }
.mpf__pill { cursor: pointer; list-style: none; user-select: none;
  transition: border-color var(--t-fast), background var(--t-fast); }
.mpf__pill::-webkit-details-marker { display: none; }
.mpf__pill:hover { background: rgba(var(--ink-rgb), .06); }
.mpf__pill:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.mpf__c { opacity: .7; transition: transform var(--t-fast) var(--ease-out); }
.mpf__drop[open] .mpf__c { transform: rotate(180deg); }
/* A chosen value is the chip's own state, so it takes the filled treatment
   the toggle uses rather than a second accent colour. */
.mpf__drop.is-set .mpf__pill { background: var(--ink); color: var(--on-ink); border-color: var(--ink); }

.mpf__menu {
  position: absolute; inset-block-start: calc(100% + .4rem); inset-inline-start: 0;
  z-index: 8; min-inline-size: 100%;
  padding: .35rem; margin: 0; list-style: none;
  background: var(--paper); border: 1px solid var(--mpf-line);
  border-radius: 5px; box-shadow: 0 18px 40px -22px rgba(0, 0, 0, .55);
}
.mpf__opt {
  display: block; inline-size: 100%; text-align: start; white-space: nowrap;
  padding: .6rem .85rem; min-block-size: 40px; border-radius: 5px;
  font-family: inherit; font-size: var(--fs-xs); color: var(--on-paper);
  background: transparent; cursor: pointer;
  transition: background var(--t-fast);
}
.mpf__opt:hover, .mpf__opt:focus-visible { background: rgba(var(--ink-rgb), .09); outline: none; }

.mpf__search {
  flex: 1 1 14rem; min-inline-size: 10rem; color: var(--on-paper-mute);
}
.mpf__search svg { flex: none; opacity: .8; }
/* Below this the row wraps and the search field earns a line of its own. */
@media (max-width: 860px) {
  .mpf { gap: .4rem; }
  .mpf__search { flex-basis: 100%; }
  .mpf__pill { padding-inline: .8rem; }
}

.mp__stage { position: absolute; inset: 0; cursor: grab; touch-action: none; }
.mp__stage.is-drag { cursor: grabbing; }
.mp__stage svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.mp__inner { transform-origin: center; will-change: transform; }

.plan .lot { fill: rgba(var(--clay-rgb), .05); stroke: rgba(var(--clay-rgb), .30); stroke-width: .35; vector-effect: non-scaling-stroke; transition: fill var(--t-fast) linear; }
.plan .lot:hover { fill: rgba(var(--clay-rgb), .22); }
.plan .lot--c { fill: rgba(207,200,192,.07); stroke: rgba(207,200,192,.34); }
.plan .road { stroke: rgba(219,213,205,.24); stroke-width: 1; fill: none; vector-effect: non-scaling-stroke; }
.plan .road--main { stroke: rgba(219,213,205,.45); stroke-width: 2.2; }
.plan .edge { fill: none; stroke: var(--clay); stroke-width: 1.4; vector-effect: non-scaling-stroke; }
.plan .plabel { fill: rgba(219,213,205,.5); font-family: var(--font-latin); font-size: 3px; letter-spacing: .12em; }

.mp__hud {
  position: absolute; inset-inline-start: var(--s3); top: var(--s3); z-index: 4;
  display: flex; gap: .5rem; align-items: center;
}
.mp__btn {
  inline-size: 44px; block-size: 44px; display: grid; place-items: center;
  border: 1px solid var(--hair-ink); border-radius: 50%; color: var(--on-ink);
  background: rgba(18, 18, 18,.5); backdrop-filter: blur(8px);
  transition: background var(--t-fast), color var(--t-fast);
}
.mp__btn:hover { background: var(--paper); color: var(--ink); }
.mp__hint {
  position: absolute; inset-inline-start: 50%; transform: translateX(-50%);
  bottom: var(--s3); z-index: 4;
  font-size: var(--fs-xs); letter-spacing: .08em; color: var(--on-ink-mute);
}
[dir="rtl"] .mp__hint { transform: translateX(50%); }
.mp__badge {
  position: absolute; inset-inline-end: var(--s3); top: var(--s3); z-index: 4;
  font-size: var(--fs-xs); letter-spacing: .14em; text-transform: uppercase;
  color: var(--on-ink-mute); border: 1px solid var(--hair-ink);
  padding: .4rem .8rem; border-radius: 2px; background: rgba(18, 18, 18,.5);
  backdrop-filter: blur(8px);
}
html[lang="ar"] .mp__badge { text-transform: none; letter-spacing: 0; }

/* The honest line, in the corner the eye lands in last. Not decoration:
   it is the only thing on the frame that says the plan is not live yet. */
.mp__status {
  position: absolute; inset-inline-start: var(--s3); bottom: var(--s3); z-index: 4;
  margin: 0; display: inline-flex; align-items: center; gap: .55rem;
  padding: .55rem .95rem; border-radius: 2px;
  border: 1px solid rgba(207, 200, 192, .55);
  background: rgba(18, 18, 18, .62);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  font-size: var(--fs-xs); letter-spacing: .02em; color: var(--on-ink);
}
.mp__dot { inline-size: 7px; block-size: 7px; border-radius: 50%; background: var(--clay); flex: none; }

/* The way into the plan, on its own row under the frame and aligned to the
   end edge. It sat on the frame while the frame was full bleed and there was
   no room beneath it; now the section is one screen, the row costs 46px and
   the button stops covering the drawing. */
.mp__foot { display: flex; justify-content: flex-end; }
.mp__cta {
  padding: .8rem 1.6rem; min-height: 46px;
  font-size: var(--fs-xs); letter-spacing: .06em;
}
/* Below this the status pill and the drag hint cannot share the bottom edge.
   The hint is the less load-bearing of the two. */
@media (max-width: 860px) {
  .mp__hint { display: none; }
}

/* ---------- 18. GALLERY -------------------------------------------------- */

.gal { display: grid; gap: clamp(2rem, 5vw, 5rem); }
@media (min-width: 900px) { .gal { grid-template-columns: repeat(12, 1fr); gap: clamp(1.5rem,3vw,3rem) clamp(1.5rem,3vw,3rem); } }
.g { grid-column: span 1; }
@media (min-width: 900px) {
  .g--full { grid-column: 1 / -1; }
  .g--l    { grid-column: 1 / span 7; }
  .g--r    { grid-column: 8 / span 5; margin-block-start: var(--s8); }
  .g--half { grid-column: span 6; }
  .g--c    { grid-column: 3 / span 8; }
}
.g button { display: block; width: 100%; }
.g .plate img { transition: transform 1200ms var(--ease-out), filter 600ms; }
.g button:hover .plate img, .g button:focus-visible .plate img { transform: scale(1.03); }
.g__zoom {
  position: absolute; inset-block-end: 1rem; inset-inline-end: 1rem;
  inline-size: 44px; block-size: 44px; display: grid; place-items: center;
  background: rgba(230,225,218,.9); color: var(--ink); border-radius: 50%;
  opacity: 0; transform: scale(.86); transition: opacity var(--t-med), transform var(--t-med) var(--ease-out);
}
.g button:hover .g__zoom, .g button:focus-visible .g__zoom { opacity: 1; transform: scale(1); }

.lb {
  position: fixed; inset: 0; z-index: var(--z-menu);
  background: rgba(18, 18, 18,.97); display: grid; grid-template-rows: auto 1fr auto;
  opacity: 0; visibility: hidden;
  transition: opacity var(--t-med) var(--ease-out), visibility 0s linear var(--t-med);
}
.lb.is-open { opacity: 1; visibility: visible; transition: opacity var(--t-med) var(--ease-out), visibility 0s; }
.lb__bar { display: flex; align-items: center; justify-content: space-between; padding: var(--s2) var(--gutter); color: var(--on-ink); }
.lb__count { font-family: var(--font-latin); font-size: var(--fs-xs); letter-spacing: .16em; font-variant-numeric: tabular-nums; }
.lb__stage { position: relative; display: block; padding: var(--s2) var(--gutter); min-height: 0; }
/* ABSOLUTE, BECAUSE A PERCENTAGE HAD NOTHING TO RESOLVE AGAINST. The bottom
   of every enlarged photograph was being cut off, and the cause took four
   tries to pin down.

   The stage is the 1fr row of a grid. A row sized by a fraction is not a
   DEFINITE height as far as a child's percentage is concerned, so the
   picture's `max-height: 100%` was ignored and it fell back to its own
   intrinsic size - measured 954px inside an 868px box, 70px of it under the
   footer. Stretching the item did not help, and neither did swapping the
   percentage for the physical property: both measured 954 again.

   An absolutely positioned element is different: its percentages resolve
   against its containing block's PADDING BOX, which always has a used height
   once the row is laid out. Measured after: 868px, sitting 16px clear of the
   bar and 16px clear of the footer, aspect intact.

   Worth remembering as a rule: when a percentage height is being ignored,
   look at whether an ancestor's height comes from `fr` or `flex` before
   looking at the element itself. */
.lb__stage img {
  position: absolute; inset: var(--s2) var(--gutter);
  inline-size: auto; block-size: auto; margin: auto;
  max-inline-size: calc(100% - 2 * var(--gutter));
  max-block-size: calc(100% - 2 * var(--s2));
  object-fit: contain;
}
.lb__foot { display: flex; align-items: center; justify-content: center; gap: var(--s3); padding: var(--s2) var(--gutter) var(--s4); color: var(--on-ink); }
/* THE ARROWS WERE TWO THIN GREY RINGS ON BLACK, 48px apart in the middle
   of a wide bar - hard to see and harder to aim at. They are solid, 56px,
   and they sit where a thumb and a cursor both expect them: against the
   left and right edges of the stage, vertically centred on the picture.
   The close button keeps the bar to itself. */
.lb__nav {
  inline-size: 56px; block-size: 56px; display: grid; place-items: center;
  border: 0; border-radius: 50%;
  background: rgba(207, 200, 192, .12); color: var(--on-ink);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  cursor: pointer;
  transition: background var(--t-fast) var(--ease-out), transform var(--t-fast) var(--ease-out);
}
.lb__nav:hover { background: var(--paper); color: var(--ink); transform: scale(1.06); }
.lb__nav:focus-visible { outline: 2px solid var(--paper); outline-offset: 3px; }
.lb__nav svg { inline-size: 22px; block-size: 22px; }

/* Out of the foot and onto the stage. The foot keeps the caption, which is
   the only thing in it that is about the picture rather than about moving
   between pictures. */
.lb__prev, .lb__next {
  position: fixed; inset-block-start: 50%; margin-block-start: -28px; z-index: 2;
}
.lb__prev { inset-inline-start: clamp(.75rem, 2.5vw, 2.25rem); }
.lb__next { inset-inline-end: clamp(.75rem, 2.5vw, 2.25rem); }
/* the glyphs point the way the page reads */
[dir="rtl"] .lb__prev svg, [dir="rtl"] .lb__next svg { transform: scaleX(-1); }
.lb__foot { justify-content: center; }
@media (max-width: 560px) {
  .lb__nav { inline-size: 48px; block-size: 48px; }
  .lb__prev, .lb__next { inset-block-start: auto; inset-block-end: clamp(1rem, 4vh, 2rem); margin-block-start: 0; }
}
.lb__cap { font-size: var(--fs-sm); color: var(--on-ink-mute); text-align: center; min-width: 12ch; }

/* ---------- 19. FORM ----------------------------------------------------- */

.form { display: grid; gap: var(--s4); }
.field { display: grid; gap: .4rem; position: relative; }
.field label { font-size: var(--fs-xs); font-weight: 500; letter-spacing: .1em; text-transform: uppercase; color: var(--fg-mute); }
html[lang="ar"] .field label { text-transform: none; letter-spacing: 0; font-size: .8125rem; }
.field .req { color: var(--accent); margin-inline-start: .25em; }

.field input, .field select, .field textarea {
  font: inherit; font-size: var(--fs-body); color: var(--fg);
  background: transparent; border: 0; border-bottom: 1px solid var(--rule);
  padding: .85rem 0; min-height: 52px; border-radius: 0; width: 100%;
  transition: border-color var(--t-med) var(--ease-out);
}
.field textarea { resize: vertical; min-height: 110px; line-height: 1.7; }
.field select { appearance: none; cursor: pointer; }
/* the underline draws in from the inline start on focus */
.field::after {
  content: ''; position: absolute; bottom: 0; inset-inline-start: 0;
  height: 1px; width: 100%; background: var(--accent);
  transform: scaleX(0); transform-origin: var(--o, left);
  transition: transform var(--t-med) var(--ease-inout);
}
[dir="rtl"] .field::after { --o: right; }
.field:focus-within::after { transform: scaleX(1); }
.field input:focus, .field select:focus, .field textarea:focus { outline: none; }
.field input::placeholder, .field textarea::placeholder { color: var(--fg-mute); opacity: .5; }
.field__hint { font-size: var(--fs-xs); color: var(--fg-mute); }
.field__err { font-size: var(--fs-sm); color: #9E1B24; display: none; }
.field.has-err { --rule: #9E1B24; }
.field.has-err .field__err { display: block; }
.field.has-err::after { background: #9E1B24; transform: scaleX(1); }

/* chip-style radio row for plot type */
.chips { display: flex; flex-wrap: wrap; gap: .6rem; }
.chip { position: relative; }
.chip input { position: absolute; opacity: 0; inset: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.chip span {
  display: inline-flex; align-items: center; min-height: 44px; padding: .55rem 1.15rem;
  border: 1px solid var(--rule); border-radius: 2px;
  font-size: var(--fs-sm); color: var(--fg-mute);
  transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast);
}
.chip input:checked + span { border-color: var(--ink); background: var(--ink); color: var(--paper); }
.chip input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 3px; }

.form__summary { display: none; padding: var(--s3); border: 1px solid #9E1B24; border-radius: 3px; color: #7A141B; font-size: var(--fs-sm); }
.form__summary.is-on { display: block; }
.form__summary ul { display: grid; gap: .35rem; margin-top: .5rem; }
.form__summary a { text-decoration: underline; }
.form__status { font-size: var(--fs-sm); min-height: 1.6em; }
.form__status[data-state="ok"]  { color: var(--accent); }
.form__status[data-state="err"] { color: #9E1B24; }
@media (min-width: 720px) { .form__row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s4); } }

.cinfo { display: grid; gap: 0; align-content: start; }
.cinfo__row { position: relative; display: grid; gap: .3rem; padding-block: var(--s4); border-top: 0; }
.cinfo__row:last-child { border-bottom: 1px solid var(--rule); }
.cinfo__k { font-family: var(--font-latin); font-size: var(--fs-xs); letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--fg-mute); }
html[lang="ar"] .cinfo__k { font-family: var(--font-arabic); text-transform: none; letter-spacing: 0; font-size: .8125rem; }
.cinfo__v { font-size: var(--fs-h3); font-weight: 500; }
.cinfo__v a { display: inline-flex; align-items: center; min-height: 44px; }

/* ---------- 19c. THE RING -----------------------------------------------
   Twelve landscape cards on a cylinder, 30 degrees apart. Replaces the
   horizontal rail on the homepage; the rail itself stays, because the
   gallery page still uses it.

   ONE UNIT, AND EVERYTHING IS A MULTIPLE OF IT. The client's note was that
   it must look the same at every screen size - same gaps, same arc, same
   perspective. That only holds if the card, the cylinder's radius and the
   perspective distance all scale together: a 3D projection is similar only
   when the RATIOS between those three are constant, so fixing the radius in
   pixels while the card grew with the viewport would have changed the gaps
   at every width.

   So --ru is a share of the viewport and the other three are multiples of
   it, taken from the numbers the client settled on at 1538px wide:

       card  25vw         -> 384.5px      --ru
       radius 490px       -> 1.2744 x --ru
       persp  500px       -> 1.3004 x --ru
       height 24vw        -> 0.96   x --ru

   The whole composition is then one scale of the same picture at any width:
   the same number of cards visible, the same gaps, the same curve.         */
/* The flag field continues out of the masterplan above, which shares this
   ground - two sections of the same Sandstone with the pattern stopping dead
   at the boundary read as two surfaces rather than one. */
.ring-sec {
  position: relative; isolation: isolate; overflow: hidden;
  /* SANDSTONE, like the masterplan above it. On the dark ground the section
     read as a third full-bleed dark stage within two screens of the
     infrastructure one, and the boundary between it and the masterplan was a
     hard edge rather than a continuation. */
  background: var(--beige); color: var(--on-paper);
  /* NO --head OVERRIDE. The root already sets it to the structural dark
     (#002426), which is what every other heading on a light ground takes -
     pointing it at --on-paper (#3B2412) made this one heading browner and
     lighter than the masterplan's directly above it. */
  /* AIR. It was running edge to edge: the arc began where the section began,
     so the pictures were cut by the boundary instead of sitting inside it. */
  /* 13vh WAS 161px OF A 620px SCREEN before anything was drawn. 9vh is 112,
     and the top is additionally held to the header's height - the header is
     fixed and paints over this section, so padding smaller than it puts the
     heading behind the bar, which is the first thing the client's screenshot
     shows. */
  padding-block: max(var(--header-h), clamp(2.5rem, 9vh, 11rem))
                 clamp(2.5rem, 9vh, 11rem);

  /* 42vw, not 25. The RATIOS are the client's and do not move - the curve is
     the shape he approved - but the whole cylinder is a bigger share of the
     screen now, which is the only lever that changes how many cards fit.

     Worked out rather than nudged. A card at angle t projects to
         x = R sin t * P / (P + R cos t)
     and is on screen while |x| < V/2. At 25vw the pair at +/-90 landed at
     0.319V, inside the edge, so seven were showing. At 42vw they land at
     0.535V - just outside - which leaves five whole cards and those two as
     slivers at the margins: six, as asked. The centre card goes from 12.6%
     of the screen's width to 21.2%.

     THE BOX IS LANDSCAPE NOW. It was .96 of the card's width - near square -
     so a 16:9 photograph was being cropped to almost a square and the cards
     at the sides, foreshortened, read as portraits. At .62 the box is 16:10
     and the pictures keep their shape. */
  /* THE SECTION HAS TO FIT THE WINDOW IT IS IN, and it did not: the arc was
     sized in `vw` alone, so a short wide window - a laptop carrying a tab
     bar, a bookmarks bar and a taskbar - got the same 376px arc as a tall
     one and the section came to 721px in a 620px viewport. Measured. The
     heading went behind the header and the button sat on the bottom edge.

     The cap is solved from the measurement, and the first attempt at it was
     wrong in a way worth recording: I assumed the padding sat on its 2.5rem
     floor at 620px tall, when 9vh is 56 and the floor only binds below 444.
     A clamp's floor is not where it is - read the computed value.

     What is not the arc: the header's 88px of top padding, 9vh of bottom,
     the heading and its margin (84), and the hint plus the button (85).
     So the arc may have `100svh - 0.09svh - 257`, and the arc is .70 of
     --ru, which gives --ru <= 1.3 x svh - 367. Below 180px the cards stop
     being photographs, so that is the floor.

     At 800px tall this resolves to 673px, above the 537px that 42vw gives
     at 1280 - so a normal window is untouched. It only bites where it was
     broken. */
  --ru: max(180px, min(42vw, calc(130svh - 367px)));
  --ring-radius: calc(var(--ru) * 1.2744);
  --ring-persp:  calc(var(--ru) * 1.3004);
  /* .70, FROM .62. Fifteen cards at 24 degrees put a card nearer the
     camera pole more often than twelve at 30 did, and the nearest two
     project to 803px against a 605px layout width - measured, they spill
     176px above and below a .62 box. A taller window takes the ragged
     edge off the ones you actually read. */
  --ring-h:      calc(var(--ru) * .70);
  --ring-step: 30deg;
}
/* A PHONE IS THE ONE PLACE PROPORTION HAS TO GIVE. At 25vw a card is 94px on
   a 375px screen - the composition would be perfectly faithful and far too
   small to look at. Scaling the unit up keeps the shape and the gaps exactly
   as they are and simply shows less of the arc. */
/* The same ceiling, or a handset held sideways repeats the fault: 64vw of
   an 812px-wide landscape screen is a 520px arc in 375px of height. */
@media (max-width: 860px) { .ring-sec { --ru: max(180px, min(64vw, calc(130svh - 367px))); } }
@media (max-width: 560px) { .ring-sec { --ru: max(180px, min(84vw, calc(130svh - 367px))); } }

/* THE HEADING TAKES THE MIDDLE and the action goes under the pictures, where
   "see the rest of them" belongs. The two were sharing one row, which left
   the button crammed against the heading and fighting it for width. */
/* +5px down on the heading, and -10px on the foot below. The client's own
   numbers, kept as literal pixels so they read back as what was asked. */
.ring-sec__head {
  padding-block-start: 5px;
  inline-size: 100%; max-inline-size: var(--maxw); margin-inline: auto;
  padding-inline: var(--gutter); text-align: center;
  margin-block-end: clamp(2.25rem, 5.5vh, 4rem);
}
.ring-sec__head h2 { margin: 0; }
.ring-sec__foot {
  /* A COLUMN SINCE THE HINT ARRIVED. It was a one-item row, so a second
     child simply sat down beside the button and the hint's own bottom
     margin - a block margin on a flex ITEM in a row - did nothing.
     align-items, not justify-content, is what centres them now. */
  display: flex; flex-direction: column; align-items: center;
  /* Up against the ring. The gap was built when the arc ended well inside
     its box; now that the box is the frame, the two belong together. */
  margin-block-start: calc(clamp(.9rem, 1.8vh, 1.6rem) - 10px);
  padding-inline: var(--gutter);
}

/* The viewport carries the perspective; the ring carries the 3D. */
.ring-vp {
  position: relative;
  perspective: var(--ring-persp);
  block-size: var(--ring-h);
  /* THE ARC IS A WINDOW, NOT A SPILL. Without a limit here the two cards at
     the near pole are drawn 176px past the box at both ends: over the hint
     and the button below, and under the heading above.

     BUT A HARD CLIP IS THE WRONG LIMIT, and the client is right about why.
     `overflow: hidden` alone cuts the card AND the shadow it casts at the
     same straight line, and a shadow with a razor edge reads as a rendering
     fault - it is the one thing on a page that must never have an edge.

     So the box still bounds the arc, and a mask feathers the last stretch of
     it at the top and the bottom. The cards that reach the edge fade out of
     it instead of being sliced; the ones that do not reach it are untouched,
     which is most of them.

     The mask is on THIS element, which carries the perspective, not on the
     `.ring` inside it, which carries `preserve-3d`. Masking the element that
     holds preserve-3d would flatten the cylinder; masking the one that only
     projects it composites the finished 3D output and leaves it alone. */
  overflow: hidden;
  /* ASYMMETRIC, because the two edges are not doing the same job. The top
     edge of a card is a drawn edge - part of the picture - so it gets the
     smallest fade that still takes the razor off. The bottom is where the
     shadow is, and a shadow needs room to end. */
  --ring-fade-t: clamp(10px, 4%, 26px);
  --ring-fade-b: clamp(22px, 11%, 64px);
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--ring-fade-t),
    #000 calc(100% - var(--ring-fade-b)), transparent 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--ring-fade-t),
    #000 calc(100% - var(--ring-fade-b)), transparent 100%);
  touch-action: pan-y;          /* the page still scrolls through it */
  cursor: grab;
}
.ring-vp.is-drag { cursor: grabbing; }
.ring {
  position: absolute; inset: 0; margin: auto;
  inline-size: var(--ru); block-size: 100%;
  transform-style: preserve-3d;
  transform: rotateY(var(--spin, 0deg));
  will-change: transform;
}
/* Every card sits at a FIXED angle and never moves on its own. Only the ring
   turns, so the section costs one transform on one element per frame. */
.ring__card {
  position: absolute; inset-block: 0; inset-inline-start: 0;
  inline-size: var(--ru);
  transform: rotateY(calc(var(--i) * var(--ring-step)))
             translateZ(calc(var(--ring-radius) * -1));
  backface-visibility: hidden;   /* the far half is turned away */
}
.ring__card button {
  display: block; inline-size: 100%; block-size: 100%;
  padding: 0; border: 0; background: none; cursor: zoom-in;
  position: relative; overflow: hidden; border-radius: 5px;
  /* THE SHADOW IS CORRECT AND IT STAYS. It was taken off once, to chase a
     black line along the top and bottom of the arc, and that was the wrong
     suspect: the line is a band of pure black MATTED INTO THE SOURCE
     RENDERS - twelve of the thirteen carry one, up to 160px on 12.jpg -
     and it is cut at the source now, in optimise-renders.ps1.

     What the shadow does do is scale with its card. A box-shadow is painted
     in the element's own plane and projected with it, and these cards sit
     on a cylinder of radius 803px seen through an 819px perspective, so the
     card nearest the camera is 16px from it and is drawn at 50.3x. That is
     real, and it is why the band looked worse with the shadow on than off.
     It is not a reason to remove it; with the matte gone there is no hard
     line for the soft one to sit against, and the depth is what the client
     wants. If it ever has to be reduced, the lever is the RADIUS-TO-
     PERSPECTIVE RATIO (the client's own 490/500), not the blur. */
  box-shadow: 0 20px 36px rgba(18, 18, 18, .26), 0 3px 8px rgba(18, 18, 18, .14);
}
.ring__card picture { display: block; block-size: 100%; }
.ring__card img { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }
.ring__card button:focus-visible { outline: 3px solid var(--clay); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .ring { will-change: auto; }
}

/* ---------- 20. FOOTER --------------------------------------------------
   Panna beige ground, deep blue type, zen used once - on the rules that
   separate the strip, and nowhere else. The closing call lives in the
   register section above, so the footer's only job is to be findable.

   On a large screen it is fixed behind the page and the content slides up
   off it. That only works if nothing above can see through: `main` takes an
   opaque ground and a layer of its own, which is what keeps the footer from
   appearing behind a section that has no background of its own.         */

.ftr {
  --ftr-h: min(68svh, 620px);
  position: relative; overflow: hidden; isolation: isolate;
  background-color: var(--beige);
  color: var(--ink-800);
  --fg: var(--ink-800);
  /* back to .76: that compensation was for a full-bleed frond. As a corner
     accent at .22 it darkens the ground by 12% at its very densest, and the
     muted tone still measures 4.7:1 there. */
  --fg-mute: rgba(var(--ink-rgb), .76);
  --rule: rgba(var(--ink-rgb), .14);
}
.ftr__in {
  position: relative; z-index: 1;
  block-size: 100%;
  display: grid; grid-template-rows: 1fr auto; align-content: center;
  padding-block: clamp(2rem, 5vh, 3.5rem) clamp(1.25rem, 3vh, 2rem);
}

/* ---- the backdrop: a date-palm frond, cast as a shadow ------------------
   A corner accent, not a field. It sits in the bottom corner on the reading
   edge - the same place and the same treatment the oasis painting had - and
   it is masked into that corner so it dissolves long before it reaches the
   columns. Low enough to read as light through a leaf rather than as a
   picture of one: it should be felt, not looked at.

   `multiply` means it darkens the footer's own ground where it falls and
   leaves it untouched everywhere else, so no second colour is introduced.
   The artwork is pre-flipped so its mass lands in the lower-left, which
   keeps background-position honest; RTL just mirrors the whole layer.    */
.ftr::before {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image: url("../img/texture/ftr-frond-1200.webp");
  background-image: image-set(
    url("../img/texture/ftr-frond-1200.avif") type("image/avif"),
    url("../img/texture/ftr-frond-1200.webp") type("image/webp"));
  background-position: 0% 100%;
  background-size: 42%;
  background-repeat: no-repeat;
  -webkit-mask-image: radial-gradient(78% 95% at 0% 100%, #000 0 26%, transparent 76%);
          mask-image: radial-gradient(78% 95% at 0% 100%, #000 0 26%, transparent 76%);
  mix-blend-mode: multiply;
  opacity: .22;
}
/* the corner follows the reading direction */
[dir="rtl"] .ftr::before { transform: scaleX(-1); }

/* The field sits under the frond, not beside it: the frond is a cast shadow
   and wants clean ground to fall on, so the pattern fades out before the
   corner it occupies. .028 is the lowest setting on the site - at .04 the
   triangles started competing with the lockup. */
.pfield--ftr {
  z-index: -1;
  --pat-op: .028;
  --pat-w: 118px;
  --pat-fade: linear-gradient(to bottom,
    transparent 0%, rgba(0,0,0,.6) 12%, #000 26%, rgba(0,0,0,.45) 52%, transparent 78%);
}

@media (max-width: 719px) {
  .ftr::before {
    background-size: 78%;
    background-image: url("../img/texture/ftr-frond-800.webp");
    background-image: image-set(
      url("../img/texture/ftr-frond-800.avif") type("image/avif"),
      url("../img/texture/ftr-frond-800.webp") type("image/webp"));
  }
}


/* --- the reveal ------------------------------------------------------- */
@media (min-width: 860px) and (min-height: 620px) {
  .ftr {
    position: fixed; inset-inline: 0; bottom: 0; z-index: 0;
    block-size: var(--ftr-h);
  }
  /* The lid: opaque, and one layer above the fixed footer. The min-height
     matters as much as the background - a page whose content is shorter
     than the screen would otherwise leave the bottom of the viewport
     uncovered at scroll 0, and the footer would show through there. */
  main {
    position: relative; z-index: 1;
    min-block-size: 100svh;
    background: var(--bg);
    margin-bottom: min(68svh, 620px);
  }
}

/* --- the four columns ------------------------------------------------- */
.ftr__grid {
  display: grid;
  gap: clamp(2rem, 4vw, 3.5rem) clamp(2rem, 4.5vw, 5rem);
  align-content: center;
}
@media (min-width: 720px) { .ftr__grid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1060px) {
  .ftr__grid { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, .75fr); }
}

/* NO RULE UNDER IT. The heading earns its place on weight, case and space,
   which is the quieter answer and the one the client picked.

   What it must not go back to is .8125rem - a hard-coded rem that stayed at
   13px while --fs-xs climbed to 16, so it fell behind at every tier and
   shrank at every zoom level. That is the SS8e mistake, made once more in
   the one place nobody re-measured.

   The separation is SPACE, and it has to be bigger than the gap between two
   list rows or the heading reads as one of them. It is roughly twice the row
   gap now. */
.ftr h2 {
  font-family: var(--font-latin); font-size: var(--fs-xs); font-weight: 600;
  letter-spacing: var(--tr-label); text-transform: uppercase;
  line-height: 1.3;
  color: var(--fg-mute);
  margin-block: 0 clamp(1.15rem, 2.4vh, 1.9rem);
}
/* ARABIC HAS NEITHER CASE NOR LETTERSPACING to mark a label with, so it gets
   the one device it does have: weight. 700 is Almarai's real bold - the face
   has no 600, so asking for 600 here would silently render 400 and the
   heading would have nothing at all. */
html[lang="ar"] .ftr h2 {
  font-family: var(--font-arabic); text-transform: none; letter-spacing: .01em;
  font-size: calc(var(--fs-xs) + 1px); font-weight: 700; line-height: 1.45;
}

/* --- identity --------------------------------------------------------- */
.ftr__id { display: grid; gap: var(--s3); align-content: start; }
.ftr__mark {
  display: block; inline-size: clamp(150px, 15vw, 210px); height: auto;
  aspect-ratio: 155.53 / 59.41; color: var(--ink-800);
}
.hdr__lockup {
  /* the wordmark-plus-icon horizontal variant, per the identity profile */
  aspect-ratio: 147.69 / 48.63;
}
.hero__mark, .loader__mark { height: auto; aspect-ratio: 83.03 / 81.69; }
.ftr__tag { font-size: var(--fs-sm); color: var(--fg-mute); max-width: 26ch; }

.ftr__social { display: flex; gap: .6rem; flex-wrap: wrap; }
/* `.ftr a` and `.ftr a:hover` are declared after this block at the same
   specificity, so without the extra class the glyph took the muted link
   tone at rest and deep-blue-on-deep-blue on hover - it disappeared. */
.ftr .ftr__social a {
  display: grid; place-items: center;
  inline-size: 42px; block-size: 42px;
  border: 1px solid var(--rule); border-radius: 50%;
  color: var(--ink-800);
  transition: background var(--t-med) var(--ease-out),
              border-color var(--t-med) var(--ease-out),
              color var(--t-med) var(--ease-out);
}
.ftr__social svg { inline-size: 18px; block-size: 18px; }
.ftr .ftr__social a:hover, .ftr .ftr__social a:focus-visible {
  background: var(--ink-800); border-color: var(--ink-800); color: var(--beige);
  transform: translateY(-2px);
}
.ftr .ftr__social a { transition: background var(--t-med) var(--ease-out),
  border-color var(--t-med) var(--ease-out), color var(--t-med) var(--ease-out),
  transform var(--t-med) var(--ease-lux); }

/* --- contact ---------------------------------------------------------- */
.ftr__lines { display: grid; gap: 0; }
/* ONE ROW HEIGHT ACROSS THE WHOLE FOOTER. Contact rows were 40px and nav
   rows 36, so the columns never shared a baseline and the block read as
   three lists standing next to each other rather than one footer. */
:root { --ftr-row: clamp(38px, 2.3vw, 50px); }
.ftr__line {
  display: flex; align-items: center; gap: .7rem;
  min-height: var(--ftr-row); font-size: var(--fs-sm);
  color: var(--ink-800);
}
.ftr__line-i { inline-size: 17px; block-size: 17px; flex: none; opacity: .5; }

/* --- location --------------------------------------------------------- */
.ftr__addr { font-size: var(--fs-sm); color: var(--fg-mute); max-width: 26ch; line-height: 1.65; }
.ftr__coords {
  margin-top: .5rem; font-family: var(--font-latin); font-size: var(--fs-xs);
  color: var(--fg-mute); font-variant-numeric: tabular-nums;
}
.ftr__maplink {
  display: inline-flex; align-items: center; gap: .55em;
  min-height: var(--ftr-row); margin-top: .5rem; font-size: var(--fs-sm);
  color: var(--ink-800);
}
.ftr__maplink svg { inline-size: 1.15em; block-size: 1.15em; flex: none; opacity: .62; }
/* the pin is symmetrical about its own axis, so it does NOT mirror */

/* ---- the one hover ----
   A hairline drawn from the reading edge, the same gesture the rail rules
   and the map routes make. It is a scaleX on a pseudo-element: transform
   only, one composited layer, nothing laid out again. What it replaces was
   an animated `gap` on three different elements - a layout property, which
   SS5 bans, and which read as the icon shoving its own label sideways. */
.ftr__ul,
.ftr__nav a,
.ftr__maplink { position: relative; }
.ftr__line span:not([class]),
.ftr__nav a,
.ftr__maplink { --uh: 1px; }
.ftr__line span:not([class])::after,
.ftr__nav a::after,
.ftr__maplink::after {
  content: ''; position: absolute; inset-block-end: -2px; inset-inline: 0;
  block-size: var(--uh); background: currentColor; opacity: .55;
  transform: scaleX(0); transform-origin: left center;
  transition: transform 520ms var(--ease-lux);
}
[dir="rtl"] .ftr__line span:not([class])::after,
[dir="rtl"] .ftr__nav a::after,
[dir="rtl"] .ftr__maplink::after { transform-origin: right center; }
.ftr__line span:not([class]) { position: relative; }
.ftr__line:hover span:not([class])::after,
.ftr__nav a:hover::after,
.ftr__nav a:focus-visible::after,
.ftr__maplink:hover::after,
.ftr__maplink:focus-visible::after { transform: scaleX(1); }

/* --- nav -------------------------------------------------------------- */
.ftr__nav ul { display: grid; gap: 0; }
/* LINKS LOOK LIKE LINKS. Every one of them was --fg-mute, the same tone as
   the address and the strapline beside them, so nothing in this footer said
   which words could be pressed. They take the full ink and one step of
   weight; the prose that is only prose stays muted and regular. */
.ftr a { color: var(--ink-800); font-weight: 500; transition: color var(--t-med) var(--ease-out); }
.ftr__tag, .ftr__addr, .ftr__creds span { font-weight: 400; }
.ftr__nav a { display: inline-flex; align-items: center; min-height: var(--ftr-row); font-size: var(--fs-sm); }
/* .ftr a sets the muted link tone and outranks these on specificity - the
   contact rows and the map link are the point of this footer, so they take
   the full deep blue back. */
.ftr a:hover { color: var(--ink-800); }

/* --- credentials + legal, one strip ----------------------------------- */
.ftr__strip {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4);
  justify-content: space-between; align-items: baseline;
  margin-top: clamp(2rem, 5vh, 3.25rem); padding-top: clamp(1.1rem, 2.4vh, 1.75rem);
  /* the one place zen appears down here */
  border-top: 1px solid color-mix(in oklab, var(--clay) 55%, transparent);
  font-size: var(--fs-xs);
}
/* TWO SIDES: what the site is obliged to say, and what it has to link to.
   `space-between` resolves against the page's own direction, so English puts
   the legal block on the left and the documents on the right, and Arabic
   swaps both without a rule - which is the convention in each language. */
.ftr__strip-main {
  display: flex; flex-wrap: wrap; gap: .5rem var(--s4); align-items: baseline;
}
.ftr__docs ul { display: flex; flex-wrap: wrap; gap: .5rem var(--s4); }
.ftr__docs a {
  color: var(--fg-mute);
  transition: color var(--t-med) var(--ease-out);
}
.ftr__docs a:hover, .ftr__docs a:focus-visible { color: var(--ink-800); }

.ftr__creds { display: flex; flex-wrap: wrap; gap: .5rem var(--s5); }
/* The label and its value ran together at .5em. They are a pair, so they
   stay on one line - but with a gap wide enough to read as two things. */
.ftr__creds li { display: flex; gap: .85em; align-items: baseline; }
/* THE DEVELOPER'S OWN MARK, between the label and the name.
   Sized in `em` so it tracks the line it sits in at every tier rather than
   needing a value per breakpoint, and aligned by `align-self: center`: the
   row is baseline-aligned for its text, and a logo has no baseline to align
   to - left on `baseline` it hangs below the line. */
.ftr__zood {
  align-self: center;
  block-size: 1.45em; inline-size: calc(1.45em * 189.94 / 148.85);
  color: var(--ink-800);
  flex: none;
}
/* the only text that crosses the darkest part of the painting */
.ftr__creds span { color: rgba(var(--ink-rgb), .9); }
.ftr__creds b { font-weight: 500; color: var(--ink-800); }
.ftr__legal { display: flex; flex-wrap: wrap; gap: .5rem var(--s3); color: var(--fg-mute); }
.ftr__legal a { color: var(--ink-800); border-bottom: 1px solid var(--rule); }
.ftr__legal a:hover { border-color: var(--ink-800); }

@media (max-width: 719px) {
  .ftr__strip { flex-direction: column; align-items: flex-start; }
  /* one column of content is unavoidably tall on a phone; the page list is
     four short words, so it pairs up rather than running down the screen */
  .ftr__grid { gap: 1.6rem; }
  .ftr__nav ul { grid-template-columns: 1fr 1fr; column-gap: var(--s3); }
  .ftr__in { padding-block: var(--s5) var(--s4); }
}

/* ---------- 20a. THE SINGLE BANNER --------------------------------------
   The mark is ten banners united into one symbol. The site uses the SINGLE
   banner as its punctuation - the one where the mark uses the many - so the
   two are the same idea at two scales rather than the same drawing twice.

   It is banner 5, lifted straight out of the icon (the upright one at the
   top of the wheel, viewBox measured with getBBox), so the angles and the
   staff are the logo's own, not a redraw. It marks every section eyebrow on
   every page, which is the only place it appears.

   A mask on ::before, so it takes the palette's colour, needs no markup and
   comes out in one rule. It mirrors with the reading direction: the banner
   always flies away from the words. */
.label::before {
  content: ''; display: inline-block;
  inline-size: .68em; block-size: .98em;
  margin-inline-end: .6em; vertical-align: -.12em;
  background: currentColor; opacity: .85;
  -webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='109.92 36.85 19.32 27.63'><path d='M110.36,50.93l18.88,13.55-10.78-9.44,8.93-7.33-2.64-10.86-12.38,12.87-.64-.56c-.49-.38-1.2-.29-1.58.21-.38.49-.29,1.2.21,1.58Z' fill='%23fff'/></svg>"); mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='109.92 36.85 19.32 27.63'><path d='M110.36,50.93l18.88,13.55-10.78-9.44,8.93-7.33-2.64-10.86-12.38,12.87-.64-.56c-.49-.38-1.2-.29-1.58.21-.38.49-.29,1.2.21,1.58Z' fill='%23fff'/></svg>");
  -webkit-mask-size: contain; mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
}
/* THE BANNER DOES NOT MIRROR. It used to flip with the reading direction, on
   the argument that it should always fly away from the words. The mark
   settles it, exactly as it settled the map pins: in the logo the banner
   flies RIGHT, so one that flies left in Arabic is the brand's own shape
   backwards. Right in both languages. */

/* ---------- 20c. THE WHEEL -----------------------------------------------
   The mark used once as a ground, turning with the scroll. Scrubbed by the
   Wheel module off a data-wheel attribute that carries the degrees. Rotation
   only - no size, no filter, no repaint beyond the layer itself. */
/* The wheel is wider than the viewport on purpose, so the section has to
   clip it. `clip`, not `hidden`: hidden would make this a scroll container
   and kill the sticky panel inside it. */
/* The wheel was built for a single pinned screen and is gone with it:
   150vw of mark hanging behind three tall scrolling rows sat in the
   wrong place at every scroll position. The flag field is the
   section's ground now. */

/* ---------- 21. WHATSAPP ------------------------------------------------- */

/* The brand green in every state. It was Midnight Teal at rest and only
   turned green on hover, which reads as a generic round button until you
   touch it - and WhatsApp's green is the whole point of the mark. Hover is
   a scale now, nothing else: the colour has a job and should not move. */
.wa {
  position: fixed; z-index: var(--z-float);
  inset-inline-end: clamp(1rem, 3vw, 2rem); inset-block-end: clamp(1rem, 3vw, 2rem);
  inline-size: 64px; block-size: 64px; display: grid; place-items: center;
  background: #1FA855; color: #FFFFFF; border-radius: 50%;
  box-shadow: 0 12px 38px rgba(18, 18, 18, .32);
  transform: translateY(140%); opacity: 0;
  transition: transform 560ms var(--ease-lux), opacity 400ms var(--ease-out);
}
.wa.is-in { transform: translateY(0); opacity: 1; }
.wa.is-in:hover, .wa.is-in:focus-visible { transform: translateY(0) scale(1.08); }
.wa.is-in:active { transform: translateY(0) scale(.96); }
/* 40 in 64 is 62% - the proportion the official mark uses on its own round
   badge. It was 38 in 78, and the glyph's own padding made the drawn mark
   only 40% of the circle, which is what read as a small icon floating in a
   big green disc. */
.wa svg { inline-size: 40px; block-size: 40px; }
@media (prefers-reduced-motion: reduce) { .wa, .wa.is-in:hover { transition: none; transform: translateY(0); } }

/* ---------- 22. UTILITIES ------------------------------------------------ */

.stack   { display: grid; gap: var(--s3); }
.stack-4 { display: grid; gap: var(--s4); }
.stack-5 { display: grid; gap: var(--s5); }
.row     { display: flex; flex-wrap: wrap; gap: var(--s3); align-items: center; }
.center  { text-align: center; justify-items: center; }
.mt-4 { margin-top: var(--s4); } .mt-5 { margin-top: var(--s5); } .mt-6 { margin-top: var(--s6); }

/* ---------- 23. REDUCED MOTION ------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-duration: .001ms !important; scroll-behavior: auto !important;
  }
  [data-fx] { opacity: 1 !important; transform: none !important; clip-path: none !important; }
  [data-fx] .sp-word, [data-fx] .sp-char { opacity: 1 !important; transform: none !important; filter: none !important; }
  .fx-draw path, .fx-draw line, .fx-draw circle, .fx-draw polyline { stroke-dashoffset: 0 !important; }
  .net { opacity: 1 !important; }
  .menu__link { opacity: 1 !important; transform: none !important; }
  .wa, .hdr { opacity: 1 !important; transform: none !important; visibility: visible !important; }
  .hero__film { -webkit-mask-image: none !important; mask-image: none !important; opacity: 1 !important; }
  .hero__veil { opacity: .55 !important; }
  .hero__scroll, .hero__scroll-wheel { animation: none; opacity: .7; }
  /* The headline stays put — hiding it here would remove the page's only
     h1 for reduced-motion visitors. The caption just joins it immediately
     rather than waiting on a scroll-linked delay that won't otherwise fire. */
  .hero__caption {
    opacity: 1 !important; visibility: visible !important;
    transform: translateY(-50%) !important;
  }
  .mp__frame { width: 100% !important; }
}

/* ---------- 24. HORIZONTAL GALLERY --------------------------------------
   A wall of prints that travels sideways as the page scrolls down. Vintage
   comes from the mount, not a filter: each frame sits inside a deep paper
   border with a hairline rule, a stamped index, and a caption set like a
   contact-sheet annotation. CSS sticky pins it; ScrollTrigger only scrubs
   the horizontal travel — pinning with both fights itself.               */

.hgal { position: relative; background: var(--paper); }
/* IT WAS STICKING AT top: 0, so the whole stage sat BEHIND the fixed
   header and the section's own heading and its button were cut off by the
   bar. It starts below the header and is only as tall as what is left -
   the same correction the infrastructure frame needed. */
.hgal__sticky {
  position: sticky; top: var(--header-h);
  height: calc(100svh - var(--header-h));
  display: grid; grid-template-rows: 1fr auto; align-content: center;
  overflow: hidden;
}
.hgal__rail { display: flex; align-items: center; overflow: hidden; }
.hgal__track {
  display: flex; align-items: flex-end;
  gap: clamp(1.5rem, 3.5vw, 4rem);
  padding-inline: var(--gutter);
  will-change: transform;
}

.hgal__item {
  flex: 0 0 auto;
  inline-size: clamp(15rem, 34vw, 30rem);
  display: grid; gap: .9rem;
  position: relative;
}
/* every third print drops, so the row reads as a hung wall not a conveyor */
.hgal__item:nth-child(3n+2) { align-self: center; }
.hgal__item:nth-child(3n)   { inline-size: clamp(12rem, 25vw, 22rem); }

.hgal__no {
  font-family: var(--font-latin); font-size: var(--fs-xs);
  letter-spacing: var(--tr-label); color: var(--fg-mute); opacity: .7;
}

/* the mount — a deep paper border with a hairline inside it */
.hgal__item button {
  display: block; inline-size: 100%;
  padding: clamp(.6rem, 1.1vw, 1rem);
  background: var(--paper-2);
  box-shadow: 0 1px 0 rgba(var(--ink-rgb), .06), 0 26px 44px -32px rgba(var(--ink-rgb), .55);
}
.hgal__item .plate {
  /* the mount is a <span> inside the button, and `overflow`, `border-radius`
     and `aspect-ratio` all silently do nothing on an inline box — without
     this the frames keep square corners and the hover scale spills past
     the paper border */
  display: block;
  outline: 1px solid rgba(var(--ink-rgb), .14); outline-offset: -1px;
  /* a portrait print at 34vw is taller than a short laptop screen; the cap
     crops through object-fit rather than overflowing the rail */
  max-block-size: min(52vh, var(--plate-cap, 30rem));
}
.hgal__item .plate img {
  filter: saturate(.9) contrast(1.02);
  transition: transform 1400ms var(--ease-out), filter 900ms var(--ease-out);
}
.hgal__item button:hover .plate img,
.hgal__item button:focus-visible .plate img { transform: scale(1.035); filter: saturate(1) contrast(1.04); }

.hgal__item .cap {
  display: flex; justify-content: space-between; gap: var(--s2);
  font-family: var(--font-latin); font-size: var(--fs-xs);
  letter-spacing: .08em; text-transform: uppercase; color: var(--fg-mute);
}
html[lang="ar"] .hgal__item .cap { font-family: var(--font-arabic); letter-spacing: 0; text-transform: none; }

.hgal__hint {
  display: flex; align-items: center; justify-content: center; gap: .8em;
  padding-block: clamp(.5rem, 1.4vh, 1.25rem);
  font-family: var(--font-latin); font-size: var(--fs-xs);
  letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--fg-mute);
}
html[lang="ar"] .hgal__hint { font-family: var(--font-arabic); letter-spacing: 0; text-transform: none; }
.hgal__hint-i {
  inline-size: 46px; block-size: 1px; background: currentColor; opacity: .5;
  animation: hgal-run 2.6s var(--ease-inout) infinite;
}
@keyframes hgal-run { 0%,100% { transform: scaleX(.4); transform-origin: left; } 50% { transform: scaleX(1); transform-origin: left; } }
[dir="rtl"] .hgal__hint-i { animation-name: hgal-run-rtl; }
@keyframes hgal-run-rtl { 0%,100% { transform: scaleX(.4); transform-origin: right; } 50% { transform: scaleX(1); transform-origin: right; } }

/* The homepage variant keeps the section heading on screen for the whole
   sideways run — on the way to the footer the wall still has to say what
   it is, where the gallery page can rely on its own page title. */
/* `1fr` for the rail gave every spare pixel to the middle row, which
   pushed the hint hard onto the bottom edge and left it stranded a long
   way under the pictures. Three content-sized rows with a real gap,
   centred as a group: the hint sits just below the images and the whole
   stack is optically centred in the frame. */
.hgal--home .hgal__sticky {
  grid-template-rows: auto auto auto;
  align-content: center;
  gap: clamp(.75rem, 2.4vh, 2rem);
}
.hgal__head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--s3) var(--s5); flex-wrap: wrap;
  padding-inline: var(--gutter);
  padding-block: clamp(1.75rem, 5vh, 3.5rem) clamp(1rem, 2.5vh, 2rem);
}
.hgal__title { display: grid; gap: var(--s2); }
.hgal__title .t-h2 { max-width: 20ch; }
.hgal__all { flex: 0 0 auto; margin-bottom: .35em; }

/* ---- the homepage wall: nine identical cards on a travelling ground ----
   Each print is a card, not a bare frame: the caption lives inside the
   paper mount rather than on the section behind it. That is what lets the
   ground run all the way from panna to deep blue without ever putting the
   captions on a mid-tone they cannot survive. */
/* The ground used to be a colour-mix recomputed on the section itself, so
   every step of the travel repainted a full viewport - most of the roughness
   in this section came from there. It is now two flat layers that cross-fade:
   opacity on a composited layer costs the GPU almost nothing, and it makes
   the change perfectly linear, which a two-stage mix never was. */
.hgal--home .hgal__wash {
  position: absolute; inset: 0; z-index: -1; pointer-events: none;
  will-change: opacity;
}
.hgal--home .hgal__sticky { isolation: isolate; }
.hgal--home .hgal__wash--zen { background: var(--clay); opacity: 0; }
.hgal--home .hgal__wash--ink { background: var(--ink); opacity: 0; }

.hgal--home {
  background: var(--paper);
  /* The ground interpolates; the type must not. It hard-flips on a class
     at the crossover, with literal colours on both sides. Driving the flip
     through a custom property fed to color-mix was measured resolving to
     the wrong end for some consumers and the right end for others in the
     same paint. A class invalidates cleanly. */
  --fg: var(--on-paper); --fg-mute: var(--on-paper-mute);
  color: var(--on-paper);
  transition: color 420ms var(--ease-out);
}
.hgal--home.hgal--dark { --fg: var(--on-ink); --fg-mute: var(--on-ink-mute); color: var(--on-ink); }
/* full strength, not the muted tone: it crosses the mid-band */
.hgal--home .hgal__hint { color: var(--fg); }
.hgal--home .hgal__hint-i { opacity: .65; }

/* To the reference: no mount, no paper border - the image itself is the
   object, sized off the viewport width so one print nearly fills the frame
   with its neighbours just breaking the edges, and rounded hard. */
.hgal--home .hgal__item { inline-size: min(68vw, 880px); display: block; }
.hgal--home .hgal__item:nth-child(3n),
.hgal--home .hgal__item:nth-child(3n+2) { inline-size: min(68vw, 880px); align-self: auto; }

.hgal__card { display: block; }
.hgal--home .hgal__item button {
  padding: 0; background: none; display: block;
  /* a smaller, cheaper shadow: nine 60px-blur shadows recompositing under a
     transform is real frame cost on a wall this wide */
  box-shadow: 0 18px 34px -24px rgba(var(--ink-rgb), .45);
  border-radius: var(--hgal-r);
}
.hgal--home {
  /* One token feeds the card, the <picture>, the <img> and the caption's
     bottom corners - which is why setting the radius on the shared .plate
     rule did nothing. */
  --hgal-r: 5px;
}
.hgal--home .hgal__item .plate {
  inline-size: 100%; block-size: auto; max-block-size: none;
  aspect-ratio: 16 / 9;
  border-radius: var(--hgal-r);
  outline: 0;
}
/* The radius goes on the image itself as well as the box that clips it.
   `overflow: hidden` on an ancestor stops clipping composited children once
   the track is being transformed, which is why the corners came out square
   mid-scroll. */
.hgal--home .hgal__item picture { display: block; block-size: 100%; border-radius: var(--hgal-r); overflow: hidden; }
.hgal--home .hgal__item .plate img { border-radius: var(--hgal-r); }
/* The caption belongs to the picture, not to the page behind it.

   With the mount gone it had nowhere to sit but the travelling ground, and
   measured there it fell to 1.55:1 at the crossover - a light-to-dark
   ground passes through a mid-tone where neither dark nor light 12px type
   can reach 4.5:1, whatever the flip point. On the image under its own
   scrim it is constant, and the print stays full-bleed and rounded. */
.hgal--home .hgal__card { position: relative; }
.hgal--home .hgal__item .cap {
  position: absolute; inset-inline: 0; bottom: 0; z-index: 2;
  padding: clamp(2.2rem, 5vw, 3.4rem) clamp(1.1rem, 2vw, 1.75rem) clamp(.85rem, 1.5vw, 1.3rem);
  color: var(--beige);
  background: linear-gradient(to top,
    rgba(18, 18, 18, .88) 0%, rgba(18, 18, 18, .58) 42%, rgba(18, 18, 18, 0) 100%);
  border-radius: 0 0 var(--hgal-r) var(--hgal-r);
  pointer-events: none;
}

/* The one interactive thing left on the travelling ground. As a bare link
   it crossed the same mid-tone band at 3.1:1; as a filled pill the ground
   stops mattering - --fg is a hard flip, never a mid value, so pill and
   label are always at opposite ends of the scale. */
/* It takes the site's button treatment now - the flood and the word roll -
   so --bwipe and --bhov are what the shared rules read. */
.hgal--home .hgal__all {
  /* Filled, not outlined. An outlined pill puts its 15px label straight on
     the travelling ground, and at the crossover that measured 3.33:1 - fine
     for a border, short for text. Filled, the label always sits on its own
     solid colour, and the flood still reads because it arrives in the
     opposite tone. */
  padding: .7rem 1.35rem; border-radius: 4px;
  background: var(--ink); color: var(--on-ink);
  --bwipe: var(--on-ink); --bhov: var(--ink); --bfg: var(--on-ink);
  transition: background 420ms var(--ease-out), color 420ms var(--ease-out);
}
.hgal--home.hgal--dark .hgal__all {
  background: var(--sand); color: var(--ink);
  --bwipe: var(--ink); --bhov: var(--on-ink); --bfg: var(--ink);
}
.hgal--home .hgal__all::after { display: none; }

@media (max-width: 760px) {
  .hgal--home .hgal__item,
  .hgal--home .hgal__item:nth-child(3n),
  .hgal--home .hgal__item:nth-child(3n+2) { inline-size: 84vw; }
  /* 16:9 at 84vw is a 177px strip in a 583px rail - the ratio has to stand
     up on a phone, not just reproduce the desktop reference */
  .hgal--home .hgal__item .plate { aspect-ratio: 4 / 5; }
}

@media (max-width: 760px) {
  .hgal__item { inline-size: 74vw; }
  .hgal__item:nth-child(3n) { inline-size: 62vw; }
  /* The sticky screen is the whole budget here, so the head is reflowed
     rather than stacked: `display:contents` lifts the label and heading
     out of their wrapper so the label and the all-images link can share
     one row and the heading take the next. Hiding the link would have
     been cheaper and would have cost phones their only route from this
     section to the full set. */
  .hgal__head { gap: var(--s2) var(--s3); padding-block: var(--s4) var(--s3); align-items: center; }
  .hgal__title { display: contents; }
  .hgal__title .label { flex: 1 1 auto; }
  /* order, not DOM position: the link has to share the label's row, and
     the heading takes the one below it */
  .hgal__title .t-h2 { order: 1; flex: 1 0 100%; max-width: none; }
  .hgal__all { margin-bottom: 0; font-size: var(--fs-xs); }
}
@media (prefers-reduced-motion: reduce) { .hgal__hint-i { animation: none; } }

/* ---------- 25. REGISTER (above the footer) ------------------------------
   Centred head, a two-column field grid, one wide action. Fields are
   boxed rather than ruled: each carries its own label as the placeholder,
   so the box is what separates one from the next. The palm banner is
   duotoned into brand navy and beige and sits well back, with a wash over
   it — the wash is what keeps the type legible, so every change to it is
   a contrast change.                                                     */

/* The boundary into the footer is raked, the same shallow full-width edge
   the infrastructure section ends on.

   It goes on the REGISTER, not on the footer and not on `main`. On a large
   screen the footer is fixed behind and `main` is the opaque lid sliding
   up off it, so the edge you actually see is the bottom of the last thing
   in `main` - which is this section. Clipping `main` itself would make it a
   containing block for the fixed WhatsApp button and clip every sticky
   stage inside it; clipping the footer's top edge would do nothing, because
   that edge is never the one on screen.

   The triangle it opens shows `main`'s own ground, and --bg and --beige are
   both Sandstone, so it reads as the footer arriving. */
.reg {
  position: relative; isolation: isolate;
  --rake: clamp(48px, 7.6vw, 132px);
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 calc(100% - var(--rake)));
  /* A SECTION'S MAIN BLOCK SHOULD OWN A SCREEN. This one was sized by its
     own padding, so on a short-and-wide window it closed the page in about
     two thirds of a viewport and the form read as a strip rather than as
     the page's destination. It takes a screen as its floor and centres
     inside it: wider screens spend the room on width, narrower ones on
     height, and the section can still grow past a screen if the content
     needs it. */
  min-block-size: 100svh;
  display: grid; align-content: center;
  /* the extra below is breathing room before the footer begins */
  padding-block: clamp(4.5rem, 12vh, 10rem) calc(clamp(4.5rem, 12vh, 10rem) + 3.25rem);
  /* Oxblood, and it does not move with the palette. This is the site's one
     storytelling colour: the page's closing surface, and the inline policy
     link inside it. Nowhere else. */
  background: var(--wine); color: var(--on-ink);
  --fg: var(--on-ink); --fg-mute: var(--on-ink-mute);
  --accent: var(--clay); --rule: var(--hair-ink);
  /* the ground the fields are drawn on — flat navy, no glass */
  --reg-line: rgba(207, 200, 192, .28);
  --reg-line-hi: rgba(207, 200, 192, .45);
  --reg-focus: #BD8B6B;
  --reg-fill: rgba(var(--wine-rgb), .28);
  --reg-text: rgba(207, 200, 192, .94);
  --reg-ph: rgba(207, 200, 192, .60);
  /* #9E1B24 is the error red on paper; it sits at about 2.3:1 here, so
     the dark ground gets its own lighter tint */
  --reg-err: #F08A8A;
}
[dir="rtl"] .reg { clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--rake)), 0 100%); }
.reg__bg { position: absolute; inset: 0; z-index: -2; display: block; }
.reg__bg img { inline-size: 100%; block-size: 100%; object-fit: cover; opacity: 1; }
/* The wash. The render is already dark, so this is lighter than the one the
   duotoned palm needed — but the lit sign and the uplit trunks are bright,
   and the form sits over them. */
.reg::after {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(115% 85% at 50% 44%, rgba(var(--wine-rgb), .60) 0%, rgba(var(--wine-rgb), .84) 66%),
    linear-gradient(to bottom, rgba(var(--wine-rgb), .42), rgba(var(--wine-rgb), .74));
}

.reg__in { display: grid; gap: clamp(1.75rem, 4vh, 3.25rem); justify-items: center; }
.reg__head { display: grid; gap: .55rem; justify-items: center; text-align: center; }
.reg__head .t-h2 { text-wrap: balance; }
.reg__head .label { color: var(--clay); }
.reg__note { font-size: var(--fs-sm); color: rgba(207, 200, 192, .73); max-width: 46ch; }

.reg__form { inline-size: min(100%, 45rem); display: grid; gap: clamp(1.05rem, 2.6vh, 1.9rem); }
@media (min-width: 1440px) { .reg__form { inline-size: min(100%, 54rem); } }
@media (min-width: 1800px) { .reg__form { inline-size: min(100%, 64rem); } }
@media (min-width: 2200px) { .reg__form { inline-size: min(100%, 74rem); } }
/* The controls grow with the section rather than staying at their 1280px
   proportion inside a form that has nearly doubled in width. */
@media (min-width: 1800px) {
  .reg { --fld-fs: 1.15rem; --fld-h: 68px; --fld-pad: 1.45rem 1.35rem; }
}
@media (min-width: 2200px) {
  .reg { --fld-fs: 1.25rem; --fld-h: 76px; --fld-pad: 1.6rem 1.5rem; }
}
.reg__grid {
  display: grid; gap: clamp(.75rem, 1.7vh, 1.15rem);
  grid-template-columns: 1fr;
}
@media (min-width: 720px) { .reg__grid { grid-template-columns: 1fr 1fr; } }
.reg__form .field { display: grid; gap: .35rem; align-content: start; }
/* `.field::after` is the ruled underline the contact page's fields use — it
   draws in on focus and turns red on error. These fields are boxes, so the
   border carries both states and the underline is just a second line
   fighting it. */
.reg__form .field::after { display: none; }
.reg__form .field--full { grid-column: 1 / -1; }

/* ---- the box ---- */
.reg__form input,
.reg__form select,
.reg__form textarea {
  inline-size: 100%; min-height: var(--fld-h, 64px);
  padding: var(--fld-pad, 1.25rem 1.15rem);
  font-family: inherit; font-size: var(--fld-fs, 1rem); line-height: 1.5; color: var(--reg-text);
  background: var(--reg-fill);
  border: 1px solid var(--reg-line);
  border-radius: 5px;
  transition: border-color var(--t-med) var(--ease-out),
              box-shadow var(--t-med) var(--ease-out);
}
.reg__form textarea {
  /* rows="4" sets the height, so min-height never bound and the box stayed
     140px. block-size overrides the attribute. */
  block-size: 124px; min-height: 0; resize: vertical; padding-bottom: 2rem;
}
.reg__form select { appearance: none; cursor: pointer; padding-inline-end: 2.9rem; }
.reg__form option { color: var(--ink); background: var(--beige); }
/* The placeholders carry the field labels here, so they are held at the
   top of the 50-55% range the brief allows. */
.reg__form input::placeholder,
.reg__form textarea::placeholder { color: var(--reg-ph); }
/* the placeholder option reads as a placeholder, not as a chosen value */
.reg__sel:has(option[value=""]:checked) select { color: var(--reg-ph); }

.reg__form input:hover,
.reg__form select:hover,
.reg__form textarea:hover { border-color: var(--reg-line-hi); }
/* the border itself is the focus indicator — a bright hairline on navy,
   not a glow. The 1px ring only firms up the edge. */
.reg__form input:focus-visible,
.reg__form select:focus-visible,
.reg__form textarea:focus-visible {
  outline: none; border-color: var(--reg-focus);
  box-shadow: 0 0 0 1px var(--reg-focus);
}

/* ---- select + country code: the chevron is an element, not a background
        image, so it mirrors with the writing direction ---- */
.reg__sel, .reg__cc { position: relative; display: block; }
.reg__chev {
  position: absolute; inset-block-start: 50%; inset-inline-end: 1.05rem;
  inline-size: 18px; block-size: 18px; margin-block-start: -9px;
  color: var(--on-ink-mute); pointer-events: none;
}

/* ---- phone: one box holding a code select and the number ---- */
.reg__tel {
  display: flex; align-items: stretch;
  background: var(--reg-fill);
  border: 1px solid var(--reg-line);
  border-radius: 5px;
  transition: border-color var(--t-med) var(--ease-out), background var(--t-med) var(--ease-out);
}
.reg__tel:hover { border-color: var(--reg-line-hi); }
.reg__tel:focus-within { border-color: var(--reg-focus); box-shadow: 0 0 0 1px var(--reg-focus); }
.reg__tel .reg__cc { flex: 0 0 auto; }
.reg__tel select,
.reg__tel input {
  background: transparent; border: 0; border-radius: 0; box-shadow: none;
}
.reg__tel select { inline-size: auto; padding-inline: 1.15rem 2.4rem; }
.reg__tel .reg__chev { inset-inline-end: .9rem; }
.reg__tel input { flex: 1 1 auto; min-inline-size: 0; }
/* the divider between the code and the number */
.reg__tel .reg__cc::after {
  content: ''; position: absolute; inset-block: 1.05rem; inset-inline-end: 0;
  inline-size: 1px; background: var(--reg-line);
}
.reg__tel select:focus-visible,
.reg__tel input:focus-visible { outline: none; box-shadow: none; border-color: transparent; background: transparent; }

/* ---- textarea counter ---- */
.reg__area { position: relative; display: block; }
/* The counter carries dir="ltr" so "0 / 500" does not get reordered by
   bidi, which also means logical inset properties resolve against ITS
   direction, not the page's. Physical sides, scoped to the page, are the
   only unambiguous way to place it. */
.reg__count {
  position: absolute; inset-block-end: .75rem; right: 1.15rem;
  font-family: var(--font-latin); font-size: var(--fs-xs);
  color: var(--on-ink-mute); pointer-events: none;
}
[dir="rtl"] .reg__count { right: auto; left: 1.15rem; }

/* ---- consent ---- */
/* `.reg__form .field` sets display:grid at a higher specificity than a bare
   `.reg__consent`, so this row was silently laid out as a grid and the
   policy link stacked under the checkbox - 86px instead of 40px. */
.reg__form .reg__consent {
  display: flex; align-items: flex-start; flex-wrap: wrap; gap: .1rem .7rem;
  /* it was squeezed between the textarea and the button */
  margin-block: clamp(.5rem, 1.4vh, 1.1rem);
}
/* .field label sets uppercase 12px muted for every form label on the site;
   the consent line is a sentence, not a field label, so it has to outrank
   that rule rather than inherit it */
.reg__form .reg__check {
  display: flex; align-items: center; gap: .85rem;
  cursor: pointer; min-height: 44px;
  font-size: var(--fs-sm); font-weight: 400; line-height: 1.55;
  letter-spacing: 0; text-transform: none; color: rgba(207, 200, 192, .84);
}
.reg__form .reg__check { align-items: flex-start; padding-block: .5rem; }
.reg__form .reg__box { margin-block-start: .1rem; }
.reg__form .reg__check input {
  position: absolute; inline-size: 1px; block-size: 1px;
  padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); border: 0;
}
.reg__box {
  flex: none; inline-size: 26px; block-size: 26px;
  display: grid; place-items: center;
  border: 1px solid var(--reg-line-hi); border-radius: 6px;
  background: var(--reg-fill);
  transition: border-color var(--t-fast) var(--ease-out), background var(--t-fast) var(--ease-out);
}
.reg__box svg { inline-size: 15px; block-size: 15px; color: var(--ink-900); opacity: 0; transform: scale(.7); transition: opacity var(--t-fast) var(--ease-out), transform var(--t-fast) var(--ease-out); }
.reg__form .reg__check input:checked + .reg__box { background: var(--clay); border-color: var(--clay); }
.reg__form .reg__check input:checked + .reg__box svg { opacity: 1; transform: none; }
.reg__form .reg__check input:focus-visible + .reg__box { border-color: var(--clay); box-shadow: 0 0 0 3px rgba(var(--clay-rgb), .25); }
/* Inline in the sentence now, so it must not be a block of its own. */
.reg__policy {
  color: var(--on-ink);
  text-decoration: underline; text-underline-offset: 3px;
  text-decoration-color: rgba(207, 200, 192, .5);
  transition: text-decoration-color var(--t-med) var(--ease-out), color var(--t-med) var(--ease-out);
}
.reg__policy:hover, .reg__policy:focus-visible { color: var(--clay); text-decoration-color: var(--clay); }
.reg__consent .field__err { flex: 1 0 100%; color: var(--reg-err); }

/* ---- the action ---- */
.reg__submit {
  /* sized to its label, not stretched across the form - a 544px-wide
     button reads as a banner, not as something you press */
  justify-self: center; inline-size: min(100%, max(21.5rem, calc(var(--fld-fs, 1rem) * 21.5)));
  min-height: max(52px, var(--ui-h)); margin-top: .25rem;
  display: inline-flex; align-items: center; justify-content: center; gap: 1.2em;
  padding-inline: clamp(2rem, 4vw, 3rem);
  font-family: inherit; font-size: var(--fld-fs, 1rem); font-weight: 500;
  color: var(--ink-800); background: var(--beige);
  border: 0; border-radius: 0; cursor: pointer;
  clip-path: polygon(var(--cut-x) 0, 100% 0, 100% 100%, 0 100%, 0 var(--cut-y));
  /* No transition on `transform` here. The reveal scrubs transform every
     frame, and a 620ms CSS transition on the same property tries to ease
     toward each new value in turn - the two fight and the button lags
     behind the rest of the form. The :active press reads better instant
     anyway. */
  --bwipe: var(--ink-800); --bhov: var(--beige); --bfg: var(--ink-800);
}
/* THAT PALE LINE ON HOVER IS NOT A BORDER - this button has none. It is the
   button's OWN BEIGE BACKGROUND showing through where the flood does not
   reach. The two Fluid shapes are organic blobs with curved tops: they cover
   the middle of the box and leave a hairline along the top edge, and a
   thinner one at the bottom where the smaller blob's curve falls short.

   A flat layer behind both of them closes it. It rises on the same curve, so
   the organic shapes still lead and still read as the effect, and by the time
   they settle the whole box is one colour. `inset: -1px` overshoots so
   nothing is left at the very edge, and the button's own clip-path trims it
   back. z-index -3 keeps it behind both blobs. */
.reg__submit::before {
  content: ''; position: absolute; inset: -1px; z-index: -3;
  background: var(--bwipe); pointer-events: none;
  transform: translateY(101%);
  transition: transform 1.25s var(--ease-flow);
}
.reg__submit:hover::before,
.reg__submit:focus-visible::before { transform: translateY(0); }
@media (prefers-reduced-motion: reduce) { .reg__submit::before { transition: none; } }

[dir="rtl"] .reg__submit {
  clip-path: polygon(0 0, calc(100% - var(--cut-x)) 0, 100% var(--cut-y), 100% 100%, 0 100%);
}
.reg__submit:active { transform: translateY(1px); }
.reg__submit:disabled { opacity: .6; cursor: default; }
.reg__submit .btn__i { inline-size: 1.15em; block-size: 1.15em; transition: transform var(--t-med) var(--ease-out); }
[dir="rtl"] .reg__submit .btn__i { transform: scaleX(-1); }
.reg__submit:hover .btn__i { transform: translateX(4px); }
[dir="rtl"] .reg__submit:hover .btn__i { transform: scaleX(-1) translateX(4px); }

.reg__status { justify-self: center; text-align: center; font-size: var(--fs-sm); min-height: 1.4em; }
.reg__status[data-state="ok"]  { color: var(--clay); }
.reg__status[data-state="err"] { color: var(--reg-err); }

/* the dark ground needs its own error tint; the paper red disappears here */
.reg__form .field__err { color: var(--reg-err); }
.reg__form .field.has-err input,
.reg__form .field.has-err select,
.reg__form .field.has-err textarea,
.reg__form .field.has-err .reg__tel { border-color: var(--reg-err); }

@media (prefers-reduced-motion: reduce) {
  .reg__submit, .reg__submit .btn__i, .reg__box, .reg__box svg { transition: none; }
}

/* ---------- 26. THE CONTACT PAGE ---------------------------------------
   The details used to run down the page as five loose rows of key/value with
   nothing holding them together, and the form sat under them with no ground
   of its own. Two blocks now: the ways in, and the plan beside them, with
   the homepage's own form closing the page underneath.                    */
.cpage {
  display: grid; gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}
@media (min-width: 900px) { .cpage { grid-template-columns: minmax(0, .95fr) minmax(0, 1.05fr); } }

.cpage__ways { display: grid; gap: clamp(.4rem, 1.2vh, .9rem); align-content: start; }

/* EACH WAY IN IS ONE TARGET, not a label beside a link. The old rows made
   only the value clickable - a 15px string of digits - while the row it sat
   in did nothing. The whole row is the link now, which is both a far larger
   tap target and the thing a visitor actually points at. */
.cway {
  display: flex; align-items: center; gap: clamp(.9rem, 2vw, 1.4rem);
  padding: clamp(.85rem, 1.8vh, 1.25rem) 0;
  color: var(--ink-800); position: relative;
  border-block-end: 1px solid var(--hair);
}
.cway:last-of-type { border-block-end: 0; }
.cway__i {
  inline-size: clamp(38px, 2.4vw, 46px); block-size: clamp(38px, 2.4vw, 46px);
  flex: none; display: grid; place-items: center;
  color: var(--accent); background: rgba(var(--ink-rgb), .05);
  border-radius: 50%;
  transition: background var(--t-med) var(--ease-out), color var(--t-med) var(--ease-out);
}
.cway__i svg { inline-size: 45%; block-size: 45%; }
.cway:hover .cway__i { background: var(--accent); color: var(--beige); }
.cway__t { display: grid; gap: .15rem; min-width: 0; }
.cway__k {
  font-size: var(--fs-xs); letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--fg-mute); font-weight: 600;
}
html[lang="ar"] .cway__k { text-transform: none; letter-spacing: .01em; font-weight: 700; }
.cway__v {
  font-size: clamp(1rem, 1.3vw, 1.25rem); font-weight: 500; line-height: 1.35;
  overflow-wrap: anywhere;
}
/* the site's one hover gesture, the hairline drawn from the reading edge */
.cway::after {
  content: ''; position: absolute; inset-block-end: -1px; inset-inline: 0;
  block-size: 1px; background: var(--accent);
  transform: scaleX(0); transform-origin: left center;
  transition: transform 520ms var(--ease-lux);
}
[dir="rtl"] .cway::after { transform-origin: right center; }
.cway:hover::after, .cway:focus-visible::after { transform: scaleX(1); }

.cpage__dev {
  margin-block-start: clamp(1rem, 2.5vh, 1.75rem);
  display: flex; flex-wrap: wrap; gap: .5rem .9rem; align-items: baseline;
  font-size: var(--fs-xs); color: var(--fg-mute);
}
.cpage__dev b { color: var(--ink-800); font-weight: 500; font-size: var(--fs-sm); }

.cpage__plan { margin: 0; display: grid; gap: clamp(.9rem, 2.4vh, 1.5rem); justify-items: end; }
[dir="rtl"] .cpage__plan { justify-items: start; }
.cpage__plan picture { display: block; inline-size: 100%; border-radius: 5px; overflow: hidden;
  box-shadow: 0 20px 36px rgba(18, 18, 18, .22), 0 3px 8px rgba(18, 18, 18, .12); }
.cpage__plan img { inline-size: 100%; block-size: auto; display: block; }
.cpage__plan figcaption { display: flex; }

/* The same form, on its own flat ground. The client asked for the homepage's
   styling without the photograph behind it, so the wash that existed to hold
   that photograph down goes with it - over a flat colour it is a gradient
   over nothing. */
.reg--plain::after { display: none; }

/* ---------- 27. THE MASTERPLAN PAGE -------------------------------------
   The same panel the homepage carries - same bar, same furniture, same
   controls - with the one difference that matters: there, the plan shares a
   screen with a heading and a way in, so the drawing takes what is left of
   one viewport. Here the drawing IS the page, so it gets a tall frame and
   the section is sized by it rather than the other way round.            */
.mp--page { min-block-size: auto; padding-block: 0 var(--sp-section); }
.mp--page .mp__panel { inline-size: min(1640px, 94%); }
@media (min-width: 1800px) { .mp--page .mp__panel { inline-size: min(1900px, 95%); } }
@media (min-width: 2200px) { .mp--page .mp__panel { inline-size: min(2320px, 96%); } }
/* HEIGHT FIRST, THEN THE RATIO. Sized by a 16:10 ratio off its own width it
   came out taller than the window on any wide screen, so the drawing ran past
   the fold - and a plan that has to be scrolled to be seen whole is the one
   thing this page must not do. It takes the screen it is allowed, less the
   header and the bar above it, and the ratio only governs below that. */
.mp--page .mp__frame {
  block-size: min(
    calc(100svh - var(--header-h) - clamp(7rem, 15vh, 11rem)),
    calc((100vw - 2 * var(--gutter)) * 0.625)
  );
  max-block-size: none; aspect-ratio: auto;
  min-block-size: 320px;
}
@media (max-width: 760px) {
  .mp--page .mp__frame { block-size: auto; aspect-ratio: 4 / 5; }
}

/* ---------- 28. THE GALLERY GRID ----------------------------------------
   Minimal: the pictures do the work and nothing else is drawn. The only
   furniture is a plate number and a caption under each, which is the same
   device the numbers section uses - a survey entry rather than a card.   */
/* The opener centres, the same way the homepage gallery's does, so the two
   read as the same section at two scales. */
.gallery-open { text-align: center; }
.gallery-open .label { justify-content: center; }
.gallery-open h1 { margin-inline: auto; }
.gallery-open .t-lead { margin-inline: auto; }
.ggrid-sec { padding-block: 0 var(--sp-section); }
.ggrid {
  display: grid; gap: clamp(1.6rem, 3.4vw, 3.25rem) clamp(1.1rem, 2.2vw, 2rem);
  grid-template-columns: 1fr;
}
@media (min-width: 700px)  { .ggrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1100px) { .ggrid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* One in six runs double width. Enough to break the grid out of a
   checkerboard; not enough to make it a collage. */
@media (min-width: 700px)  { .g--wide { grid-column: span 2; } }

.gcard { margin: 0; display: grid; gap: clamp(.6rem, 1.3vh, .95rem); }
.gcard button {
  display: block; inline-size: 100%; padding: 0; border: 0; background: none;
  cursor: zoom-in; overflow: hidden; border-radius: 5px;
  box-shadow: 0 18px 32px rgba(18, 18, 18, .18), 0 2px 6px rgba(18, 18, 18, .1);
}
.gcard picture { display: block; }
.gcard img {
  inline-size: 100%; block-size: 100%; display: block; object-fit: cover;
  aspect-ratio: 4 / 3;
  transition: transform 900ms var(--ease-lux);
}
.g--wide img { aspect-ratio: 16 / 9; }
.gcard button:hover img { transform: scale(1.035); }
.gcard button:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }

.gcard__cap {
  display: flex; align-items: baseline; gap: .7em;
  font-size: var(--fs-sm); color: var(--fg-mute); line-height: 1.4;
}
.gcard__n {
  font-family: var(--font-display); font-size: var(--fs-xs);
  letter-spacing: .14em; color: var(--accent); flex: none;
  font-variant-numeric: tabular-nums;
}
html[lang="ar"] .gcard__cap { font-family: var(--font-arabic); }

@media (prefers-reduced-motion: reduce) { .gcard img { transition: none; } }

/* ---------- 29. THE DIALLING CODE, THE HINTS, THE RING'S STATUS --------
   Three things that arrived together because they answer the same note:
   the site was leaving the visitor to work out how it worked.            */

/* ---- 29a. the country control ----------------------------------------
   The native <select> stays in the DOM and keeps submitting; it is hidden
   only once the JS has built the layer over it, so with scripting off the
   plain select is still there and still works. That is what `tel--js`
   marks - the class is added by the module, never authored.             */
.tel--js select[data-tel-cc] { display: none; }
.tel--js .reg__cc > .reg__chev { display: none; }

.tel__btn {
  display: flex; align-items: center; gap: .55rem;
  block-size: 100%; padding-inline: 1.05rem .85rem;
  background: none; border: 0; cursor: pointer;
  color: inherit; font: inherit; line-height: 1;
}
.tel__flag {
  inline-size: 22px; block-size: 15px; flex: none; display: block;
  border-radius: 2px;
  /* A flag is artwork, not an icon: it needs an edge of its own or a white
     field (Lebanon, Bahrain) dissolves into a pale ground. */
  box-shadow: 0 0 0 1px rgba(18, 18, 18, .22);
}
.tel__code { font-family: var(--font-latin); font-size: var(--fs-sm); letter-spacing: .01em; }
.tel__chev {
  inline-size: 15px; block-size: 15px; flex: none;
  color: var(--on-ink-mute);
  transition: transform var(--t-fast) var(--ease-out);
}
.is-open .tel__chev { transform: rotate(180deg); }
.tel__btn:focus-visible { outline: 2px solid var(--reg-focus); outline-offset: -2px; }

/* ---- the list -------------------------------------------------------
   A light panel on a dark form, which is what the native dropdown already
   did - `.reg__form option` has been beige-on-ink since the form was
   built, so this is the same surface rather than a new one.             */
.tel__pop {
  position: absolute; inset-block-start: calc(100% + 8px); inset-inline-start: 0;
  inline-size: min(340px, 86vw); z-index: 40;
  background: var(--beige); color: var(--on-paper);
  border-radius: 6px;
  box-shadow: 0 24px 48px rgba(18, 18, 18, .34), 0 2px 8px rgba(18, 18, 18, .2);
  overflow: hidden;
}
/* THE FIELD IS LIFTED, NOT THE POPUP. z-index: 40 looked right and did
   nothing: FormFx gives every field a transform as it reveals, a transform
   creates a stacking context, and the popup's z-index was therefore only
   ever compared against its OWN field's children. Every field after it in
   the grid - the message box above all - painted straight over the list.
   Raising the field raises the context the list lives in. */
.field:has(.tel--js.is-open) { position: relative; z-index: 50; }

.tel__search { padding: .55rem; border-block-end: 1px solid rgba(0, 36, 38, .12); }
.tel__search input {
  inline-size: 100%; border: 0; border-radius: 4px;
  background: rgba(0, 36, 38, .05); color: var(--on-paper);
  padding: .6rem .8rem; font: inherit; font-size: var(--fs-sm);
}
.tel__search input::placeholder { color: var(--on-paper-mute); }
.tel__search input:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.tel__pop--bare .tel__search { display: none; }
.tel__pop--bare .tel__list { max-block-size: none; }
.tel__list:focus-visible { outline: none; }
.tel__list {
  list-style: none; margin: 0; padding: .3rem;
  max-block-size: 280px; overflow-y: auto; overscroll-behavior: contain;
}
.tel__row {
  display: flex; align-items: center; gap: .7rem;
  padding: .5rem .65rem; border-radius: 4px; cursor: pointer;
  font-size: var(--fs-sm); line-height: 1.3;
}
.tel__row[hidden] { display: none; }
.tel__name { flex: 1 1 auto; min-inline-size: 0; }
.tel__dial { color: var(--on-paper-mute); font-family: var(--font-latin); flex: none; }
.tel__row.is-cursor { background: rgba(0, 36, 38, .07); }
.tel__row.is-on { background: rgba(103, 58, 29, .12); }
.tel__row.is-on .tel__name { font-weight: 600; }
html[lang="ar"] .tel__row { font-family: var(--font-arabic); }

/* ---- 29b. hints and the privacy line ---------------------------------
   The hint sits under the field and stays there - it is not an error, so
   it does not come and go. The error appears beneath it when there is one,
   which is why both are legible on the dark ground.                     */
.reg__form .field__hint { color: var(--on-ink-mute); line-height: 1.45; }
.field__hint { display: block; margin-block-start: .4rem; }
html[lang="ar"] .field__hint { font-family: var(--font-arabic); }

.reg__priv {
  justify-self: center; text-align: center;
  font-size: var(--fs-xs); color: var(--on-ink-mute);
  margin-block-start: -.2rem;
}
html[lang="ar"] .reg__priv { font-family: var(--font-arabic); }

/* ---- 29c. the ring says what it is doing -----------------------------
   An image strip that moves on its own and stops when you point at it is
   two behaviours a visitor has to discover by accident. Both states are in
   the DOM and CSS picks one, rather than JS writing the sentence - writing
   it would mean rebuilding it on every language change and reading it back
   out of the DOM, which is the "ARAR" trap.                             */
.ring-hint {
  display: flex; align-items: center; justify-content: center; gap: .55rem;
  font-size: var(--fs-xs); color: var(--on-paper-mute);
  letter-spacing: .02em; margin-block-end: clamp(1.1rem, 2.4vh, 1.9rem);
}
html[lang="ar"] .ring-hint { font-family: var(--font-arabic); letter-spacing: 0; }
.ring-hint__dot {
  position: relative; inline-size: 6px; block-size: 6px; flex: none;
  border-radius: 50%; background: var(--accent);
}
/* transform and opacity only - a halo that grows by scaling costs one
   composited layer, where one that grows by width repaints every frame. */
.ring-hint__dot::after {
  content: ''; position: absolute; inset: -3px; border-radius: 50%;
  border: 1px solid var(--accent);
  animation: ringPulse 2.4s var(--ease-out) infinite;
}
@keyframes ringPulse {
  0%   { transform: scale(.5); opacity: .8; }
  70%  { transform: scale(1.5); opacity: 0; }
  100% { transform: scale(1.5); opacity: 0; }
}
.ring-hint.is-hold .ring-hint__dot { background: var(--on-paper-mute); }
.ring-hint.is-hold .ring-hint__dot::after { animation: none; opacity: 0; }

/* The hint used to carry two sentences and swap them here. It says one
   thing now; the dot above is the state indicator and does not need the
   text to repeat it. */

@media (prefers-reduced-motion: reduce) {
  .ring-hint__dot::after { animation: none; }
  .tel__chev { transition: none; }
}

/* ---- 29d. two more places that were leaving the visitor to guess ----- */

/* The viewer's keys. Hidden under 700px: naming Escape to someone holding
   a phone is noise, and the arrows there are the buttons, not the keyboard. */
.lb__keys { font-size: var(--fs-xs); color: var(--on-ink-mute); letter-spacing: .02em; }
html[lang="ar"] .lb__keys { font-family: var(--font-arabic); letter-spacing: 0; }
@media (max-width: 700px) { .lb__keys { display: none; } }

.grid-hint {
  font-size: var(--fs-xs); color: var(--on-paper-mute);
  letter-spacing: .02em; text-align: center;
  margin-block-end: clamp(1.2rem, 2.6vh, 2.1rem);
}
html[lang="ar"] .grid-hint { font-family: var(--font-arabic); letter-spacing: 0; }

/* ========================================================================
   30. THE PHONE
   ------------------------------------------------------------------------
   The desktop layout was locked first and mobile deliberately deferred -
   SS8b says so in as many words. This is that work, and it is mostly not
   "the same thing, narrower": three sections change SHAPE here, because the
   shape that is right at 1920 is wrong at 375.

   One breakpoint does the talking, 760px, except where a rule has to agree
   with geometry the stylesheet already changes at 860.
   ===================================================================== */

/* ---- 30a. the aperture, on a phone only -------------------------------
   THE WIDE HERO IS UNCHANGED. It keeps the 57%/61% stop, which is 0.285 of
   the mark box: the hollow's inscribed radius measures 0.2202, but a radial
   scan hits the thin slivers between the banners first and the ring is only
   half-solid at 0.305, so a circle at the inscribed radius leaves a visible
   gap all the way round. 0.285 closes it by overlapping the slivers, and
   that trade is the right one at 160px of mark.

   It is the wrong one at 54px. The banners are thin there, the overlap is a
   larger share of what you can see, and the result reads as a dark disc
   sitting ON the mark rather than film seen THROUGH it. The client wants the
   circle inside the wheel on a phone, and accepts what that costs.

   So the phone gets its own: solid to 38% and gone by 46% of closest-side -
   0.19 to 0.23 of the box, which lands ON the inscribed hollow and never
   crosses a banner. The gap the wide hero was tuned to avoid IS there; at
   this size it reads as the mark having a centre rather than as a fault. */
@media (max-width: 760px) {
  .hero__film {
    -webkit-mask-image: radial-gradient(circle closest-side, #000 0 38%, rgba(0,0,0,.55) 43%, rgba(0,0,0,0) 46%);
    mask-image: radial-gradient(circle closest-side, #000 0 38%, rgba(0,0,0,.55) 43%, rgba(0,0,0,0) 46%);
  }
}

@media (max-width: 860px) {

  /* ---- 30b. the nearby places are a deck, not a stage ------------------
     Five screens on a phone for seven place names, and the map - the thing
     the distances are about - had already scrolled away for four of them.
     The pane gives up its scroll run, the stage stops sticking, and the
     card sits above the map on one screen. */
  /* THE GRID PINS, NOT THE CARD. On a wide screen `.details-sticky` pins
     inside `.details-pane` and the map pins separately beside it. Stacked
     into one column that cannot work: a grid item's sticky travel is bounded
     by its own grid AREA, and the map's row is only 44svh tall, so it has
     nowhere to travel. The whole grid pins instead, below the header, and
     the run moves up to `.project-context`. Card and map stay together for
     the entire section, which is the thing the phone layout had lost. */
  .project-context {
    position: relative;
    /* A FLICK ON A PHONE CARRIES ABOUT 600px, so at 26svh (211px here) one
       flick crossed three landmarks and the section read as a blur. 40svh
       is 325px at 812, which is about two - and a deliberate push moves
       one. The section grows from 3.1 screens to about 3.8, which is the
       cost of being able to stop on a place name. */
    --lm-run: clamp(300px, 40svh, 460px);
    /* THE RUN IS THE TRAVEL, NOT THE TRAVEL PLUS A SCREEN. A sticky
       element's run is its parent's height MINUS its own, and this one is
       `100svh - header` tall - so adding a whole `100svh` gave the pane a
       header's worth of slack past the last stop, a quarter of the section
       scrolling by with no card showing at all. */
    min-block-size: calc(100svh - var(--header-h) + var(--stops, 7) * var(--lm-run));
  }
  .context-grid {
    position: sticky; top: var(--header-h);
    block-size: calc(100svh - var(--header-h));
    grid-template-columns: 1fr;
    /* CONTENT ROWS AND A CENTRED STACK, not a map that takes every spare
       pixel. With the map on `1fr` it could not fill the row it was given -
       `block-size: 100%` inside an auto-height flex pane does not resolve -
       so 163px of nothing opened between the picture and the rail while the
       distance sat hard against the header. All four rows are their own
       height now and the group centres: the air lands above and below the
       stack, where it reads as breathing room, instead of inside it. */
    grid-template-rows: auto auto auto auto;
    gap: clamp(.75rem, 2vh, 1.4rem);
    align-content: center;
    padding-block: clamp(.5rem, 2vh, 1.4rem);
  }
  .details-pane { min-height: 0; padding-inline-start: 0; order: 1; }
  .details-sticky { position: static; height: auto; }
  .map-pane { order: 2; position: relative; height: 42svh; min-block-size: 0; padding: 0; }
  .map-pane .map-mask { block-size: 100%; max-block-size: none; }
  .landmark-card {
    inline-size: 100%;
    /* The flip-in was built for a card arriving as you scrolled past it. On
       a deck the card is replaced in place, so it cross-fades instead -
       a rotate on every swap reads as a glitch at this speed. */
    transform: translateY(8px);
    transition: opacity 320ms var(--ease-out), transform 420ms var(--ease-lux);
  }
  .landmark-card.is-active { transform: translateY(0); }
  /* THE CARD IS THE ONLY THING ON SCREEN HERE, so it takes the centre line
     like every other heading on a phone, and the two halves are rebalanced:
     at 60px against a 20px name the measurement was shouting over the place
     it measures. On a wide screen the distance can carry a card on its own;
     on a 282px one the name has to be readable first. */
  .landmark-card { text-align: center; }
  /* Bigger, on the client's call: the measurement is the thing the section
     is about and at 38px it was the smallest type on the screen. */
  /* `.landmark-distance` carries its own dir="ltr" so the figure never
     reorders, which means it also has its own text-align - set per direction
     further up the file, and left-aligned in English. On a phone the card is
     centred and the number has to follow it, which takes a rule of the same
     specificity rather than inheriting one. */
  .landmark-distance,
  [dir="rtl"] .landmark-distance {
    margin-inline: auto; text-align: center;
    font-size: clamp(52px, 15vw, 68px);
  }
  /* the tapered rule under it was pushed to one side for RTL; centred here */
  .landmark-distance::after,
  [dir="rtl"] .landmark-distance::after { margin-inline: auto; }
  .landmark-card h3 { font-size: clamp(22px, 6.4vw, 28px); }
  [dir="rtl"] .landmark-distance { margin-inline: auto; }

  /* The section heading and the card are both centred display type, so with
     a tight gap they read as one four-line block. This is the air that makes
     the heading the section's and the card the content's. */
  .context .opener { margin-block-end: clamp(2.2rem, 6.5vh, 3.6rem); }

  /* The map was sitting at 282px inside the shell's gutters - three quarters
     of the screen for the one element the section exists to show. It takes
     the full width here; the card above keeps the gutter. */
  .map-pane {
    /* `top` SURVIVES THE SWITCH TO RELATIVE. The desktop rule pins this
       with `position: sticky; top: var(--header-h)`, and turning it
       relative without clearing `top` does not neutralise the offset -
       it REINTERPRETS it, pushing the map 88px down out of its own grid
       row and straight over the rail underneath. */
    top: auto;
    margin-inline: calc(var(--gutter) * -1);
    inline-size: calc(100% + var(--gutter) * 2);
    height: 44svh;
  }
  .map-mask { border-radius: 5px; }

  /* across is ours, down stays the page's */
  .project-context { touch-action: pan-y; }

  .lm-rail {
    display: flex; justify-content: center; align-items: center; gap: .7rem;
    /* IN the pinned grid, not after it. Left outside, its natural position
       was the bottom of a 2,400px box, so it only appeared once the whole
       run had been scrolled - a control you cannot see until you no longer
       need it. */
    margin-block-start: 0; order: 3;
  }
  .lm-dot {
    inline-size: 9px; block-size: 9px; padding: 0; border: 0; border-radius: 50%;
    background: rgba(var(--ink-rgb), .22); cursor: pointer;
    transition: background var(--t-med) var(--ease-out), transform var(--t-med) var(--ease-out);
    /* 9px is the dot; 44px is the target. A pseudo-element carries the
       difference so the dot stays small and the finger still lands. */
    position: relative;
  }
  .lm-dot::before { content: ''; position: absolute; inset: -17px; }
  .lm-dot.is-on { background: var(--accent); transform: scale(1.35); }
  .lm-dot:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

  .lm-hint {
    text-align: center; font-size: var(--fs-xs); color: var(--on-paper-mute);
    margin-block-start: 0; order: 4; padding-block-end: .4rem;
  }
  html[lang="ar"] .lm-hint { font-family: var(--font-arabic); }
}
@media (min-width: 861px) { .lm-rail, .lm-hint { display: none; } }

@media (max-width: 760px) {

  /* ---- 30c. the statement stops holding a screen it does not fill ------
     The stage was 100svh with the text and the picture centred inside it,
     and the picture then carried `--s7` (128px) of its own margin on top -
     so 614px of content sat in 812px of section with a 128px hole through
     the middle of it. The section is its content's height here, and the gap
     is a gap rather than a desktop rhythm token. */
  /* THE GAP WAS MOSTLY THIS, not the plate's margin. The stage's own
     bottom padding was 73px at 812 and the plate's margin only 16, so
     halving the margin alone moved almost nothing. The bottom padding
     is its own value now - the section still opens with air above the
     headline, and the picture follows the words closely.
     The rest of what the eye reads as a gap is EdStage's hand-off lift
     carrying the text upward as the section leaves; that is motion, not
     layout, and it is the thing the section is for. */
  .ed__stage {
    block-size: auto;
    padding-block: clamp(3.5rem, 9vh, 6rem) clamp(1.5rem, 4.3vh, 2.8rem);
  }
  .ed__stick { position: static; block-size: auto; }
  /* Halved again on the client's call - 4vh is 32px at 812 and the two
     elements are one thought, not two blocks. */
  .ed__plate { margin-block-start: clamp(.8rem, 2vh, 1.3rem); }

  /* ---- 30d. the figures centre ----------------------------------------
     The heading had nothing above it and almost nothing below it, so it
     read as a line left over from the section before. The eyebrow is the
     break (it carries the single banner); this is the air. */
  .figs-sec .opener { margin-block-end: clamp(2.4rem, 7vh, 4rem); }
  .figs-sec .opener .label { margin-block-end: .8rem; }
  .figs { gap: clamp(2.2rem, 6vh, 3.4rem); }
  .fig, .fig:nth-child(even) { grid-template-columns: minmax(0, 1fr); }
  .fig__txt {
    justify-items: center; text-align: center;
    inline-size: 100%; max-inline-size: none; justify-self: center;
    padding-inline: 0;
  }
  .fig__head { margin-inline: auto; }
  .fig__media { order: 2; }

  /* ---- 30e. the service rail is numbers, not a second list of names ----
     Two columns of five names read as a list that had run out of room, and
     the names were already redundant: the live network's name is set at
     127px directly above them. The rail keeps what only it can say - which
     of the five you are on, and how far the dwell has run - in one row. */
  /* THE ARROWS SAT ON THE NAME. They are centred on the frame at `top: 50%`,
     which is fine beside one line of 127px type and not fine when "Paved Road
     Network" wraps to two at 375px wide - measured, the name occupied
     417-464 and the arrows 406-462. They drop below the name block here. */
  .nets__nav { inset-block-start: auto; inset-block-end: 23%; }

  /* THE SECTION HAD NO `touch-action`, so every touch on it belonged to
     the browser by default and the swipe handler never saw a second
     move. This is the declaration that divides the gesture: sideways is
     ours, down is the page's. Without it no amount of JavaScript can
     claim a horizontal drag, because by the time a move arrives the
     browser has already started scrolling and preventDefault is a
     no-op. */
  .nets-sec { touch-action: pan-y; }

  .nets__rail { grid-template-columns: repeat(5, minmax(0, 1fr)); gap: .5rem; }
  .nets__tab-t { display: none; }
  .nets__tab { justify-items: center; text-align: center; gap: .45rem; }
  .nets__tab-n { font-size: var(--fs-xs); }

  /* ---- 30f. the plan's furniture ---------------------------------------
     The eyebrow and the heading were one block of type with no air in it,
     and the filter chips wrapped into a ragged two-and-a-half row grid
     because they are sized by their own labels. A real grid of equal cells
     is the only thing that reads as deliberate at this width. */
  .stage.mp { min-block-size: 0; padding-block: clamp(3rem, 8vh, 5rem); }
  .mp__panel { inline-size: min(100%, 94%); }
  /* `.stage.mp .opener` squeezes these to .35rem/.6rem so the whole
     section fits one screen on a desktop. The phone section is no longer
     height-constrained, so it gets the air back - and it needs the same
     three-class specificity to say so. */
  .stage.mp .opener { gap: .6rem; margin-block-end: clamp(1.4rem, 4vh, 2.2rem); }

  .mpf { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5rem; }
  .mpf__seg { grid-column: 1 / -1; justify-self: center; }
  .mpf__drop { display: block; }
  .mpf__pill { inline-size: 100%; justify-content: space-between; }
  .mpf__menu { inline-size: 100%; }
  /* `white-space: nowrap` and a 10rem floor on a 172px grid cell: the
     placeholder ran straight out of its own border. It takes the full
     row here - it is the widest label in the bar and the only control
     that is a sentence - and clips rather than overflowing if it still
     does not fit. */
  .mpf__search {
    grid-column: 1 / -1; min-inline-size: 0; flex: 0 1 auto;
    overflow: hidden;
  }
  .mpf__search > span { overflow: hidden; text-overflow: ellipsis; min-inline-size: 0; }

  /* ---- 30g. headings and buttons sit on the centre line ----------------
     The homepage sections use `.opener`, which has centred since it was
     written. THE OTHER FOUR PAGES DO NOT - their openers are a label, an
     h1 and a lead sitting straight in `.shell`, so they were the only
     headings on the site still starting at the reading edge on a phone.
     Only those three elements: the contact page's list of ways in is a
     list of rows and reads as one. */
  .stage > .shell > .label,
  .stage > .shell > h1,
  .stage > .shell > h2,
  .stage > .shell > .t-lead,
  .stage > .shell > .t-body { text-align: center; margin-inline: auto; }
  .fig__txt, .ed, .mp__head, .reg__head, .ring-sec__head { text-align: center; }
  .row, .mp__foot, .hero__cta, .ring-sec__foot, .reg__form .row {
    justify-content: center; justify-items: center;
  }
  .t-h2, .t-display, .t-lead, .label { margin-inline: auto; }
  .ggrid-sec .shell > .t-h2 { text-align: center; }

  /* ---- 30h. the footer ------------------------------------------------
     Four blocks stacked flush at the start edge, each a different height,
     read as a column of leftovers. Centred, with the brand given its own
     air and the three lists on one rhythm, it reads as a close. */
  .ftr__grid { justify-items: center; text-align: center; gap: clamp(1.8rem, 5vh, 2.6rem); }
  .ftr__id { justify-items: center; }
  .ftr__tag { max-width: 30ch; }
  .ftr__social { justify-content: center; }
  .ftr__col { inline-size: 100%; justify-items: center; }
  .ftr__lines, .ftr__nav ul { align-items: center; }
  .ftr__line, .ftr__maplink { justify-content: center; }
  .ftr__addr { max-width: 28ch; margin-inline: auto; }
  .ftr__strip {
    flex-direction: column; align-items: center; gap: .7rem; text-align: center;
  }
  .ftr__creds { justify-content: center; flex-wrap: wrap; }

  /* ---- 30i. the ring ---------------------------------------------------
     It already shrinks with `--ru`, but the hint and the button wanted the
     same centre line as everything else. */
  .ring-sec__head { padding-inline: var(--gutter); }
}

/* ---------- 31. THE PRIVACY POLICY --------------------------------------
   A reading page, and the only one on the site that is mostly body text.

   It had a real measure and nothing else, and the client's note was that it
   read as basic. It was: the column sat against the reading edge of a full
   shell, so half of a wide screen was empty beside it, and the title was
   just the first line of the same column. Two changes fix both.

   ONE: THE TITLE GETS ITS OWN GROUND. A dark band carries the heading, the
   version and the authority note, and the page proper begins under it. The
   band is `--hero-a` - the brand's Earth Brown, the same hex the fixed
   header bar tints itself with at .72 - so the header composites to exactly
   the band's own colour and the two read as ONE surface rather than as a
   dark strip sitting on a dark strip. That is the same argument SS8 makes
   for the hero's veil.

   TWO: THE COLUMN IS CENTRED AND THE TYPE IS SMALLER. Both on the client's
   call, and the reference was calma.sa/ar/legal/privacy-policy - 17px over
   a 1.95 leading in a column of its own. The text inside the column still
   sits on its reading edge: the COLUMN is centred, not the lines. Centred
   lines in a legal clause are unreadable, and justified Arabic without
   kashida opens rivers between the words.

   Measured on the band, worst pixel under each element:
     heading + body on Earth Brown   #CFC8C0   5.76:1
     version + note                  #C3BCB5   5.08:1
   `--on-ink-mute` (#B9AEA2) was the obvious token for the muted lines and
   measures 4.38:1 there - under the 4.5:1 floor for small text, so the
   muted lines take `--paper-2` instead.                                  */

/* ---------- THE PAGE MASTHEAD ------------------------------------------
   A band of its own, outside `.shell`, so the ground runs the full width
   while the text inside keeps the page's own column.

   It opens EVERY inner page now - privacy, film, contact, masterplan and
   gallery - on the client's call, and it earns the consistency twice over.
   Before, each of those pages began with type on Sandstone directly under
   the fixed bar, which is where `--hc-line` measures its worst (1.91:1, SS8b).
   On the band the same border sits on a dark ground and clears the 3:1 a UI
   boundary wants, so the header stops being the weakest thing on the page
   exactly where it is most looked at.

   One device, used the same way five times. That is the SS8 brief - an
   effect that draws attention to itself is a fault, and five variations of
   one band would be five effects.                                         */
.mast {
  /* MIDNIGHT TEAL, the brand's structural dark, on the client's call - and
     it buys back a contrast step the brown could not give. On Earth Brown
     `--on-ink-mute` measured 4.38:1, under the floor for small text, so the
     muted lines had to borrow `--paper-2`. On teal the real token measures
     7.53:1 and the page uses the tokens it is supposed to:
       heading + body   --on-ink       #CFC8C0   9.90:1
       version + note   --on-ink-mute  #B9AEA2   7.53:1

     The header bar no longer composites away against it - it tints itself
     with the Earth Brown veil at .72, so over teal it reads as a slightly
     warmer band at the very top. That is the same relationship every other
     dark section on the site has with the bar. */
  position: relative; isolation: isolate;   /* or the pfield at z-index:-1
                                               falls behind the page ground */
  background: var(--ink);
  color: var(--on-ink);
  --fg: var(--on-ink);
  --fg-mute: var(--on-ink-mute);
  /* The field is drawn in LIGHT here. `--pat-col` defaults to `--ink`,
     which on an ink ground is the pattern painted in its own background. */
  --pat-col: var(--on-ink);
  text-align: center;

  /* THE BOTTOM PADDING HAS TO CLEAR THE CUT, and the cut is the full
     `--rake` deep at the raked corner. Without that term the diagonal ran
     straight through the lead sentence on every page - measured at 1500x900
     on the masterplan, the cut crossed 38px ABOVE the bottom of the lead,
     and the title cleared by only 34px.

     WHY 0.8 AND NOT THE WHOLE RAKE. The text is centred, so the deepest
     point under it is at its own raked-side edge - `(page + text) / 2`
     across, which needs `--rake * (page + text) / (2 * page)`. That is 0.68
     for the lead at its 56ch measure and 0.72 for a 22ch title on a narrow
     window; 0.8 covers text up to 60% of the page width, which the measures
     below make unreachable. The full `--rake` was tried first and overshot
     by 48% - a 505px band where 470 does it, with the content stranded in
     the top half and the rest empty.

     IT MUST BE THIS TOKEN, not a percentage of the band. The band's height
     comes from `--header-h` and vh; the rake comes from vw. A flat "20%
     taller" is correct at one window and wrong at the next, and the two
     drift apart exactly on the wide screens where the rake is deepest. */
  padding-block: calc(var(--header-h) + clamp(2.5rem, 7vh, 5.5rem))
                 calc(clamp(2.5rem, 7vh, 5.5rem) + var(--rake) * 0.8);

  /* THE RAKED EDGE, AND IT STRAIGHTENS AS YOU SCROLL.
     Same shape and the same number as the register's rake into the footer,
     so the page's raked boundaries agree: `clamp(48px, 7.6vw, 132px)`, about
     5.2 degrees at 1440. SS8b settled why a full-width edge cannot be 36 -
     across 1440px a 36-degree slope drops 1046px, which is taller than the
     screen, so the angle gives way and the shape wins.

     It falls toward the inline start, the same side the chamfer cuts, and
     RTL mirrors the polygon rather than the element.

     `--rake-now` is what `Mast` writes; with no JS it simply stays at
     `--rake` and the edge is a static angle, which is a correct page.     */
  --rake: clamp(48px, 7.6vw, 132px);
  --rake-now: var(--rake);
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 calc(100% - var(--rake-now)));
}
[dir="rtl"] .mast {
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--rake-now)), 0 100%);
}
/* The band is the page's opening ground, so the section under it starts
   where the band ends rather than carrying its own header offset. The rake
   eats into that gap on one side, so a little is added back. */
.mast + .stage { padding-block-start: calc(var(--sp-section) + var(--rake-pad, 1rem)); }
.mast__t {
  margin-block-start: clamp(.6rem, 1.6vh, 1.1rem);
  /* NO MEASURE HERE, and that was tested rather than assumed. A 22ch cap
     was added to make the 0.8 rake factor provable and it wrapped the
     Arabic masterplan title onto two lines - a composition the client had
     already approved as one. The factor does not need it: even a title
     running the FULL shell clears, because the base padding carries most of
     the requirement. Worked at three widths, required against available:
       1500x900   101 vs 154      2560x1080  126 vs 181
       1000x900    74 vs 124
     If a future title does wrap badly, cap it on that page, not here. */
  /* smaller than a section heading - this is a document title, not a
     display moment, and the band is already carrying the emphasis */
  font-size: clamp(1.9rem, 3.1vw, 4.2rem);
  line-height: var(--lh-head, 1.12);
  color: var(--on-ink);
}
/* The sentence under the title on the four content pages. The privacy page
   has no equivalent - its version line and authority note do that job - so
   this is a separate class rather than a reuse of `.policy__meta`. */
.mast__lead {
  margin-block-start: clamp(.85rem, 2vh, 1.35rem);
  margin-inline: auto;
  max-inline-size: 56ch;
  font-size: var(--fs-doc);
  line-height: 1.75;
  color: var(--on-ink-mute);
}
html[lang="ar"] .mast__lead { font-family: var(--font-arabic); font-weight: 300; }

.policy__meta {
  margin-block-start: .95rem;
  font-size: var(--fs-xs); letter-spacing: .06em; text-transform: uppercase;
  color: var(--on-ink-mute);
}
html[lang="ar"] .policy__meta { text-transform: none; letter-spacing: .01em; font-family: var(--font-arabic); }

/* The authority note lives in the band, under the version line: it is the
   first thing a reader needs and the last thing they should have to hunt
   for. Quiet, centred, no panel - a bordered box inside a dark band reads
   as an alert, and this is a statement of record. */
.policy__note {
  margin-block-start: 1.15rem;
  margin-inline: auto;
  max-inline-size: 56ch;
  font-size: var(--fs-doc);
  line-height: 1.7;
  color: var(--on-ink-mute);
}
html[lang="ar"] .policy__note { font-family: var(--font-arabic); }

/* THE READING COLUMN, centred on the page.
   `--fs-doc` is its own token rather than `--fs-sm`, because the buttons
   and notes that `--fs-sm` sets have no reason to follow a document down.
   Calibrated the way SS8h requires - a vw term that keeps the physical size
   constant under browser zoom, with a ceiling far enough out not to bind
   until about 3,000px. 15.5px at 1500, 17.6px at 1920. */
:root { --fs-doc: clamp(.9rem, .5vw + .5rem, 1.45rem); }

/* No top margin. The section own padding is the separation from the
   band, and the two together measured 153px where 99 was the design. */
.policy__body {
  margin-inline: auto;
  max-inline-size: 62ch;
}
.policy__body p {
  font-size: var(--fs-doc); line-height: 1.95; color: var(--ink-800);
}
.policy__body p + p { margin-block-start: 1.05rem; }
.policy__body h2 {
  font-size: calc(var(--fs-doc) * 1.32); line-height: 1.45;
  color: var(--head, var(--ink)); font-weight: 600;
  margin-block: clamp(2.4rem, 5.5vh, 3.4rem) 1rem;
  padding-block-end: .6rem; border-block-end: 1px solid var(--hair);
}
html[lang="ar"] .policy__body h2 { font-weight: 700; font-family: var(--font-display-ar); }
.policy__body ul {
  margin-block: .8rem 0; padding-inline-start: 1.15rem;
  display: grid; gap: .5rem;
}
.policy__body li {
  font-size: var(--fs-doc); line-height: 1.8; color: var(--ink-800);
}
.policy__body li::marker { color: var(--accent); }
.policy__body a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
html[lang="ar"] .policy__body { font-family: var(--font-arabic); }
/* ARABIC SETS HEAVIER AT THE SAME NOMINAL WEIGHT. Readex Pro's 400 carries
   more ink per line than Inter's does, so a page that is almost entirely
   body text reads as a slab in Arabic and as paragraphs in English - which
   is exactly the note that came back. 300, on the client's call, and only
   in Arabic. Readex Pro is variable 200-700, so this is the real weight
   rather than a synthesised one. The headings keep their own weight. */
html[lang="ar"] .policy__body p,
html[lang="ar"] .policy__body li { font-weight: 300; }

@media (max-width: 760px) {
  /* The reading column is already the whole width here, and `.stage > .shell`
     centres headings on a phone - but a legal paragraph centred is unreadable,
     so the body keeps its reading edge. The note stays centred: it is one
     short sentence in the band, not a clause. */
  .policy__body, .policy__body p, .policy__body li { text-align: start; }
  .policy__body { max-inline-size: none; }
}

/* ---------- 32. THE SENT PANEL ------------------------------------------
   The one moment on this site where someone has handed something over. It
   used to be a line of text under the button: below the fold on a short
   window, identical to the error state but for its colour, and with the form
   still sitting there full of their details.                              */
.sent {
  position: fixed; inset: 0; z-index: calc(var(--z-menu) + 1);
  display: grid; place-items: center;
  padding: var(--gutter);
  background: rgba(18, 18, 18, .62);
  -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
  opacity: 0; visibility: hidden;
  transition: opacity 420ms var(--ease-out), visibility 0s linear 420ms;
}
.sent.is-on {
  opacity: 1; visibility: visible;
  transition: opacity 420ms var(--ease-out), visibility 0s;
}
.sent__card {
  inline-size: min(34rem, 100%);
  display: grid; justify-items: center; text-align: center;
  gap: clamp(.9rem, 2.2vh, 1.4rem);
  padding: clamp(2rem, 5vh, 3.25rem) clamp(1.5rem, 4vw, 3rem);
  background: var(--beige); color: var(--on-paper);
  border-radius: 5px;
  box-shadow: 0 40px 80px -30px rgba(18, 18, 18, .55);
  /* arrives from just below, so it reads as rising into place rather than
     appearing - transform only, one composited layer */
  transform: translateY(14px) scale(.985);
  transition: transform 520ms var(--ease-lux);
}
.sent.is-on .sent__card { transform: none; }

.sent__mark { inline-size: 54px; block-size: 54px; color: var(--accent); display: block; }
.sent__mark svg { inline-size: 100%; block-size: 100%; }
/* THE TICK DRAWS ITSELF, which is the whole reason this is an SVG and not a
   glyph: 44 and 26 are the real path lengths, measured, not guessed. */
.sent__ring { stroke-dasharray: 133; stroke-dashoffset: 133; }
.sent__tick { stroke-dasharray: 26; stroke-dashoffset: 26; }
.sent.is-on .sent__ring { animation: sentRing 620ms var(--ease-out) 120ms forwards; }
.sent.is-on .sent__tick { animation: sentTick 380ms var(--ease-out) 580ms forwards; }
@keyframes sentRing { to { stroke-dashoffset: 0; } }
@keyframes sentTick { to { stroke-dashoffset: 0; } }

.sent__t { font-size: clamp(1.5rem, 3.4vw, 2.1rem); line-height: 1.25; color: var(--head, var(--ink)); }
.sent__b { font-size: var(--fs-sm); line-height: 1.7; color: var(--ink-800); max-inline-size: 34ch; }
html[lang="ar"] .sent__b { font-family: var(--font-arabic); }
.sent__row {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: .7rem; margin-block-start: .4rem;
}
/* the WhatsApp route keeps its own green - it is a third-party mark and the
   point of it is that it is recognised before it is read */
.sent__wa { --bwipe: #1FA855; --bhov: #fff; background: #1FA855; color: #fff; border-color: #1FA855; }

@media (prefers-reduced-motion: reduce) {
  .sent, .sent__card { transition: none; }
  .sent__ring, .sent__tick { stroke-dashoffset: 0; animation: none; }
}

/* ---------- 33. SENDING -------------------------------------------------
   The second between pressing and the panel. The endpoint is a Google Apps
   Script round trip, so it is a real second or two - long enough to press
   twice and long enough to wonder whether anything happened.             */
.reg__submit.is-busy { cursor: progress; opacity: 1; }
.reg__submit.is-busy .btn__i { opacity: 0; }
/* The arrow's slot, so nothing jumps when one becomes the other. */
.reg__submit.is-busy::after {
  content: ''; position: absolute;
  inset-inline-end: clamp(2rem, 4vw, 3rem);
  inline-size: 1.15em; block-size: 1.15em;
  margin-block-start: -.05em;
  border: 2px solid currentColor; border-top-color: transparent;
  border-radius: 50%;
  animation: sendSpin .78s linear infinite;
}
@keyframes sendSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .reg__submit.is-busy::after { animation-duration: 2.4s; }
}

/* ---------- 32. THE 404 ------------------------------------------------
   One screen, nothing else on it. The brief was "bigger and in the
   limelight", and what carries that is the lockup: 4, the mark, 4 - the
   ring of ten banners around a hollow is the one glyph in this identity
   that is already an O.

   THE WEIGHT IS MEASURED, NOT CHOSEN. Rendered to a canvas and read back:
   the mark's own line is .0588em at this size, against Almarai's stem of
   .058 at 300, .084 at 400, .118 at 700 and .146 at 800. The first draft
   ran 800 - two and a half times the mark's line - and read as a heavy
   number with a delicate ornament dropped into it.

   400 SHIPS, and the 43% it is still over the mark is a deliberate trade:
   300 would match almost exactly but the site self-hosts 400 and 700 only,
   and SS3 does not allow a third-party font host. One more woff2 closes the
   gap whenever that is worth 25KB.                                       */
.e404 {
  position: relative;
  min-block-size: 100svh;
  display: grid; place-items: center;
  padding: calc(var(--header-h) * .5 + var(--sp-section)) var(--gutter) var(--sp-section);
  background: var(--ink);
  color: var(--on-ink);
  --fg: var(--on-ink);
  --head: var(--on-ink);
  /* the field is drawn in LIGHT on an ink ground; `--pat-col` defaults to
     `--ink`, which here would be the pattern painted in its own background */
  --pat-col: var(--on-ink);
  isolation: isolate;          /* or the z-index:-1 field falls behind the
                                  page's own ground and vanishes - SS8b */
  text-align: center;
  overflow: hidden;
}
.e404__in { display: flex; flex-direction: column; align-items: center; }

.e404__lock {
  display: flex; align-items: center; justify-content: center;
  gap: .03em; margin: 0; line-height: 1;
  font-family: var(--font-display-ar); font-weight: 400;
  font-size: clamp(5.5rem, 17vw, 18rem);
  color: var(--on-ink);
}
/* .78em against a digit height of about .72: a round shape has to run a
   little large to read the same size as a flat-topped one, and at 1:1 the
   mark sat visibly small between the fours. `em`, so the proportion holds
   at every size without a second breakpoint. */
.e404__wheel {
  inline-size: .78em; block-size: .78em;
  flex: none; display: block;
  color: var(--on-ink);
  animation: e404-turn 48s linear infinite;
}
/* It behaves like a wheel, because that is what it is - one turn every 48
   seconds, which you notice only if you stay. SS9 makes the same argument
   for the mark used as a ground. */
@keyframes e404-turn { to { transform: rotate(360deg); } }

.e404__t {
  margin-block-start: clamp(1.5rem, 4vh, 2.75rem);
  font-size: var(--fs-h2);
  line-height: var(--lh-head, 1.3);
  color: var(--on-ink);
}
.e404__b {
  margin-block-start: clamp(.9rem, 2vh, 1.35rem);
  max-inline-size: 46ch;
  font-size: var(--fs-doc);
  line-height: 1.9;
  color: var(--on-ink-mute);
}
html[lang="ar"] .e404__b { font-family: var(--font-arabic); font-weight: 300; }

/* A SOLID BUTTON ON A DARK GROUND HAS TO INVERT, and nothing on the site
   needed that until now. `.btn--solid` is `--bbg: var(--ink)` - Midnight
   Teal, which is this section's own background, so the button rendered as
   bare text on an invisible box. Measured before the fix: fill
   rgb(0,36,38) against a section of rgb(0,36,38).

   Every button on the site reads these four tokens (SS9), so inverting them
   is the whole change - no new rule for the shape, the chamfer or the
   hover flood. Label ink on Sandstone measures 9.9:1, and the hover lands
   Sandstone on ink at the same 9.9:1.

   Scoped to `.e404` rather than made a modifier: one user is not a pattern.
   If a second dark section ever wants a filled button, lift these four
   lines into `.btn--solid-onink` then. */
.e404 .btn--solid {
  --bbg: var(--on-ink);
  --bfg: var(--ink);
  --bwipe: var(--ink);
  --bhov: var(--on-ink);
  border-color: var(--on-ink);
}
.e404__row { margin-block-start: clamp(1.75rem, 4.5vh, 2.75rem); justify-content: center; }
.e404__sig {
  margin-block-start: clamp(2.5rem, 6vh, 4rem);
  font-size: var(--fs-xs); letter-spacing: .16em;
  color: var(--on-ink-mute);
}
html[lang="ar"] .e404__sig { letter-spacing: .04em; }

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