/* =========================================================================
   Components — authored fresh for R2.

   v1's components.css (1,263 lines) was deliberately NOT carried across.
   It encoded v1's card, slider, filter-bar, testimonial and nav patterns,
   none of which survive the R2 structure change. Carrying it would have
   meant building R2 out of v1's vocabulary while calling it a redesign.

   What DID carry, and what this file may therefore rely on:

     tokens.css         the v1.2 type scale, weights, letter-spacing,
                        measures (E11-corrected) and the neutral colour
                        ramp, plus the .t-* type-role classes
     tokens-layout.css  spacing (--gap- and --pad- aliases), radius,
                        motion, z-index
     base.css           reset + element defaults + .eyebrow / .lead
     layout.css         .container, .section, .grid, .split, aspect ratios

   Rules for anything written here:

     - No raw colour. stylelint's color-no-hex allows hex only in
       tokens.css. If a component needs a colour, it needs a token, and a
       new token needs a guideline revision — not a local literal.
     - No px font-sizes. rem/em only.
     - No physical direction properties (left/right/margin-left/…) and no
       text-align: left|right. Use logical properties throughout; the site
       has to flip to dir="rtl" without a second stylesheet.
     - Prose columns take --measure-body / --measure-body-lg. Do not
       hand-set a max-width on running text.
     - Hierarchy comes from size, weight and space — never from opacity
       (v1.2 §9.16).
   ========================================================================= */


/* =========================================================================
   THE CHROME AND THE MENU OVERLAY
   Spec: _bmad/wds/D-UX-Design/09-menu.md · v1.3 E13.1–E13.4 · v1.2 §4.7, §8

   The overlay is the site's ONLY navigation surface, at every width. It is
   not a mobile fallback for a desktop bar, and there is no width at which
   navigation lives somewhere else.
   ========================================================================= */

/* -------------------------------------------------------------------------
   The chrome (Board 17, Session K — H1-a/b/c; amends E13.2's reach)

   Geometry first (P-2): both objects are content plus ONE clear space of
   padding, flush at the top edge — the rule §8 already has, extended to
   the pair instead of a fourth number invented for the pill. The plate is
   taller because its contents are taller; that is a consequence.

   Position (G-2): the chrome sits on the CONTENT AXIS, not at a fixed
   80px. --gutter-desktop is a content token; borrowed as a chrome offset
   it was neither a bleed nor an alignment — 400px adrift at 1920, zero
   only at exactly 1120. The offset below is the .container's own text
   edge: half the leftover beyond --container-max, plus the gutter. The %
   resolves against the viewport, which is also what centres .container.

   The field (ground-aware — the E13.2 permanent field is now the plate's
   rule alone): BOTH HALVES ship fielded — the safe state, correct on
   every ground — and js/chrome.js removes the field only while the band
   under the chrome is clean paper. Three grounds: paper (no field, ink
   strokes and the black master), black (field dissolves — identical either
   way), photograph (the field returns; §8 bars identity on bare
   photography).

   🔴 THE PLATE JOINED THE TRIGGER ON 20 AUG 2026 — E8.1's OPEN ACTION IS
   DISCHARGED. It kept a permanent field for as long as there was only one
   master: the pill's strokes are CSS and could turn to ink any day, but
   the mark is artwork and §8 bars deriving the inverted master with a
   filter. ProArc supplied both masters, so the derivation is not needed
   and the field can go. E8.2's "on #FFFFFF the plate is a visible black
   rectangle" is amended with them — see the guideline.
   ------------------------------------------------------------------------- */

.chrome-trigger,
.chrome-plate {
  --chrome-axis: calc(max(0px, (100% - var(--container-max)) / 2) + var(--gutter-desktop));

  /* ⚠️ THE MARK HAS ONE SIZE PER BREAKPOINT SINCE 21 Aug — the two-state
     step is retired; see the note further down. The paragraph below is the
     record of the decision it replaced.

     The mark's two states (2 Aug follow-up, decided live): identity is
     loudest at ARRIVAL — 160 while the page's opening band is under the
     chrome — and steps to the reading size once it has scrolled past.
     The reading size is 96, §8's own desktop floor (tuned down from a
     first pass at 128 — Mahesh, on the live site): the 96 complaint was
     about ARRIVAL, and the arrival state now answers it. Tablet steps
     128→96 the same way; mobile is already at 96 and stays constant.
     Dewan-style hide-on-scroll was considered and declined: the trigger
     is the site's only navigation control and never retreats.

     🔴 THAT DECLINE WAS REVERSED ON 4 AUG (Mahesh), AND NOT ON TASTE. The
     permanent chrome covers text ink on every long page: measured over ink
     rather than boxes, across five pages and every half viewport of scroll,
     the trigger buries 4.53% of all visible lines at 1440 and 8.00% at 375,
     the plate 2.89% and 4.89%. /ajman's authority sentence was severed
     mid-word at display size and a 375 ledger row read "…llem Park Private
     S", cut at both ends. E13.2's permanence is what bought that, and its
     amendment is recorded in the guideline as E13.2a — see the retreat rules
     below. The reachability half of the decline's reason is kept: the
     trigger returns on any upward scroll, on focus, and whenever the overlay
     opens, so it is never more than one gesture away. */
  --mark-size: var(--logo-size-desktop);

  /* The inset, named once so both offsets below and the breakpoints stay
     derived rather than hand-tuned in two places (H4-h's shape). It was the
     FIELD's padding until 21 Aug; it is now §8's clear space doing the only
     job it ever really had — holding the page's ink off the mark's. */
  --chrome-pad: var(--logo-clear-space-desktop);

  position: fixed;
  inset-block-start: 0;
  z-index: var(--z-nav);
  padding: var(--chrome-pad);
}

/* C-1: the three lines on one side... */
.chrome-trigger {
  /* 🔴 THE AXIS IS THE BARS' INK, NOT THE BUTTON'S BOX (Mahesh, Session
     XXXV, on the rendered page): "the hamburger looks a little inside in
     all pages ... visually on page it is misaligned most of the time."
     Measured, the box sat dead on the text edge and the three lines sat
     25px inside it — the field's own padding, which the trigger inherited
     by SHARING A SELECTOR with the plate rather than by owing it. §8's
     clear space is the MARK's rule; the trigger is not the mark. So the
     box is pulled out by exactly its own padding and the ink lands on the
     axis, while the field still surrounds it. Logical property, so the
     RTL flip is carried by --chrome-axis alone and needs no second rule. */
  inset-inline-start: calc(var(--chrome-axis) - var(--chrome-pad));
  display: flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 44px;   /* §4.7's hit target — the padded box is 73×67 */
  min-block-size: 44px;
  border: none;
  appearance: none;
  transition: background-color var(--dur-fast) var(--ease-out);
}

/* ...the logo on the other. The plate is also the Home door: inside the
   open overlay its field has dissolved and the letterforms are the way
   home, which is why the nav lock needs no Home item. */
.chrome-plate {
  /* 🔴 THE AXIS IS THE MARK'S INK, NOT THE PLATE'S BOX — Session XXXV's
     ruling arriving at the other corner, and it only became visible when
     the field went (21 Aug).

     The trigger was pulled out by its own padding in XXXV because Mahesh
     read the page and saw three strokes sitting 25px inside the text edge.
     The plate had the identical offset and nobody could see it, because a
     black rectangle WAS on the axis and the reader had no way to know the
     letterforms were not. Remove the rectangle and the mark is a wordmark
     floating 25px in from every other right edge on the page.

     Same fix, same shape, same logical property — --chrome-axis carries the
     RTL flip alone and this needs no second rule. §8's clear space is not
     spent by it: the padding still holds, it is simply no longer measured
     from the wrong edge. */
  inset-inline-end: calc(var(--chrome-axis) - var(--chrome-pad));

  /* GRID, so the two masters share one cell. The plate's box is therefore
     one mark's box whichever is painted, and the ground swap can never
     move the layout. */
  display: grid;
}

.chrome-plate__mark {
  grid-area: 1 / 1;
  inline-size: var(--mark-size);
  block-size: auto;

  /* One property animates — the padding holds at 25 (above the 24 floor
     at every size), so the box follows the img and nothing else moves. */
  transition: inline-size var(--dur-fast) var(--ease-out);
}

/* The paper master is the one the script ARMS, so it ships hidden: a page
   whose script never runs shows the white mark on its field, which is
   legible on all three grounds. Same doctrine as chrome-ink, chrome-quiet
   and the retreat — the shipped state is the safe one.

   `visibility`, not `display` or `opacity`. `display: none` would drop the
   element from the grid and let a missing file resize the cell; opacity
   would leave the hidden master in the accessibility tree and paint it
   over the visible one at every intermediate value during a transition. */
.chrome-plate__mark--on-paper { visibility: hidden; }

/* 🔴 THE MARK NO LONGER STEPS DOWN, AND THAT IS A RULING ON THE RENDERED
   PAGE (Mahesh, 21 Aug): "After practically seeing, can we avoid logo
   resize? anyway it is a disappearing after sometime and it is making it
   look odd now."

   The two-state mark was decided on 2 Aug — 160 at ARRIVAL, stepping to 96
   once the page had scrolled past its opening band. It survived three weeks
   because NOBODY EVER SAW IT HAPPEN: from 4 Aug the plate was keyed on the
   arrival, so it left the screen before the step and came back after it,
   and from 20 Aug `plate-away` guaranteed that ordering. E13.2b put the
   plate back on screen through the body and the step became visible for the
   first time — and a mark that resizes on its way to disappearing is
   fidgeting, not hierarchy.

   📌 A DECISION CAN SURVIVE ON THE STRENGTH OF BEING INVISIBLE. This one was
   never wrong on the page it was tuned against; it was simply never rendered
   in the state it described. The identity size is now one value per
   breakpoint (160 / 128 / 96) and `--mark-size-quiet` is deleted with the
   rule that read it. */

/* -------------------------------------------------------------------------
   THE RETREAT — E13.2a (4 Aug 2026, Mahesh). The chrome uncovers the page.

   The page SHIPS with the chrome present, exactly as it ships fielded and
   ships in the arrival state: `html.chrome-retired` is armed by the script,
   so a script that never runs leaves a complete page rather than a page with
   no way into the menu. Same doctrine as chrome-ink and chrome-quiet, and it
   is the reason the retreat is safe to build on a scroll listener at all.

   TRANSFORM, NOT VISIBILITY OR DISPLAY. A retired element must stay in the
   accessibility tree and stay focusable — that is what lets a keyboard reader
   tab to the trigger and have `focusin` bring it back, which is the half of
   the original decline's reason that is kept rather than reversed. `display:
   none` or `visibility: hidden` would make the site's only navigation control
   genuinely unreachable for the length of a downward scroll.

   NO RTL RESTATEMENT, AND THAT IS DELIBERATE RATHER THAN FORGOTTEN. The
   retreat is on the BLOCK axis, which does not flip with direction — unlike
   the label wipe on /ajman, whose physical `clip-path` insets had to be
   restated for RTL and whose release then lost a specificity contest to that
   restatement. A transform is also not one of hard rule 5's physical
   direction properties; there is no logical form to reach for here.

   NO REDUCED-MOTION BRANCH, ALSO DELIBERATE. tokens.css clamps every
   transition-duration to 0.01ms under `prefers-reduced-motion: reduce`, so
   the retreat becomes instant on its own. The retreat is FUNCTION — it
   uncovers text — and only its travel is motion; a reader who asked for less
   motion still wants the words. A branch that suppressed the retreat entirely
   would hand exactly that reader the buried ink this rule exists to clear.
   ------------------------------------------------------------------------- */

.chrome-trigger,
.chrome-plate {
  /* Composited, and it moves nothing else: both are position:fixed, so no
     other box can be reflowed by their travel. */
  transition: transform var(--dur-fast) var(--ease-out);
}

/* The trigger already animates its background for the ground swap; naming
   both here keeps one transition declaration per element rather than two
   that race. */
.chrome-trigger {
  transition:
    transform var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

/* 🔴 THE PLATE IS AT THE PAGE'S OPENING AND NOWHERE ELSE (4 Aug, Mahesh —
   E13.2a amended the same day it was written).

   The retreat's first form brought the plate BACK on any upward scroll, and
   back it came straight over the ledger: "Habitat Schoo", cut mid-word.
   Measured on /ajman at 1440, the plate covers 146px of a 339px ledger
   column — 43% of it — so moving the TEXT clear of the logo is not a gap,
   it is a redesign, and the building names would wrap. The only way to put
   air between the wordmark and the words is for the wordmark not to be
   there while they are being read.

   So the plate is keyed on the ARRIVAL, not on a direction: present while
   the reader is at the page's opening band, gone for the whole body,
   back when they return to the top. Identity is made once, at the top of
   each of fifty-eight pages, and does not reintroduce itself over the
   argument.

   📌 IT NEEDS NO SCROLL LISTENER. `chrome-quiet` is computed by the arrival
   observer this file already owned before E13.2a — so of the two halves of
   the retreat, only the TRIGGER's costs a listener. The plate's half is the
   cheaper thing that was measurable all along. */
/* 🔴 THE PLATE TRAVELS WITH THE TRIGGER NOW (Mahesh, 21 Aug): "keep the
   logo and hamburger longer when scrolling (and vice versa appear sooner
   when scrolling back)."

   That reverses the note above, and the thing that makes it affordable is
   the field's removal in the same ruling. The plate was keyed on the
   ARRIVAL because bringing it back on scroll-up brought a BLACK RECTANGLE
   back over the ledger — 146px of a 339px column on /ajman, "Habitat Schoo"
   cut mid-word. What returns now is a wordmark with nothing behind it,
   sized to the reading state, and the measurement that closed the door was
   a measurement of the rectangle.

   `plate-away` is deleted with it. It existed only because a field is a
   rectangle and cannot be half-black while a ground boundary crosses it, so
   the plate had to leave a band early; ink can flip at the midpoint instead.
   See js/chrome.js.

   At ALL widths, unlike the trigger below: the plate is identity and can
   simply be absent, the trigger is navigation and is permanent under 780. */
html.chrome-retired .chrome-plate {
  transform: translateY(-100%);
}

/* 🔴 THE PLATE IS ABSENT OVER A PHOTOGRAPH, AND THIS IS WHAT "NO BOX
   ANYWHERE" TURNED INTO WHEN IT WAS MEASURED (21 Aug).

   Built bare exactly as ruled, the mark came back at 1.22–1.78:1 over
   /projects' photo grid — invisible, on the surface that carries 46 of the
   site's photographs. The failures are architecture against sky: both ends
   of the range inside one 210×99 box, which no single ink survives, so
   choosing the master per image does not reach it either.

   The choice over a photograph is therefore a box or nothing, and E13.2a
   already says which one identity takes: "the plate is identity and can
   simply be absent, the trigger is navigation and cannot." So the plate
   leaves and the trigger fields — see the rule further down. ProArc's
   complaint is answered in full for the WORDMARK, which is what they wrote
   about: there is now no black rectangle behind it on any ground.

   Same travel as the retreat, so the two compose rather than fight: a plate
   that is both retired and over media is simply away, once. */
html.plate-on-media .chrome-plate {
  transform: translateY(-100%);
}

/* 🔴 THE TRIGGER'S BAND IS A DECISION, AND IT IS THE INVERSE OF WHAT THE INK
   MEASURES. Mahesh, 4 Aug, with the measurement in front of him: the trigger
   stays PERMANENT below 780 — the narrow widths, where the overlay is the
   only navigation and the reader scrolls most — and retires at 780 and above.
   The ink says the opposite: the trigger buries 4.53% of visible lines at
   1440, 5.13% at 781 and 8.00% at 375, so this protects navigation exactly
   where the burying is worst. That cost is accepted, not overlooked, and it
   is written here so it is never "fixed" by someone reading the numbers
   alone.

   780, NOT 767 OR 768: the chrome moves on the LAYOUT's breakpoints, and a
   rule made of a token inherits that token's breakpoint. `--logo-size-*`
   steps to the mobile mark at 780, so the band where the trigger is permanent
   is exactly the band where it is already drawn at its mobile size. */
/* The TRIGGER keeps the direction-based retreat, and it must: it is the only
   way into the menu, so it may retreat from a reader scrolling away and has
   to return to one scrolling back. That is why the two halves are governed
   differently — the plate is identity and can simply be absent, the trigger
   is navigation and cannot. */
@media (min-width: 780px) {
  html.chrome-retired .chrome-trigger {
    transform: translateY(-100%);
  }
}

/* The overlay owns the chrome while it is open — the trigger IS the close
   control and the plate IS the way home (see the note above). Neither can be
   retired underneath it. This sits after the retreat rules on purpose, the
   same way the is-menu-open ground rules sit after the chrome-ink ones: one
   direction of override, nothing to desync. */
html.is-menu-open .chrome-plate,
html.is-menu-open .chrome-trigger {
  transform: none;
}

/* The bars, and the ✕ they become.

   E13.1 says the bars become a ✕ "in the same pill, in the same place", so
   they are not swapped for a glyph — the same three strokes rotate into it.
   No icon font, no second element to keep in sync, and no font-size for a
   mark that is not type.

   Three real elements rather than one plus two pseudo-elements: pseudo-
   elements are children of the bar they hang off, so their transforms would
   compose with its rotation and the ✕ would come out skewed. Siblings each
   carry exactly the transform they are given. */
/* The bars grow with the mark but far more slowly — three strokes are not
   a wordmark, and the pill must stay a target rather than becoming a
   second plate: 18 + (160 − 96) × 0.08 = 23. --bar-travel is the middle
   stroke's offset, (block-size − 1.5) / 2, and the ✕ reuses it so the
   cross always meets at the centre whatever the breakpoint says. */
.chrome-trigger__bars {
  --bar-travel: 7.875px;

  position: relative;
  display: block;
  inline-size: 23px;
  block-size: 17.25px;
}

.chrome-trigger__bars i {
  position: absolute;
  inset-inline: 0;
  display: block;
  block-size: 1.5px;
  background: var(--color-on-dark);
  transition:
    transform var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.chrome-trigger__bars i:nth-child(1) { inset-block-start: 0; }
.chrome-trigger__bars i:nth-child(2) { inset-block-start: var(--bar-travel); }
.chrome-trigger__bars i:nth-child(3) { inset-block-start: calc(var(--bar-travel) * 2); }

/* The outer two travel to the middle and cross; the middle one closes to
   nothing. It scales rather than fades, because hierarchy — and everything
   else — never comes from opacity (§9.16). */
.chrome-trigger[aria-expanded="true"] .chrome-trigger__bars i:nth-child(1) {
  transform: translateY(var(--bar-travel)) rotate(45deg);
}

.chrome-trigger[aria-expanded="true"] .chrome-trigger__bars i:nth-child(2) {
  transform: scaleX(0);
}

.chrome-trigger[aria-expanded="true"] .chrome-trigger__bars i:nth-child(3) {
  transform: translateY(calc(var(--bar-travel) * -1)) rotate(-45deg);
}

.chrome-trigger:focus-visible,
.chrome-plate:focus-visible {
  outline: 2px solid var(--color-on-dark);
  outline-offset: -4px;
}

/* On a paper page the focus ring must not be white-on-white where it
   overhangs the field. It never does: the ring is inset. */

/* -------------------------------------------------------------------------
   Ground-aware states (H1-b)

   The page SHIPS in the safe state above — fielded, correct on every
   ground — and js/chrome.js arms .chrome-ink only while nothing but paper
   sits under the chrome's band. A blocked or broken script leaves a
   complete page rather than an illegible one: the same doctrine as the
   map, the drawing and the form panels. Removing the field moves nothing —
   the padded box still lays the strokes out; only the paint goes.

   The open overlay forces the black state, and it is settled HERE, by
   source order, never by the script — these rules sit after the ink ones
   so is-menu-open wins while both classes are on the root.
   ------------------------------------------------------------------------- */

html.chrome-ink .chrome-trigger__bars i { background: var(--color-ink); }
html.chrome-ink .chrome-trigger:focus-visible { outline-color: var(--color-ink); }

/* 🔴 THE ONE FIELD LEFT ON THE SITE, AND IT IS THE TRIGGER'S OVER MEDIA
   ONLY (21 Aug). Three CSS strokes are 1.5px tall: measured bare over
   photographs they ran 1.28–2.65:1 on four routes, and unlike the plate the
   trigger cannot answer that by leaving — it is the only way into the site's
   only navigation surface (E13.1). So §8's field returns for exactly the
   case §8 was written about, and for no other.

   It sits after chrome-ink deliberately: on a paper page with a photograph
   under the trigger both are candidates, and the field must win. */
/* 🔴 THE FIELD IS THE INK'S CHIP, NOT THE BUTTON'S BOX (Mahesh, 21 Aug, on
   the live page: "the box still appears when we scroll back").

   The first form of this painted the BUTTON, which is 73×67 because it
   carries §8's clear space as padding — four times the area its three
   strokes need, reading as a slab dropped on a photograph. The hit target is
   still the button and is untouched (§4.7); what shrinks is only the paint,
   so the bars do not move and the axis is unaffected.

   🔴 THREE BOX-FREE DEVICES WERE BUILT AND MEASURED BEFORE SETTLING FOR A
   SMALLER BOX, and all three failed on the same instrument (p33):
     bare strokes                      1.28 – 2.65:1
     white strokes + a drop-shadow     1.02 – 2.81:1
     mix-blend-mode: difference        1.28 – 2.11:1
   The shadow fails because a 1.5px stroke's halo falls OUTSIDE the stroke,
   so it barely darkens the ground the ink actually stands on. The blend
   fails because difference against mid-grey returns mid-grey, and a
   photograph is full of it. Against a 3.0 floor, none of them is a
   candidate; do not re-propose them without a number. */
html.chrome-fielded .chrome-trigger__bars::before {
  content: "";
  position: absolute;
  inset: -8px;
  z-index: -1;
  background: var(--color-surface-dark);
}

html.chrome-fielded .chrome-trigger__bars i { background: var(--color-on-dark); }
html.chrome-fielded .chrome-trigger:focus-visible { outline-color: var(--color-on-dark); }

/* 🔴 THE PLATE IS KEYED ON `plate-ink`, NOT ON `chrome-ink`, AND THE TWO
   ARE DIFFERENT QUESTIONS (Mahesh, 20 Aug 2026). chrome-ink asks whether
   the band is nothing but paper; plate-ink asks only whether a DECLARED
   dark band is under it, counting a photograph as paper. The white master
   belongs to a black ground and not to media — see js/chrome.js.

   The record pages are why: their hero photograph runs down the left of
   the arrival while the plate sits on the white spec table at the right,
   so a shared answer fielded the plate over white on all 47 of them.

   The two masters swap rather than recolour — nothing here touches ink. */
html.plate-ink .chrome-plate__mark--on-dark { visibility: hidden; }
html.plate-ink .chrome-plate__mark--on-paper { visibility: visible; }
html.plate-ink .chrome-plate:focus-visible { outline-color: var(--color-ink); }

/* The overlay covers the page, so whatever the trigger was standing on is
   no longer visible — but `chrome-fielded` is computed from geometry rather
   than from what can be seen, so it is still armed underneath. Cleared here
   by source order, exactly as the ground rules are: one direction of
   override, nothing to desync. */
html.is-menu-open .chrome-trigger { background: transparent; }
html.is-menu-open .chrome-trigger__bars i { background: var(--color-on-dark); }
html.is-menu-open .chrome-trigger:focus-visible { outline-color: var(--color-on-dark); }

/* The open overlay is black, so the plate returns to the white master —
   which is now the whole of it, the field having been the thing that used
   to "dissolve into the overlay" and make the letterforms the way home.
   They are the way home directly now. Settled HERE by source order rather
   than by the script, exactly as the trigger's pair above: chrome-ink may
   well be armed on the page underneath, and one direction of override means
   there is nothing to desync. */
html.is-menu-open .chrome-plate__mark--on-dark { visibility: visible; }
html.is-menu-open .chrome-plate__mark--on-paper { visibility: hidden; }
html.is-menu-open .chrome-plate:focus-visible { outline-color: var(--color-on-dark); }


/* -------------------------------------------------------------------------
   The nib — the white hairline that leads the wipe (§9.1)
   ------------------------------------------------------------------------- */

.menu-nib {
  position: fixed;
  inset-block: 0;
  inset-inline-start: 0;
  z-index: calc(var(--z-nav) - 1);
  display: none;
  inline-size: 1px;
  background: var(--color-on-dark);
  transform: scaleY(0);
  transform-origin: top;
  pointer-events: none;
}

.menu-nib.is-running {
  display: block;
  animation: menu-nib-run 420ms cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}

/* The line drops from the trigger's edge, then sweeps the viewport. In RTL
   it starts at the mirrored corner and runs the other way — one sign flip,
   no second keyframe set. */
[dir="rtl"] .menu-nib { --nib-direction: -1; }

@keyframes menu-nib-run {
  0%   { transform: scaleY(0) translateX(0); }
  24%  { transform: scaleY(1) translateX(0); }
  100% { transform: scaleY(1) translateX(calc(100vw * var(--nib-direction, 1))); }
}


/* -------------------------------------------------------------------------
   The overlay — full viewport, black, three regions (§4)
   ------------------------------------------------------------------------- */

.menu-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  overflow-y: auto;
  background: var(--color-surface-dark);
  visibility: hidden;

  /* Closed, the field is clipped away to the trigger's own edge. Opening
     transitions the clip, so the black arrives as a wipe travelling across
     the viewport rather than as a box appearing. */
  clip-path: inset(0 100% 0 0);
}

[dir="rtl"] .menu-overlay { clip-path: inset(0 0 0 100%); }

/* Armed: the hidden state is committed and painted. The class flip to
   .is-open happens only AFTER a forced reflow — arming and flipping in one
   style pass makes the browser treat the reveal itself as the transition,
   and nothing animates. */
.menu-overlay.is-armed { visibility: visible; }

.menu-overlay.is-open {
  clip-path: inset(0 0 0 0);
  transition: clip-path 420ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* Close runs the wipe back the way it came, faster. */
.menu-overlay.is-open.is-closing {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 300ms ease;
}

[dir="rtl"] .menu-overlay.is-open.is-closing { clip-path: inset(0 0 0 100%); }

/* Two columns, and the block axis is CLAIMED (8 Aug, Freya's cut).

   The fault this replaces was not a shortage of content — adding two new
   stages to the old grid made the dead share WORSE, 49.3% -> 53.6%, because
   a six-line list is shorter than the 28-mark field it displaced. The cause
   was structural: three columns in a SINGLE row leave no second row for
   `min-block-size: 100%` to distribute, so every region hung off the top
   padding and 44-49% of the frame at 1440 was never claimed at all. Below
   1024 the grid already collapsed to one column and two rows, which is why
   the narrow widths were fine and only desktop was wrong.

   So the identifiers leave their own column and become the FOOT of the nav
   column, and everything they release goes to the stage. Measured at 1440:
   dead below 49.3% -> 9.2%, and the 9.2% that remains is `--gutter-desktop`.
   It also repairs a clearance the old arrangement broke by construction —
   the identifiers sat 15.3px under the plate where the plate owes 25. */
.menu-overlay__regions {
  display: grid;
  grid-template-columns: 267px minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--gap-component);
  align-items: start;
  min-block-size: 100%;
  padding-block: 132px var(--gutter-desktop);

  /* The chrome's own axis (G-2): the open overlay and the chrome are one
     object — the trigger is its ✕, the plate its Home door — so the nav
     column shares the trigger's edge and the contact column the plate's.
     Without this the ✕ would sit 400px inside the nav's edge at 1920. */
  padding-inline: calc(max(0px, (100% - var(--container-max)) / 2) + var(--gutter-desktop));
}


/* -------------------------------------------------------------------------
   The nav list (§5) — measured, not estimated

   The column reserves 267px, the ceiling of the widest bold metric
   (CONTACT at 266.3px), so moving between pages never reflows the list.
   ------------------------------------------------------------------------- */

.menu-nav__row {
  position: relative;
}

/* Decorative rules under the items are list furniture — under EVERY item,
   never a marker. #333333 on black is 1.66:1, below 1.4.11's floor, which
   is exactly why it may separate sections but may not bound a hit target
   or carry a state. Here it does neither. */
.menu-nav__row::after {
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  display: block;
  block-size: 1px;
  background: var(--color-hairline-dark);
  transform: scaleX(0);

  /* transform-origin has no logical keywords, so the drawing edge is set as
     a percentage and flipped by direction below — the same one-value flip
     the nib uses. */
  transform-origin: 0 50%;
  transition: transform 300ms ease;
  content: "";
}

[dir="rtl"] .menu-nav__row::after { transform-origin: 100% 50%; }

/* The rules draw in sequence behind the wipe (§9.1). Four explicit delays
   rather than an nth-child formula: the nav lock is four items, and if a
   fifth ever appears the missing delay should be noticed, not interpolated. */
.menu-overlay.is-open .menu-nav__row::after { transform: scaleX(1); }
.menu-overlay.is-open .menu-nav__row:nth-child(1)::after { transition-delay: 400ms; }
.menu-overlay.is-open .menu-nav__row:nth-child(2)::after { transition-delay: 480ms; }
.menu-overlay.is-open .menu-nav__row:nth-child(3)::after { transition-delay: 560ms; }
.menu-overlay.is-open .menu-nav__row:nth-child(4)::after { transition-delay: 640ms; }

/* 🔴 THE ROWS SHARE THE COLUMN'S HEIGHT AT EVERY WIDTH — hoisted out of
   `min-width: 1025px` on 21 Aug (XLI). It was desktop-only, and below 1024
   the doors hugged their type while the overlay's spare height collected
   into a single void between the nav and the identifiers. The mechanism was
   already written and already approved; it simply never reached the width
   where the reader's thumb is. It is stated ONCE rather than restated in a
   mobile block, because two copies of one rule is how two widths start
   disagreeing about a surface.

   The 76px floor is what makes this safe to share: a viewport too short to
   give the rows anything degrades to exactly the layout this replaced.
   `position: relative` stays for the rail, which is still desktop's alone —
   the stage it belongs to is `display: none` below 1024. */
.menu-nav__list {
  position: relative;
  display: grid;
  grid-auto-rows: minmax(76px, 1fr);
  block-size: 100%;
}

/* The word STANDS ON its rule. Centred in a tall row the hairline floats
   free of the word and stops reading as list furniture. */
.menu-nav__item {
  display: flex;
  align-items: flex-end;
  block-size: 100%;
  padding-block: 0 var(--gap-inline);
  font-size: var(--fs-menu-nav);
  font-weight: var(--fw-light);
  line-height: var(--lh-nav);

  /* Zero, and not the hero tracking a 52px uppercase line would normally
     take. §5's four label metrics were measured at letter-spacing 0 and the
     267px column reserve is derived from them, so tracking the labels
     tighter would quietly invalidate the one number on this screen that was
     measured rather than estimated. It also keeps one rule across both
     languages, since Arabic tracking is always zero. */
  letter-spacing: var(--ls-body);
  color: var(--color-on-dark-secondary);
  text-transform: uppercase;
  transition: color var(--dur-fast) var(--ease-out);
}

/* The active state is weight and brightness only — never colour, and never
   an underline, which the row already owns as furniture.

   .is-lit answers "which of these four takes me back toward where I am",
   so the whole project block lights WORK. aria-current is separate and
   exact-match only: a project record is not the page the link leads to. */
.menu-nav__item.is-lit {
  font-weight: var(--fw-bold);
  color: var(--color-on-dark);
}

.menu-nav__item:hover,
.menu-nav__item:focus-visible {
  color: var(--color-on-dark);
}

.menu-nav__item:focus-visible {
  outline: 2px solid var(--color-on-dark);
  outline-offset: 4px;
}

/* Arabic has no weight 300 (§7.3), so the rest state is 400 and the
   rest→current contrast narrows to 400→700. Brightness carries more of the
   job in Arabic, which is why the active state has two signals. */
html:lang(ar) .menu-nav__item {
  font-weight: var(--fw-regular);
  text-transform: none;
}


/* -------------------------------------------------------------------------
   The desktop arrangement (8 Aug) — the nav claims the block axis and the
   identifiers become its foot.

   Everything here is inside `min-width: 1025px` and nothing below it moves,
   because the ≤1024 overlay is nav + contact by decision — approved by
   Mahesh on the rendered 768/375 frames, with the alternative offered and
   declined. Only its distribution changes: `align-content: space-between`
   puts the identifiers on the floor rather than under the nav, which is a
   change of position and not of content (dead below 22.1% -> 4.8% at 1024,
   21.1% -> 3.3% at a true 375).
   ------------------------------------------------------------------------- */

@media (min-width: 1025px) {
  .menu-nav     { grid-column: 1; grid-row: 1; align-self: stretch; }
  .menu-contact { grid-column: 1; grid-row: 2; align-self: end; }
  .menu-centre  { grid-column: 2; grid-row: 1 / span 2; align-self: stretch; }

  /* 📌 `.menu-nav__list` and `.menu-nav__item` stood HERE until 21 Aug
     (XLI). They now sit unconditionally beside the nav's other rules —
     nothing about desktop changed, and the nav still stays at
     --fs-menu-nav at every width, so §5's measured 267px reserve is
     untouched. */

  /* THE RAIL — the one thing that moves between sections.

     A 2px line in the gutter that travels one row per step, so moving down
     the list is a single continuous gesture rather than four jump cuts. It
     is line-work drawing itself: the site's own vocabulary, and neither a
     fade nor a text animation, so nothing in the motion language reopens.

     It parks on the LIT row at rest, because the resting stage shows the
     page the reader is already on — a stage saying "About" with no row
     marked would be the ambiguity this whole change exists to remove. Where
     nothing is lit it is simply absent (scaleY(0)).

     25% is one row of four, which is the nav lock (§5, §11); the offset is
     derived from the gutter it sits in rather than chosen, so a change to
     --gap-component carries it. */
  .menu-nav__list::before {
    position: absolute;
    inset-block-start: 0;
    inset-inline-end: calc((var(--gap-component) + 2px) / -2);
    inline-size: 2px;
    block-size: 25%;
    background: var(--color-on-dark);
    transform-origin: 0 0;
    transform: translateY(calc(var(--rail-row, 0) * 100%)) scaleY(var(--rail-on, 0));
    transition: transform 220ms var(--ease-out);
    content: "";
  }

  [dir="rtl"] .menu-nav__list::before { transform-origin: 100% 0; }
}

/* Reduced motion leaves the rail AT its destination rather than halting it
   mid-travel — a finished state, not a stopped one. */
@media (prefers-reduced-motion: reduce) {
  .menu-nav__list::before { transition: none; }
}


/* -------------------------------------------------------------------------
   The centre (§4) — G-4, "the swap"

   The constellation at rest; the preview while WORK is hovered or focused.
   Never both: the centre holds one idea at a time. The swap is instant and
   has no animation of its own — a transition here would be a second
   animation competing with the open.
   ------------------------------------------------------------------------- */

/* Every occupant is stacked in one grid area, so the stage's box does not
   resize as the occupant changes and the four states share one optical
   centre — measured within 1.8px. That is the continuity guarantee: the
   reader moves down the list and the picture changes without the eye
   being thrown. */
.menu-centre {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-content: center;
  min-block-size: 0;
}

.menu-centre > * { grid-area: 1 / 1; }

/* THE STAGE — §4.4's own revision, taken 8 Aug.

   That clause refused a per-item panel because SERVICES, ABOUT and CONTACT
   had "no honest preview content", and recorded the door as "open for a
   later revision if content ever exists". It does, and the clause's test —
   spend a page's STRUCTURE, never its picture — is what chooses it:
   Services' six block names, About's own support line, Contact's three
   authored steps. Every string is read off the page that authors it by
   build/lib/menu.js, which throws rather than shipping a shorter list.

   The resting occupant is the page the reader is ALREADY on; where no nav
   item is lit (Home, /ajman, /careers) it is `--rest`, the story's opening
   sentence. Its pair, `--about`, is the line immediately beneath that one
   on /about — two halves of one paragraph, which is why they can share a
   surface without colliding. */
.menu-stage,
.menu-preview { display: none; }

.menu-stage {
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-h2);
  font-weight: var(--fw-light);
  line-height: var(--lh-heading, 1.3);
  color: var(--color-on-dark);
  max-inline-size: var(--measure-body-lg);
}

.menu-stage--services,
.menu-stage--contact { display: none; }

.menu-stage__row { padding-block: var(--gap-inline) 0; }
.menu-stage__row:first-child { padding-block-start: 0; }

/* One occupant at a time — never two (Mahesh: "both look crowded"). The
   swap stays instant; §4 rule 1 prices a transition here against the OPEN,
   and the rail is what carries the movement. */
.menu-overlay[data-stage="work"]     .menu-preview       { display: block; }
.menu-overlay[data-stage="services"] .menu-stage--services { display: block; }
.menu-overlay[data-stage="about"]    .menu-stage--about    { display: block; }
.menu-overlay[data-stage="contact"]  .menu-stage--contact  { display: block; }
.menu-overlay[data-stage="rest"]     .menu-stage--rest     { display: block; }

.menu-preview__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--gap-block);
}

/* A plain grid — no bordered container (§4.7's card-in-card prohibition). */
.menu-preview__item {
  display: block;
}

.menu-preview__thumb {
  inline-size: 100%;
  block-size: auto;
}

.menu-preview__name {
  display: block;
  margin-block-start: var(--gap-inline);
  font-size: var(--fs-metadata);
  font-weight: var(--fw-regular);
  line-height: var(--lh-metadata);
  color: var(--color-on-dark-body);
}

.menu-preview__item:hover .menu-preview__name,
.menu-preview__item:focus-visible .menu-preview__name {
  color: var(--color-on-dark);
}

.menu-preview__item:focus-visible,
.menu-preview__exit:focus-visible {
  outline: 2px solid var(--color-on-dark);
  outline-offset: 4px;
}

/* The one piece of wayfinding in the panel, and it carries no count:
   "View All 47 Projects" is dead (E12). */
.menu-preview__exit {
  display: inline-block;
  margin-block-start: var(--gap-block);
  font-size: var(--fs-body);
  font-weight: var(--fw-medium);
  line-height: var(--lh-body);
  color: var(--color-on-dark);
}

/* The arrow is a glyph, so it flips with the language rather than being
   mirrored by a transform. */
[dir="rtl"] .menu-preview__arrow {
  display: inline-block;
  transform: scaleX(-1);
}


/* -------------------------------------------------------------------------
   The contact region (§8) — two identifiers

   Both are E12-clean: identifiers are not quantities. Both are direction-
   isolated in RTL by a dir="ltr" span on the value, so a phone number never
   reorders. The address line is cut by decision, not by data.
   ------------------------------------------------------------------------- */

.menu-contact {
  display: flex;
  flex-direction: column;
  gap: var(--gap-tight);
  text-align: start;
}

.menu-contact__item {
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-light);
  line-height: var(--lh-body-lg);
  color: var(--color-on-dark-body);
  transition: color var(--dur-fast) var(--ease-out);
}

.menu-contact__item:hover,
.menu-contact__item:focus-visible {
  color: var(--color-on-dark);
}

.menu-contact__item:focus-visible {
  outline: 2px solid var(--color-on-dark);
  outline-offset: 4px;
}

/* -----------------------------------------------------------
   THE DIRECTIONS MARK (Session XXXVII) — one drawing, three
   surfaces, two grounds.

   `currentColor` is the whole mechanism: the mark takes the
   colour of the link it sits inside, so it is ink on Contact's
   paper and --color-on-dark-secondary in the footer, and it
   inherits every hover and focus state for free. That is what
   removes the second asset an inverted mark would need — and
   §1.5a's "never recolour, greyscale, invert or redraw" only
   binds a THIRD-PARTY mark. This one is the site's own, drawn
   in the hairline register About's ISO ring-seals established.

   🔴 THE STROKE IS NOT SCALED. It is a true 1px at every width,
   like /ajman's marks — a hairline that thickens with its box
   stops being a hairline. So the box is fixed at 12x16 and the
   drawing does not participate in the type scale.

   `flex-shrink: 0` because the label may wrap and the drawing
   may not: a squashed pin is a different drawing.
   ----------------------------------------------------------- */
.wayfind__mark {
  flex-shrink: 0;
  inline-size: 12px;
  block-size: 16px;
  overflow: visible;
}

/* The mark sits BESIDE the word, never above or instead of it
   (07-about §13.5.4). The gap is the inline gap, so the pair
   reads as one object rather than as two. */
.menu-contact__item--way,
.site-footer__waylink,
.ct-facts__way,
.pd-specs__way {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-inline);
}


/* -------------------------------------------------------------------------
   Scroll lock (§4.7) — no scroll behind the overlay, and no layout shift

   scrollbar-gutter reserves the gutter permanently, so removing the
   scrollbar removes nothing: content does not shift and neither does the
   fixed chrome, which is positioned against the viewport and would
   otherwise slide by the scrollbar's width every time the menu opened.
   ------------------------------------------------------------------------- */

html.is-menu-open { overflow: hidden; }


/* -------------------------------------------------------------------------
   Breakpoints — the LAYOUT's own (1024/780), not a private 1023/767

   The chrome used to switch one pixel before the container did, so at
   exactly 1024 the text edge was 40 while the chrome held 80. G-2 aligns
   the chrome to the content axis, which only means anything if both move
   on the same widths. (Type still collapses at 767 — that is the type
   system's breakpoint, and the chrome is layout.)
   ------------------------------------------------------------------------- */

@media (max-width: 1024px) {
  .chrome-trigger,
  .chrome-plate {
    --chrome-axis: var(--gutter-tablet);
    --chrome-pad: var(--logo-clear-space);
    --mark-size: var(--logo-size-tablet);
  }

  .chrome-trigger__bars {
    --bar-travel: 6px;

    inline-size: 18px;
    block-size: 13.5px;
  }

  /* 🔴 THE SPARE HEIGHT GOES INTO THE DOORS, NOT INTO ONE VOID — 21 Aug
     2026 (XLI, Mahesh, on his own phone). The identifiers still take the
     floor; what changed is where the slack lands.

     This row read `auto auto` + `align-content: space-between`, which is a
     distribution the rendered 768/375 frames were approved on. Measured on
     a real 393x852 phone it puts the four doors at their 76px minimum
     between 96 and 400 and the contact block at 708 — **a 308px void, 36%
     of the screen**, sitting in the middle of the one surface that is the
     site's only navigation (E13.1).

     📌 THE NUMBER THAT APPROVED IT WAS TRUE AND MEASURED THE WRONG THING.
     The note below this one recorded dead space going 21.1% -> 3.3% at a
     true 375 — dead space at the FOOT, which space-between does drive to
     nothing because the content ends exactly on the screen's edge. Nothing
     measured the gap it opens BETWEEN the two blocks. The register's own
     recorded failure mode, in a stylesheet: a correct number that is not
     the number the question turns on.

     The nav row takes the free space (`minmax(0, 1fr)`) and stretches, so
     `.menu-nav__list`'s `minmax(76px, 1fr)` rows share it — the identical
     mechanism the desktop arrangement has always used, now reaching the
     width where the reader's thumb is. The 76px floor still holds, so a
     short viewport degrades to exactly the layout this replaced rather than
     crushing. */
  .menu-overlay__regions {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) auto;
    gap: var(--gap-heading);
    align-items: stretch;
    padding-block: 104px var(--gutter-tablet);
    padding-inline: var(--gutter-tablet);
  }

  /* The ornament is deleted whole, never shrunk (E3.7 read for ornament):
     28 marks in a phone-width box is texture. The preview goes with it —
     touch has no hover, so the panel would never be reachable, and WORK
     simply navigates. Below 1024 the overlay is nav + contact. */
  .menu-centre { display: none; }
}

@media (max-width: 780px) {
  /* The mark steps to 96 — the desktop FLOOR becomes the mobile rendered
     size, still above §8's 80px mobile minimum. Clear space stays at the
     24px floor: the cap height at 96 is 14.8px, far under it. */
  .chrome-trigger,
  .chrome-plate {
    --chrome-axis: var(--gutter-mobile);
    --mark-size: var(--logo-size-mobile);
  }

  /* 🔴 THE MOBILE GUTTER (20) IS NARROWER THAN THE CLEAR SPACE (24), so
     axis − pad would put the trigger's box at −4px and bleed its black
     field off the inline-start edge of every page — and below 780 the
     trigger is PERMANENT, so it never retreats out of shot. The trigger
     owes §8 nothing, so its field narrows to the gutter instead. The
     PLATE keeps 24: that one IS §8's clear space and is not touched. */
  /* 🔴 AND THE AXES SPLIT, because narrowing the pad on all four sides
     moved the bars UP as well as in: their ink started 4px above the
     wordmark's at 780, 375 and 320, and 0px at 1440 and 1024 — the
     horizontal spine corrected and the vertical one broken, at exactly the
     widths where the two chrome objects sit close enough to compare. The
     BLOCK padding stays the clear space; only the INLINE padding narrows
     to the gutter. Found by Freya on the rendered page, measured after. */
  .chrome-trigger {
    padding-block: var(--logo-clear-space);
    padding-inline: var(--gutter-mobile);
    --chrome-pad: var(--gutter-mobile);
  }

  /* §10: overlay padding 24px. The nav keeps the full 52px scale — the
     widest bold label is 266px against 327px of column at 375px, so no
     label collapse exists at any width and none is needed. */
  .menu-overlay__regions {
    padding-block: 96px var(--gap-block);
    padding-inline: var(--gap-block);
  }
}


/* =========================================================================
   THE FOOTER

   Always black, on every page. On a paper page the closing band owns the
   switch and shares this ground, so band and footer are separated by scale
   — never by colour (E1.2a). On a page with no band the footer owns the
   switch itself (E1.2c).

   The hairline below is the separator only where the band above is PAPER.
   Where the band is black the two are one object; see THE MERGED CLOSE at
   the end of this section.
   ========================================================================= */

.site-footer {
  background: var(--color-surface-dark);
  border-block-start: 1px solid var(--color-hairline-dark);
  color: var(--color-on-dark-body);
}

.site-footer__inner {
  display: flex;
  flex-direction: column;
  gap: var(--gap-block);
  max-inline-size: var(--container-max);
  margin-inline: auto;
  padding-block: var(--gap-component);
  padding-inline: var(--gutter-desktop);
}

/* The one line. Its second sentence is X6-elastic: ProArc's answer on the
   discipline list rewrites it and the Services blocks together. */
.site-footer__line {
  max-inline-size: var(--measure-body);
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-light);
  line-height: var(--lh-body-lg);
  letter-spacing: var(--ls-body-lg);
  color: var(--color-on-dark);
}

.site-footer__list,
.site-footer__social {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-block);
}

.site-footer__link {
  font-size: var(--fs-metadata);
  font-weight: var(--fw-regular);
  line-height: var(--lh-metadata);
  color: var(--color-on-dark-secondary);
  transition: color var(--dur-fast) var(--ease-out);
}

.site-footer__link:hover,
.site-footer__link:focus-visible {
  color: var(--color-on-dark);
  text-decoration: underline;
  text-underline-offset: 4px;
}

.site-footer__link:focus-visible {
  outline: 2px solid var(--color-on-dark);
  outline-offset: 4px;
}

/* Its own row, above the legal line. It is not a social account and does
   not join that list; see the footer partial for why the naming matters. */
.site-footer__way {
  margin: 0;
}

/* The closing row — the legal line and the signature, one on each axis
   (Mahesh, 21 Aug). `flex-end` rather than `center`: the mark's BASELINE and
   the legal line's should read as one horizontal, and the mark's artwork ends
   on its own baseline, so aligning the boxes' bottoms aligns the ink. `wrap`
   because below 780 a 132px mark and a caption do not share a line on a
   335px column — see the media block. */
.site-footer__close {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--gap-block);
}

/* §8's clear space is not spent by the row's gap alone: --gap-block is 24,
   which IS the minimum, so nothing may be subtracted from it here. The
   footer's own padding-block (--gap-component, 64) covers the bottom. */
.site-footer__marklink {
  display: block;
  /* The mark is the whole target — no padding, so the link's box is the
     artwork's box and the row's alignment stays the ink's. §4.7's 44px
     minimum is met by the mark itself at every breakpoint (96px is the
     smallest it renders). */
  line-height: 0;
}

.site-footer__marklink:focus-visible {
  outline: 2px solid var(--color-on-dark);
  outline-offset: 4px;
}

.site-footer__mark {
  display: block;
  inline-size: var(--logo-size-footer);
  block-size: auto;
}

.site-footer__legal {
  font-size: var(--fs-caption);
  font-weight: var(--fw-regular);
  line-height: var(--lh-caption);
  letter-spacing: var(--ls-caption);
  color: var(--color-on-dark-secondary);
}

/* -----------------------------------------------------------
   THE MERGED CLOSE (Mahesh, 6 Aug) — where the closing band is
   BLACK, the band and the footer are one object.

   The footer's top hairline exists to separate it from the band
   above. Where that band is paper the ground switch already does
   the separating and the hairline is invisible against it; where
   the band is black the hairline is the ONLY thing there, and it
   divides two identical grounds. So on every page it is actually
   visible it is a seam, never structure — which is what the tail
   of Home read as: two black slabs with a rule between them.

   Merged, the object is: the summons, a rule at the TEXT column,
   then the particulars. Inset is the whole point — a full-bleed
   rule says "two bands", a rule at the column says "one object,
   internally divided". The band gives up its closing air so the
   two padded boxes stop stacking (205px of dead black at 1440).

   Derived from the ground itself rather than declared per page,
   so a band that later turns paper turns its own merge off.

   🔴 `~`, NEVER `+`. /projects carries a <script id=wk-search-
   index> between </main> and <footer>: the adjacent combinator
   matches on six pages and silently misses the seventh — and it
   misses it AFTER the band has already given up its padding, so
   the action lands hard against the full-bleed hairline. That is
   a WORSE ending than the one being fixed, on the one page the
   selector was never checked against.
   ----------------------------------------------------------- */

main:has(> .surface-dark:last-child) > .surface-dark:last-child {
  padding-block-end: 0;
}

main:has(> .surface-dark:last-child) ~ .site-footer {
  border-block-start: 0;
}

main:has(> .surface-dark:last-child) ~ .site-footer .site-footer__inner::before {
  content: '';
  display: block;
  block-size: 1px;
  /* The flex gap already pays --gap-block below the rule. */
  margin-block-end: calc(var(--gap-component) - var(--gap-block));
  background: var(--color-hairline-dark);
}

/* The footer's inner box is a .container in all but name (same max-width,
   same gutters), so it moves on the layout's breakpoints with the chrome. */
@media (max-width: 1024px) {
  .site-footer__inner { padding-inline: var(--gutter-tablet); }
  .site-footer__mark { inline-size: var(--logo-size-footer-tablet); }
}

@media (max-width: 780px) {
  .site-footer__inner { padding-inline: var(--gutter-mobile); }

  /* 🔴 FOUR AND THREE, NOT SIX AND ONE — 21 Aug (XLI, Mahesh, on his phone).
     Seven labels total 309px against 353px of column, so a single row is
     impossible at any permitted spacing (309 + six --gap-tight is 405). The
     flex rag therefore fitted six and orphaned Careers on a line of its own.

     📌 THE WRAP IS STRUCTURAL AND THE RAG WAS THE ONLY THING IN QUESTION.
     Stated as a four-column grid the break is DECIDED rather than left to
     whatever the labels happen to measure — which matters here because the
     labels are route names: renaming one, or adding an eighth door, moves a
     rag silently and cannot move this. Columns are max-content, so the row
     keeps the natural spacing of a list rather than becoming a table. */
  .site-footer__list {
    display: grid;
    grid-template-columns: repeat(4, max-content);
    justify-content: start;
    gap: var(--gap-tight);
  }

  /* The row stacks rather than shrinking further: at 375 the column is 335px
     and a 132px mark beside a caption leaves neither enough room to sit on
     its own axis. The mark takes the inline-start with the rest of the
     footer, because a lone right-aligned object in a stacked column reads as
     a mistake rather than as a signature. */
  .site-footer__mark { inline-size: var(--logo-size-footer-mobile); }

  .site-footer__close {
    flex-direction: column-reverse;
    align-items: flex-start;
  }
}


/* -------------------------------------------------------------------------
   Reduced motion (§9.6) — the finished overlay, immediately

   tokens.css already collapses every duration globally, but a duration of
   zero does not cancel a DELAY: the dot stagger alone would leave the
   constellation blank for 1.4 seconds. Delays are zeroed here, and the
   nib — which is pure motion and has nothing to show when still — does not
   run at all.
   ------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .menu-overlay,
  .menu-overlay.is-open,
  .menu-overlay.is-open.is-closing {
    clip-path: none;
    transition: none;
  }

  /* transform:none is the finished state for both — an unscaled rule is a
     full-width rule, and an unscaled mark is a whole mark. */
  .menu-overlay.is-open .menu-dot,
  .menu-overlay.is-open .menu-nav__row::after,
  .menu-nav__row::after {
    transform: none;
    transition: none;
    transition-delay: 0s;
  }

  /* The four stagger rules carry a :nth-child, so they out-specify the reset
     above. The transition is already off, which makes the delay inert — but
     a reader should be able to trust the computed value instead of having to
     work out the cascade, so the reset matches their specificity. */
  .menu-overlay.is-open .menu-nav__row:nth-child(n)::after {
    transition-delay: 0s;
  }

  .menu-nib,
  .menu-nib.is-running {
    display: none;
    animation: none;
  }
}


/* =========================================================================
   THE FORM GRAMMAR
   Spec: _bmad/wds/D-UX-Design/08-contact.md §6 · v1.3 E6 / E6.1

   Shared, not page CSS, because the site has two forms: Contact's five
   fields and /careers' application form. One grammar, two layouts — the
   layouts live in pages/contact.css and pages/careers.css.

   THE GRAMMAR, and it is all §6 fixes: visible 12px uppercase labels ABOVE
   the field · border-bottom fields, no boxes · placeholder-as-label barred
   · the submit is an ink block on paper and inverts on dark.

   THE BORDER IS #595959 ON BOTH GROUNDS — v1.3 E6.1, added from this
   screen. E6's table gave the DARK border #595959 (3.00:1, exactly WCAG
   1.4.11's floor) and left the light one at --color-hairline #D6D6D6,
   which is 1.45:1 on white — below the floor on every form this site has
   ever shipped. A field border is a component boundary and therefore
   structural under E3.2 on either ground, so light takes --color-ink-
   secondary too (7.00:1). --color-hairline stays correct for decorative
   rules; it was never correct for an input boundary.

   WHAT IS DELIBERATELY NOT HERE: focus treatment beyond a visible focus,
   filled states, the validation matrix, error and success strings, and
   whether Subject ever becomes a select. All of it is the forms phase's,
   and §6's second column is its brief. The one thing that could not wait
   is the focus indicator — a field a keyboard reader cannot locate is not
   a boundary question, and tokens.css already ships .field-error and
   .field-invalid for the phase to switch on.
   ------------------------------------------------------------------------- */

.form-field + .form-field {
  margin-block-start: var(--gap-block);
}

/* The label is a block above its field, not beside it: 12px uppercase is
   the site's small-label register (the client wall's group heads, the
   record tables' labels) and --color-ink-secondary is 7.00:1. */
.form-field__label {
  display: block;
  margin-block-end: var(--gap-inline);
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-nav);
  line-height: var(--lh-caption);
  text-transform: uppercase;
  color: var(--color-ink-secondary);
}

/* Phone is the only optional field (§6) and it says so in lowercase beside
   its own label, inside it, so a screen reader reads "Phone optional" as
   the label. The required four carry no asterisk — an asterisk is a legend
   the page would then have to print.

   It steps down by CASE AND WEIGHT, not by colour: #595959 is already the
   secondary, and going lighter to mark a lesser thing would build
   hierarchy out of contrast the text cannot afford (v1.2 §9.16). */
.form-field__optional {
  font-weight: var(--fw-regular);
  letter-spacing: 0;
  text-transform: lowercase;
}

/* No box. The rule under the field is the field.

   appearance and border-radius are STATED, not inherited: iOS Safari
   rounds and insets every input by default, and "no border-radius" is a
   system rule (v1.2 §9) that a UA stylesheet does not know about.

   background: transparent is what makes the browser's autofill paint a
   whisper on paper rather than a plate — §2's second reason for this
   page's ground. inline-size: 100% because a field inherits no width from
   its label, and box-sizing is already border-box in base.css. */
.form-field__input {
  display: block;
  inline-size: 100%;
  padding-block: var(--gap-inline);
  padding-inline: 0;
  appearance: none;
  border: 0;
  border-block-end: 1px solid var(--color-ink-secondary);
  border-radius: 0;
  background: transparent;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--color-ink);
  transition: border-block-end-color var(--dur-fast) var(--ease-out);
}

/* A textarea is a field that grew, not a different control. resize is
   vertical only: horizontal resize would break the measure the column was
   set to. */
.form-field__area {
  resize: vertical;
  min-block-size: 8em;
}

/* THE CV UPLOAD (/careers). A file input is the one control a browser
   will not let a site draw: the "no file selected" text beside the button
   is the user agent's, in its own words and its own language, and there is
   no way to replace it that does not mean hiding the real input and
   forwarding clicks to a fake one — which breaks the keyboard, the
   accessible name and the form's own reset.

   So the field takes the same rule under it as every other field, and the
   BUTTON inside it is styled through ::file-selector-button into the
   site's small action shape. What the browser writes beside it is left
   alone, at --color-ink-secondary so it reads as the metadata it is. */
.form-field__file {
  display: block;
  inline-size: 100%;
  padding-block: var(--gap-inline);
  padding-inline: 0;
  border: 0;
  border-block-end: 1px solid var(--color-ink-secondary);
  border-radius: 0;
  background: transparent;
  font-size: var(--fs-metadata);
  line-height: var(--lh-metadata);
  color: var(--color-ink-secondary);
}

.form-field__file::file-selector-button {
  margin-inline-end: var(--gap-tight);
  padding-block: var(--space-2xs);
  padding-inline: var(--gap-block);
  border: 1px solid var(--color-ink);
  border-radius: 0;
  background: transparent;
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-nav);
  text-transform: uppercase;
  color: var(--color-ink);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}

.form-field__file::file-selector-button:hover {
  background: var(--color-ink);
  color: var(--color-on-dark);
}

/* The site's focus grammar, unchanged from its links: a 2px ink outline at
   4px offset. The border darkens with it so the field reads as active in
   the same gesture, but the OUTLINE is what carries the indicator — a
   1px-to-2px rule change is not a focus indicator a reader can rely on. */
.form-field__input:focus-visible,
.form-field__file:focus-visible {
  outline: 2px solid var(--color-ink);
  outline-offset: 4px;
  border-block-end-color: var(--color-ink);
}

/* The page's one imperative (voice §3.4). An ink block on paper; the dark
   rule below is the inversion §6 asks for, and it is written now so a form
   that later lands on a dark surface does not arrive with an invisible
   button. */
.form-submit {
  display: inline-block;
  margin-block-start: var(--gap-heading);
  padding-block: var(--gap-tight);
  padding-inline: var(--gap-heading);
  border: 1px solid var(--color-ink);
  background: var(--color-ink);
  font-size: var(--fs-metadata);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-nav);
  text-transform: uppercase;
  color: var(--color-on-dark);
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}

.form-submit:hover,
.form-submit:focus-visible {
  background: transparent;
  color: var(--color-ink);
}

.form-submit:focus-visible {
  outline: 2px solid var(--color-ink);
  outline-offset: 4px;
}

.surface-dark .form-field__label {
  color: var(--color-on-dark-secondary);
}

.surface-dark .form-field__input,
.surface-dark .form-field__file {
  border-block-end-color: var(--color-ink-secondary);
  color: var(--color-on-dark-secondary);
}

.surface-dark .form-field__input {
  color: var(--color-on-dark-body);
}

.surface-dark .form-field__input:focus-visible,
.surface-dark .form-field__file:focus-visible {
  outline-color: var(--color-on-dark);
  border-block-end-color: var(--color-on-dark);
}

.surface-dark .form-field__file::file-selector-button {
  border-color: var(--color-on-dark);
  color: var(--color-on-dark);
}

.surface-dark .form-field__file::file-selector-button:hover {
  background: var(--color-on-dark);
  color: var(--color-surface-dark);
}

.surface-dark .form-submit {
  border-color: var(--color-on-dark);
  background: var(--color-on-dark);
  color: var(--color-surface-dark);
}

.surface-dark .form-submit:hover,
.surface-dark .form-submit:focus-visible {
  background: transparent;
  color: var(--color-on-dark);
}

.surface-dark .form-submit:focus-visible {
  outline-color: var(--color-on-dark);
}

/* -------------------------------------------------------------------------
   THE FORMS PHASE — the states. Spec: _bmad/wds/D-UX-Design/10-forms.md.

   Everything below is switched on by js/forms.js and by nothing else. A
   page with no script keeps the finished form above and the browser's own
   native validation, which is why none of these rules changes a resting
   field: an unflagged form renders byte-for-byte as it did before this
   block existed.

   THE ERROR STATE IS THREE SIGNALS AND NEVER FEWER (v1.2 §1.5, v1.3 E6.1):
   the 2px inline-start rule (tokens.css .field-invalid, drawn in the margin
   so the field does not move), an explicit text string, and colour. Colour
   is the only one of the three that could be missed, and it is never alone.
   ------------------------------------------------------------------------- */

/* The field's own rule reddens with the state. This is the third signal,
   not the first: it repeats what the margin rule and the string already
   say, at the one place the reader's eye is. */
.field-invalid .form-field__input,
.field-invalid .form-field__file {
  border-block-end-color: var(--color-signal);
}

.surface-dark .field-invalid .form-field__input,
.surface-dark .field-invalid .form-field__file {
  border-block-end-color: var(--color-signal-on-dark);
}

/* The string sits under the field it is about, at metadata scale — the
   register the site uses for a line that describes another line. */
.field-error {
  margin-block-start: var(--gap-inline);
}

/* THE HONEYPOT (§4). Not type="hidden", because a hidden input is exactly
   the one a bot does not fill. It is removed from the page without being
   removed from the document: no size, no ink, nothing to tab to, and
   aria-hidden in the markup keeps it out of the accessibility tree as
   well. A human cannot reach it by pointer, by keyboard or by screen
   reader; a script that fills every input it finds can. */
.form-honeypot {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* THE OUTCOME PANELS. They ship [hidden] in the markup and the script
   reveals one — the Work surfaces' idiom, so a reader without the script
   never meets a dead panel. No display is declared on the class, because a
   display value here would beat the [hidden] attribute and the panel would
   ship visible on every page load. That is not a hypothetical: it is the
   ordinary way this pattern breaks. */
/* It sits under the button, close enough to be that button's answer. On
   success the form is hidden and the panel takes its place in the layout,
   which is where the reader was already looking. */
.form-outcome {
  margin-block-start: var(--gap-block);
  max-inline-size: var(--measure-body);
  padding-inline-start: var(--gap-block);
  border-inline-start: 2px solid var(--color-ink);
}

/* NO FOCUS RING ON THE PANEL, and this was decided by looking at the
   rendered state rather than at the rule. The panel carries tabindex="-1",
   so it can NEVER be reached by Tab — the only focus it can ever receive
   is the script's own, at the moment it appears. A ring there indicates
   nothing a reader needs to know, and it drew a full 2px box around a
   paragraph on a site whose grammar has no boxes and no border-radius.
   The rule at its inline start is what marks it, in the site's own
   language. Every operable control keeps its indicator; this is not one. */
.form-outcome:focus,
.form-outcome:focus-visible {
  outline: none;
}

.form-outcome__head {
  margin: 0;
  font-size: var(--fs-h3);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-h3);
  color: var(--color-ink);
}

.form-outcome__body {
  margin-block-start: var(--gap-tight);
  margin-block-end: 0;
  color: var(--color-ink-secondary);
}

/* The failure panel's one link is the working channel, and it carries the
   inline register the site decided for a link inside a sentence: weight
   and the rule, never case and never colour. */
.form-outcome__link {
  border-block-end: 1px solid currentcolor;
  font-weight: var(--fw-semibold);
  color: inherit;
  text-decoration: none;
}

.form-outcome__link:hover,
.form-outcome__link:focus-visible {
  color: var(--color-ink);
}

.surface-dark .form-outcome {
  border-inline-start-color: var(--color-on-dark);
}

.surface-dark .form-outcome__head {
  color: var(--color-on-dark);
}

.surface-dark .form-outcome__body {
  color: var(--color-on-dark-secondary);
}

.surface-dark .form-outcome__link:hover,
.surface-dark .form-outcome__link:focus-visible {
  color: var(--color-on-dark);
}

/* In flight: the button cannot be pressed twice. It keeps its own label —
   a label that changes is a second string to translate, and the disabled
   state already says the thing. */
.form-submit[disabled] {
  cursor: default;
  background: var(--color-ink-secondary);
  border-color: var(--color-ink-secondary);
}

.surface-dark .form-submit[disabled] {
  background: var(--color-ink-secondary);
  border-color: var(--color-ink-secondary);
  color: var(--color-on-dark);
}
