/* GENERATED by tools/build/styles.mjs — cascade order comes from src/styles/order.json */
@layer reset, tokens, base, layout, components, sections, utilities, overrides;

/* ---- reset · base/reset.css ---- */
@layer reset {
/* Minimal, opinion-free normalisation. Project decisions belong in later layers. */
*,
*::before,
*::after {
  box-sizing: border-box;
}

body,
h1,
h2,
h3,
h4,
p,
figure,
blockquote,
dl,
dd {
  margin: 0;
}

img,
picture,
video,
canvas,
svg {
  display: block;
  max-width: 100%;
}

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
}

/* The reduced-motion kill switch is architectural (ADR-009), not a late patch.
   It lives in the reset layer so nothing can accidentally out-specify it. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important; /* documented exception: user preference outranks design */
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
}

/* ---- tokens · tokens/root-font.css ---- */
@layer tokens {
/* ============================================================================
   SYSTEM 1 OF 2 — THE ROOT LADDER. `1rem` is a LAYOUT unit on this site.

   `html { font-size: var(--fluid-font) }` makes the root font-size a function of
   viewport WIDTH, and `rem` then owns essentially every box: roughly 1,493 rem
   literals across the Golden Master's sheets are margins, gaps, heights and
   offsets, not type. Changing how 1rem is computed does not "adjust the
   typography" — it moves the whole page.

   This file is therefore reproduced VERBATIM from the Golden Master's four-band
   ladder, discontinuities included. It is asserted, not described:
   `tests/responsive/root-font-ladder.reference.json` holds the measured value at
   39 widths and `tests/unit/typography-ladder.test.mjs` recomputes this formula
   against every one of them.

   THE LADDER HAS TWO DISCONTINUITIES AND BOTH ARE DELIBERATE HERE:

       479 -> 480 px    13.31 -> 11.00     a -2.31px CLIFF across one pixel
       991 -> 992 px    11.47 -> 11.00     NON-MONOTONIC: wider page, smaller rem

   Do not smooth them. A rebuild that replaces this with one tidy clamp() has
   moved every rem-based constant on the site by up to ~45% and changed the
   design. Simplification is permitted only with a parity run proving equal
   computed values at every width in the matrix (typography-contract.md §3).

   THE FLOOR IS THE FIX (legacy owner, 2026-08-14, preserved here). Without
   `max(11px, …)` the 480px band computes 5.56px — a 2.4x collapse across one
   pixel of width, and it shipped. The floor lifts only the already-collapsed
   bands and leaves every width the site was composed at untouched.
   ========================================================================== */

:root {
  /* Where the container measure starts, stops, and what it is divided by.
     These four are the ladder: each breakpoint below re-declares them, and
     --fluid-font is one formula over whichever set is in effect. */
  --min-width: 992px; /* width at which scaling starts */
  --max-width: 1920px; /* width at which scaling stops */
  --design-width: 1728; /* the design's reference width, unitless */
  --design-unit: 16; /* body font-size in the design, unitless */
  --scale-factor: 1; /* global scale adjustment; 1 = 100% */

  /* NOT floored, deliberately: every max-width and container measure on the
     site keeps its value and only the type unit moves. */
  --fluid-container: clamp(var(--min-width), 100vw, var(--max-width));

  --fluid-font: max(
    11px,
    calc(var(--fluid-container) / var(--design-width) * var(--design-unit) * var(--scale-factor))
  );
}

/* Tablet — 768…991. The band that produces the 991 -> 992 non-monotonicity:
   at 991 the container clamps to 991 and the unit is 20, giving 11.47px; at 992
   the desktop band takes over with unit 16 and a 992px floor, giving 11.00px. */
@media screen and (max-width: 991px) {
  :root {
    --min-width: 768px;
    --max-width: 991px;
    --design-width: 1728;
    --design-unit: 20;
  }
}

/* Mobile landscape — 480…767. Inherits --design-unit: 20 from the block above;
   that inheritance is what produced the raw 5.56px at 480px before the floor. */
@media screen and (max-width: 767px) {
  :root {
    --min-width: 480px;
    --max-width: 767px;
  }
}

/* Mobile portrait — up to 479. --design-unit jumps to 48, which is the other
   half of the 479 -> 480 cliff: 479/1728*48 = 13.31px. */
@media screen and (max-width: 479px) {
  :root {
    --min-width: 320px;
    --max-width: 479px;
    --design-unit: 48;
  }
}
}

/* ---- tokens · tokens/color.css ---- */
@layer tokens {
/* ============================================================================
   THE PALETTE. Every value below is read from the Golden Master's own `:root`,
   not sampled from a screenshot, so it is exact rather than close.

   Naming carries the legacy vocabulary verbatim (`--color--lime`, two hyphens)
   because those names appear in the Golden Master's markup, its CSS and this
   project's evidence extracts. Renaming them would break every cross-reference
   in `docs/` for no gain; they are readable, greppable and stable, which is what
   ADR-006 asks of a class or token name.

   THE CHAPTER VARIABLES at the bottom are the seam the scroll-driven colour bus
   will write to in a later stage. They are declared here with their SETTLED
   values so that the static page — and the reduced-motion page, and the no-JS
   page — is correct without anything running. Nothing writes them yet.
   ========================================================================== */

:root {
  /* ── core ink and paper ───────────────────────────────────────────────── */
  --color--dark-green: #040504;
  --color--black: #040504;
  --color--white: #edf5f3;

  /* ── the accent ───────────────────────────────────────────────────────── */
  --color--lime: #21f1a8;
  --color--lime-off: #21f1a8;
  --color--lime-ink: #0c7a55; /* lime that stays legible on white */
  --color--lime-ink-lg: #149a6b; /* the same, for large text */
  --color--lime-zero: #21f1a800; /* the accent at zero alpha, for gradients */

  /* ── THE CREDENTIAL'S GOLD ─────────────────────────────────────────────
     ONE section consumes these — the sixth card of S07, the credential the
     five course cards add up to. They are declared here rather than locally
     because a colour that appears in the design system's vocabulary is a
     token, and because the three-way split below is a CONTRACT about where
     each value may be used, which a section-local `--scx-gold` would not
     state.

     The split mirrors `--color--lime` / `-ink` / `-ink-lg` exactly, and for
     the same reason: the saturated value reads beautifully on the near-black
     plate and fails as text on paper. Contrast is COMPUTED against the two
     grounds this site actually has, not sampled from a screenshot:

       --color--gold        on #040504 (the plate)  10.13:1   any size
       --color--gold-ink    on #edf5f3 (the paper)   4.88:1   AA, normal text
       --color--gold-ink-lg on #edf5f3 (the paper)   3.68:1   AA, large text
                                                              (>=24px, or
                                                              >=18.66px bold)

     `--color--gold` is therefore NEVER written as text on paper. If
     `npm run golden:site-a11y` ever reports a violation here, darken the
     value; the published measurement is zero violations across 144 scans
     (contracts/published-measurements.json) and a threshold is not relaxed to
     make a check pass. */
  --color--gold: #d8b25a; /* the rim and the seal, on ink */
  --color--gold-ink: #8a6320; /* gold that stays legible as text on paper */
  --color--gold-ink-lg: #a2761f; /* the same, for large text */
  --color--gold-deep: #4a3410; /* the warm ground the seal sits on */
  --color--gold-zero: #d8b25a00; /* the gold at zero alpha, for gradients */

  /* ── the greens and greys, warm to cool ──────────────────────────────── */
  --color--dark-green-tint-1: #3b3c38;
  --color--dark-green-tint-2: #535450;
  --color--surface-deep: #232520;
  --color--green-off-white-1: #dde1d2;
  --color--green-off-white-2: #b4b8a5;
  --color--green-light: #ebeee0;
  --color--grey-1: #ebeee0;
  --color--grey-2: #c8cbbd;
  --color--grey-on-track: #b9bbad;

  /* ── families ─────────────────────────────────────────────────────────── */
  --font-display: 'Quicksand Display', Quicksand, Arial, sans-serif;
  --font-body: Quicksand, Arial, sans-serif;
  --font-label: 'Quicksand Label', Arial, sans-serif;

  /* ── container measures ───────────────────────────────────────────────── */
  /* rem-based ON PURPOSE: these are LAYOUT, and layout is measured in the root
     ladder's unit (tokens/root-font.css). Type is not. */
  --padding--container: 1.25rem;
  --gap: 1.25rem;
  --padding--small: 2rem;
  --padding--med: 3rem;
  --padding--xlarge: 5rem;
  --radius--small: 1rem;
  --on-t-nav-spacer: 5.375rem;
  --grid-spacer: 31.51vh;

  /* ── THE HEADER'S REAL HEIGHT, AND WHY IT IS NOT `--on-t-nav-spacer` ──────
     `--on-t-nav-spacer` is the Golden Master's own spacer value and is KEPT
     verbatim, because eleven measured sections are spaced against it. It is not
     the header's height: the bar is `.nav-inner`, whose box is one control tall
     plus a gutter on each side, and at every width that is TALLER than the
     spacer — 79.2px against 70.1px at 390, 68.8px against 59.1px at 768. A
     section that clears the bar by adding air to the spacer is clearing an
     invented number and happens to land far enough down; one that uses the
     spacer alone lands UNDER the bar. `/contact` did exactly that (P11-02).

     So the header states its own height, once, from the same two quantities its
     own rules use — `.btn-w.is-nav`'s height and `.nav-inner`'s `var(--gap)`
     padding — and `--header-clear` adds the notch inset that
     `viewport-fit=cover` puts under a fixed bar on a notched device. Anything
     that must start BELOW the header measures against these, not the spacer. */
  --header-h: calc(3.75rem + var(--gap) * 2);
  --header-clear: calc(var(--header-h) + env(safe-area-inset-top, 0px));

  /* ── THE VIEWPORT-HEIGHT UNIT, AND WHAT IT IS NOW ────────────────────────
     Kept as a variable because iOS Safari's `vh` is the LARGE viewport — the
     height the page only has once the address bar has retracted — so every box
     sized in `vh` that must FIT the visible area is too tall for as long as the
     bar is out. This token is the seam the file header always said a later
     stage would write a measured value into. That stage is here, and the value
     is not measured: it is `svh`, the unit the platform added for exactly this
     question, supported since Safari 15.4 and Chrome 108.

     `svh`, NOT `dvh`, AND THE DIFFERENCE IS THE WHOLE POINT. `dvh` tracks the
     bar continuously, so anything sized in it RESIZES while the visitor scrolls
     — which for a pinned section is a section whose height changes underneath
     the scrub driving it. `svh` is the SMALL viewport: a constant, equal to the
     height with the bar extended, so a box sized in it always fits and never
     moves. A full-bleed overlay that is opened with scroll already locked is the
     one case that genuinely wants `dvh`, and `components/site-nav.css` says so
     for itself rather than bending this token.

     THE FALLBACK IS THE OLD BEHAVIOUR, not a broken box: a browser that does not
     know `svh` keeps `1vh` and is exactly where it was. */
  --vh: 1vh;

  /* ── THE COLOUR BUS, at its settled values ───────────────────────────────
     `--page-bg` is what the body paints. On the Golden Master a scroll-driven
     chapter system rewrites `document.body.style.backgroundColor` across the
     ring and the deck (paper -> ink -> paper). That system is stage 8+; until it
     exists the page must be correct standing still, so these hold the value the
     document rests at. A subsystem that later drives them must WRITE them, never
     rely on their absence. */
  --page-bg: var(--color--white);
  --page-ink: var(--color--dark-green);
  --nav-ink: var(--color--white);
  --nav-ink-soft: var(--color--grey-2);
  --vx-ink: var(--color--dark-green);
  /* #535450 — the value the chapter rests at, read from the scrub's own
     SOFT_DARK constant rather than guessed from the palette. */
  --vx-ink-soft: var(--color--dark-green-tint-2);
}

@supports (height: 1svh) {
  :root {
    --vh: 1svh;
  }
}

/* ── PHONE TOKENS, below 480 ──────────────────────────────────────────────
   Two of the container measures above change on phones, and BOTH were missing
   until `golden:compare` failed all 22 `mobile-390` checkpoints on a −48px
   document height. Measured on the Golden Master, `.c` resolves its gutter to
   exactly `1rem` at every width below 480 (11px at 390 and at 320, 11.94px at
   430, 13.31px at 479 — the root ladder's value each time) against `1.25rem`
   from 480 up. That 0.25rem is 5.5px per side at 390, so every `.c` child was
   5.5px narrower here, and the two systems whose height follows their width —
   the helmet tiles (`aspect-ratio: 406.89 / 411`, eight rows) and the four
   discipline cards — lost 22.25px and 16.5px of document between them.

   Worth recording WHY this was easy to miss rather than just that it was: the
   Golden Master's own source carries a comment saying this whole phone block
   had been pasted inside `.btn-text.is-footer` instead of `body`, so for as
   long as that bug lived, every phone got the ≤991 TABLET values and only one
   footer button got these. The shipped site has it fixed; a reading of the
   cascade that trusted the older shape would reproduce the bug instead. */
@media screen and (max-width: 479px) {
  :root {
    --padding--container: 1rem;
    --on-t-nav-spacer: 6.375rem;

    /* `.btn-w.is-nav` is 4.7rem here — `components/button.css`, same media
       query — so the bar is a rem taller than it is above 480. */
    --header-h: calc(4.7rem + var(--gap) * 2);
  }
}
}

/* ---- tokens · tokens/type.css ---- */
@layer tokens {
/* ============================================================================
   SYSTEM 2 OF 2 — THE TEXT RAMP. Px-anchored, and INDEPENDENT of `1rem`.

   THESE TWO SYSTEMS MUST NOT BE MERGED. `tokens/root-font.css` is a LAYOUT unit
   derived from viewport width through a discontinuous four-band ladder. This
   file is the TYPE scale, anchored in pixels, and every one of its 11 tokens is
   strictly MONOTONIC in viewport width — measured, at 39 widths, zero
   non-monotonic transitions (typography-contract.md §7).

   The separation was the Golden Master author's deliberate repair of a shipped
   bug: sizing type from the layout unit produced 24 distinct type sizes on the
   homepage at 390px wide, the smallest of them 5.5px. Merging the systems
   re-creates that. So the ramp is reproduced verbatim, including all three
   bands, and `--fluid-font` is not referenced anywhere in this file.

   Three bands, because one clamp cannot have a knee:
     ≤767    the phone ramp — a role-graded cut, 5% at the floor to 18% at the top
     768…1439 the design ramp — 360 -> 1440 carries the design
     ≥1440   the large-desktop ramp — type follows the growing layout gently
             rather than freezing, and 1440 itself is identical either way.
   ========================================================================== */

:root {
  /* THE TWO OPTICAL KNOBS. Every token below is a ramp TIMES one of these, so a
     locale can be re-tuned in one place instead of eleven. Neither may carry a
     layout decision: the ramps own the sizes, these only correct for what a
     different FACE does at the same em. Both are 1 for the Latin locales, which
     is the identity. */
  --fs-lang: 1; /* body copy and everything above it */
  --fs-lang-sm: 1; /* labels, captions, eyebrows — the floor of the scale */

  /* ── type ────────────────────────────────────────────────────────────────
                             360px             1440px    role                */
  --fs-micro: calc(
    var(--fs-lang-sm) * clamp(11px, calc(10.5px + 0.1389vw), 12.5px)
  ); /* eyebrow, chip, counter */
  --fs-cap: calc(
    var(--fs-lang-sm) * clamp(12.5px, calc(12px + 0.1389vw), 14px)
  ); /* caption, meta, label */
  --fs-sm: calc(
    var(--fs-lang-sm) * clamp(13.5px, calc(13px + 0.1389vw), 15px)
  ); /* small body, notes */
  --fs-body: calc(var(--fs-lang) * clamp(15px, calc(14.33px + 0.1852vw), 17px)); /* BODY COPY */
  --fs-lead: calc(var(--fs-lang) * clamp(17px, calc(16px + 0.2778vw), 20px)); /* lede, large body */
  --fs-h4: calc(
    var(--fs-lang) * clamp(18px, calc(16.67px + 0.3704vw), 22px)
  ); /* card title, step title */
  --fs-h3: calc(var(--fs-lang) * clamp(20px, calc(18px + 0.5556vw), 26px)); /* sub-heading */
  --fs-h2: calc(var(--fs-lang) * clamp(24px, calc(20px + 1.1111vw), 36px)); /* section heading */
  --fs-h1: calc(var(--fs-lang) * clamp(30px, calc(21.33px + 2.4074vw), 56px)); /* page title */
  --fs-display: calc(
    var(--fs-lang) * clamp(34px, calc(22px + 3.3333vw), 70px)
  ); /* hero / masthead */
  --fs-impact: calc(
    var(--fs-lang) * clamp(40px, calc(20px + 5.5556vw), 100px)
  ); /* the impact words */

  /* ── line height ─────────────────────────────────────────────────────────
     The display faces are set uppercase and tight by design; body opens up
     because the Arabic and German paragraphs are the longest on the site. */
  --lh-impact: 0.88; /* the uppercase display words */
  --lh-display: 0.95; /* hero and page titles */
  --lh-flat: 1; /* chips and pills, where the box IS the line */
  --lh-tight: 1.06; /* section headings */
  --lh-snug: 1.2; /* sub-headings, card titles */
  --lh-label: 1.3; /* micro labels, meta rows */
  --lh-body: 1.55; /* everything you actually read */

  /* ── tracking ────────────────────────────────────────────────────────────
     In em so it tracks the size it is applied to. Arabic zeroes all four:
     it is a joined script and letter-spacing breaks the joins. */
  --track-display: -0.02em;
  --track-tight: -0.015em;
  --track-body: -0.005em;
  --track-label: 0.1em;

  /* ── space ───────────────────────────────────────────────────────────────
     Same construction, same reason. 1–4 are inside-a-component gaps, 5–8 are
     between components, 9–12 are between blocks. Px-anchored so a resize across
     the 479/480 boundary does not move them. */
  --sp-1: 4px;
  --sp-2: clamp(6px, calc(5.33px + 0.1852vw), 8px);
  --sp-3: clamp(8px, calc(6.67px + 0.3704vw), 12px);
  --sp-4: clamp(12px, calc(10.67px + 0.3704vw), 16px);
  --sp-5: clamp(16px, calc(14.67px + 0.3704vw), 20px);
  --sp-6: clamp(20px, calc(17.33px + 0.7407vw), 28px);
  --sp-7: clamp(26px, calc(22.67px + 0.9259vw), 36px);
  --sp-8: clamp(32px, calc(26.67px + 1.4815vw), 48px);
  --sp-9: clamp(44px, calc(37.33px + 1.8519vw), 64px);
  --sp-10: clamp(56px, calc(45.33px + 2.963vw), 88px);
  --sp-11: clamp(72px, calc(58.67px + 3.7037vw), 112px);
  --sp-12: clamp(88px, calc(69.33px + 5.1852vw), 144px);

  /* ONE section rhythm for the whole site. */
  --section-y: clamp(52px, calc(38.67px + 3.7037vw), 92px);
}

/* ── THE PHONE RAMP ────────────────────────────────────────────────────────
   A role-graded cut rather than one multiplier, because two things are true at
   once. A 40px impact word is nine glyphs across a 360px screen and can lose a
   sixth of itself while staying the loudest thing on the page; body copy at 15px
   cannot lose anything like that and still be body copy. So the cut runs from 5%
   at the floor of the scale to 18% at the top.

   767 rather than 991: a tablet has the measure to carry the desktop ramp, and
   768–991 is where the design was composed. The ~10% step at 768 lands on a
   width no phone reports.

   Space is deliberately untouched — more air per word, not a tighter page. */
@media screen and (max-width: 767px) {
  :root {
    --fs-micro: calc(var(--fs-lang-sm) * clamp(10.5px, calc(10.06px + 0.1229vw), 11px));
    --fs-cap: calc(var(--fs-lang-sm) * clamp(11.8px, calc(11.27px + 0.1474vw), 12.4px));
    --fs-sm: calc(var(--fs-lang-sm) * clamp(12.8px, calc(12.27px + 0.1474vw), 13.4px));
    --fs-body: calc(var(--fs-lang) * clamp(14.2px, calc(13.58px + 0.172vw), 14.9px));
    --fs-lead: calc(var(--fs-lang) * clamp(15.6px, calc(14.72px + 0.2457vw), 16.6px));
    --fs-h4: calc(var(--fs-lang) * clamp(16.4px, calc(15.16px + 0.344vw), 17.8px));
    --fs-h3: calc(var(--fs-lang) * clamp(17.8px, calc(16.03px + 0.4914vw), 19.8px));
    --fs-h2: calc(var(--fs-lang) * clamp(20.8px, calc(17.35px + 0.9582vw), 24.7px));
    --fs-h1: calc(var(--fs-lang) * clamp(25.5px, calc(18.16px + 2.0393vw), 33.8px));
    --fs-display: calc(var(--fs-lang) * clamp(28.2px, calc(18.21px + 2.7764vw), 39.5px));
    --fs-impact: calc(var(--fs-lang) * clamp(32.8px, calc(16.44px + 4.545vw), 51.3px));
  }
}

/* ── THE LARGE-DESKTOP RAMP ────────────────────────────────────────────────
   Above 1440 the layout keeps growing with --fluid-font, so the type follows it
   gently rather than freezing. Values continue from exactly where the base ramp
   stops, so 1440px itself is identical either way. */
@media screen and (min-width: 1440px) {
  :root {
    --fs-micro: calc(var(--fs-lang-sm) * clamp(12.5px, calc(9.5px + 0.2083vw), 13.5px));
    --fs-cap: calc(var(--fs-lang-sm) * clamp(14px, calc(11px + 0.2083vw), 15px));
    --fs-sm: calc(var(--fs-lang-sm) * clamp(15px, calc(12px + 0.2083vw), 16px));
    --fs-body: calc(var(--fs-lang) * clamp(17px, calc(11px + 0.4167vw), 19px));
    --fs-lead: calc(var(--fs-lang) * clamp(20px, calc(14px + 0.4167vw), 22px));
    --fs-h4: calc(var(--fs-lang) * clamp(22px, calc(16px + 0.4167vw), 24px));
    --fs-h3: calc(var(--fs-lang) * clamp(26px, calc(17px + 0.625vw), 29px));
    --fs-h2: calc(var(--fs-lang) * clamp(36px, calc(18px + 1.25vw), 42px));
    --fs-h1: calc(var(--fs-lang) * clamp(56px, calc(20px + 2.5vw), 68px));
    --fs-display: calc(var(--fs-lang) * clamp(70px, calc(16px + 3.75vw), 88px));
    --fs-impact: calc(var(--fs-lang) * clamp(100px, calc(22px + 5.4167vw), 126px));
    --section-y: clamp(92px, calc(44px + 3.3333vw), 108px);
  }
}

/* Arabic. Two corrections through the knobs rather than a fourth set of clamps,
   so one pair of numbers covers all three bands. The Arabic face carries a tall,
   wide body with marks above and dots below, so it sets LARGER than the Latin
   beside it at the same font-size; and it is a joined script, so letter-spacing
   breaks it.

   0.90 IS DELIBERATELY UNDER METRIC PARITY, AND P10 LEFT IT THERE. Zain's Latin
   cap is 69.5% of its em and Quicksand's effective cap is 65.1% of its own, so
   parity would be 0.651 / 0.695 = 0.937 — 0.90 is 4% below that, which is the
   direction the owner asked for. The oversizing that was reported on `/ar/` was
   never this knob: it was the LINE-HEIGHT, opened to 1.5 against a Latin 0.81,
   which made an Arabic heading block 1.85x the height of the Latin one at the
   same nominal size. That is fixed where it belongs, in `locale/arabic.css` and
   in the two sections that needed their own number. Moving `--fs-lang` would
   move all eleven tokens below it and invalidate every recorded Arabic
   measurement in the repo, for a correction the line-height already delivers. */
html[lang='ar'] {
  --fs-lang: 0.9;
  --fs-lang-sm: 0.96;
  --track-label: 0;
  --track-display: 0;
  --track-tight: 0;
  --track-body: 0;
}
}

/* ---- tokens · tokens/space.css ---- */
@layer tokens {
/* Spacing scale. Deliberately px-anchored, NOT rem-anchored.
   Reason: on the Golden Master, 1rem is a discontinuous function of viewport
   width (479px -> 13.31px, 480px -> 11.00px). rem-anchored spacing therefore
   jumps 2.31px across one pixel of resize. See docs/architecture/typography-contract.md. */
:root {
  --space-3xs: 2px;
  --space-2xs: 4px;
  --space-xs: 8px;
  --space-s: 12px;
  --space-m: 16px;
  --space-l: 24px;
  --space-xl: 40px;
  --space-2xl: 64px;
  --space-3xl: 96px;
}
}

/* ---- tokens · tokens/motion.css ---- */
@layer tokens {
/* Motion tokens.

   The first three are the rebuild's own scale. `--cubic-default`,
   `--duration-default` and `--animation-default` are the Golden Master's, read
   from its `:root` and carried across verbatim, because they are what every
   colour and ink transition in the header and the footer is timed to — the nav
   ink crossfade in particular is 0.75s on that curve, and changing it changes
   how the site FEELS on every route rather than how one component looks. */
:root {
  --duration-fast: 150ms;
  --duration-base: 300ms;
  --duration-slow: 680ms; /* observed: the legacy overlay's reveal tail. CONFIRMED. */

  --cubic-default: cubic-bezier(0.65, 0.05, 0, 1);
  --duration-default: 0.75s;
  --animation-default: var(--duration-default) var(--cubic-default);
}

/* Safe-area insets, named once.

   `env()` is only meaningful on a notched device AND only when the document
   declares `viewport-fit=cover`, which `layouts/base.html` does. Naming them
   here means a component reads `var(--sa-t)` instead of repeating an `env()`
   call with its own fallback — and a component that forgets the fallback
   silently computes to nothing on every browser that does not support it. */
:root {
  --sa-t: env(safe-area-inset-top, 0px);
  --sa-r: env(safe-area-inset-right, 0px);
  --sa-b: env(safe-area-inset-bottom, 0px);
  --sa-l: env(safe-area-inset-left, 0px);
}

/* THE EASING REGISTER — measured, not chosen.

   Every curve below was read off the Golden Master and is cited by the count of
   declarations it replaces. motion-architecture.md §7.2 is the full table; this
   is the half of it that CSS needs. The JS half is
   `src/runtime/animation/easings.js`, and the two are deliberately separate
   vocabularies: a GSAP ease NAME and a cubic-bezier are not provably the same
   curve, so nothing here claims they are.

   `--ease-standard: cubic-bezier(0.4, 0, 0.2, 1)` used to sit above and has been
   DELETED rather than renamed. It is Material Design's standard curve and it
   appears nowhere on the Golden Master — a placeholder that, left in place,
   would have been reached for by the first component that needed an easing and
   would have quietly introduced a curve this site does not use. */
:root {
  /* Ten identical `--*-ease` declarations on the Golden Master, one per page
     wrapper, never on :root. This is that one curve. */
  --ease-signature: cubic-bezier(0.215, 0.61, 0.355, 1);

  /* A DIFFERENT CURVE, and it stays separate. 12 transitions reach it through
     `--animation-default`, one directly, all inside the inherited shell. */
  --ease-shell: var(--cubic-default);
  --duration-shell: var(--duration-default);

  /* The page-transition overlay reveal, on all 70 legacy routes. */
  --ease-burst: cubic-bezier(0.66, 0, 0.28, 1);

  /* The mark draw: `ab-mark-draw` at 1.6s, and the wireframe's per-stop draw. */
  --ease-mark-draw: cubic-bezier(0.65, 0, 0.35, 1);

  /* The nav hamburger's lime wipe, 0.34s. Exactly one consumer. */
  --ease-nav-ham: cubic-bezier(0.22, 0.61, 0.36, 1);
}
}

/* ---- tokens · tokens/layer.css ---- */
@layer tokens {
/* ============================================================================
   THE Z-INDEX CONTRACT. One file owns every stacking decision on the site, so a
   component can never win a fight by inventing a bigger number.

   The Golden Master has NO z-index token at all: every stacking value is a
   literal, 23 distinct ones, and the ladder tops out at `2147483647` on the
   Webflow badge and `99999` on an overlay. Stacking there is settled by whoever
   picked the larger number, and it is invisible to anyone reading either rule.

   The values below are the ones the homepage actually depends on, read from the
   Golden Master and given names. Sections use the tokens; a raw z-index literal
   in a section stylesheet is a review failure.

   The layers below zero are a real contract and are asserted, not described:
   `.gl-wrap` must sit ABOVE the page dot-field and BELOW everything in flow, and
   the hero's tablet text sits below both. When the two canvases arrive in stages
   14 and 16 they take these slots; today the hosts are empty and the order still
   has to hold. */
:root {
  /* Below the document. */
  --z-hero-tablet-text: -5; /* the only -5 on the site: hero text under the GL layer */
  --z-page-field: -2; /* the page dot-field canvas (stage 14) */
  --z-gl: -1; /* the WebGL host (stage 16); ABOVE the dot-field */

  /* In flow. */
  --z-base: 0;
  --z-raise: 1; /* nudge a sibling above its neighbour */
  --z-section: 5; /* a section that must overlap the one before it */
  --z-section-high: 6; /* the stack grid, which overlaps in both directions */
  --z-hero: 10; /* the hero, over the sticky track it lives in */
  --z-sticky-over: 20; /* a sticky element that must clear section content */

  /* Chrome.

     THE MENU PANEL SITS *UNDER* THE HEADER'S OWN CONTROL ROW, and that ordering
     is the whole reason the close button exists (P8-01). Both are children of
     the same `.nav` element, so `--z-nav` is not the comparison — these two are:
     the panel is `position: fixed; inset: 0` and covers the viewport, and the
     bar has to stay on top of it or the very button that closes the menu is
     painted over by the thing it closes. The rebuild shipped the panel at 900
     against a bar at 50 and the button vanished on open; the Golden Master ships
     25 against 50, and every other rule in `components/site-nav.css` already
     assumes the legacy order — `.nav-ham[aria-expanded='true']` styles the
     button as a white tile ON the dark panel, and `navigation/centre-mark.js`
     fades the centre mark to 0 on open precisely because it would otherwise be
     legible over it. Two rules written for a stacking order the stylesheet did
     not have. */
  --z-nav: 120; /* the fixed header */
  --z-menu: 25; /* the menu panel — inside the header, UNDER the bar */
  --z-nav-bar: 50; /* the header's control row — over the panel it opens */
  --z-overlay: 1000; /* the landscape notice; the legacy uses 9999 for no reason */

  /* THE PAGE LOADER, above everything including the landscape notice.
     It covers the document for the first 1.58 s of every load and the notice is
     one of the things it covers, so the two cannot share a slot. The legacy
     stacks the same pair the same way (9999 over 9998) and its own
     `assistant.css` records the rule in words: the page-transition screen
     "must always" be on top. */
  --z-loader: 1100;
}
}

/* ---- base · base/fonts.css ---- */
@layer base {
/* ============================================================================
   THE FONT CONTRACT.

   Four physical files, THREE synthetic families. That is not redundancy and the
   families must not be merged just because two of them point at the same
   `.woff2`: each declares a different `size-adjust` / `ascent-override` /
   `descent-override` triple, and those overrides are what make a line box the
   height it is. Merging two families that share a file changes every line box
   that used the other one's metrics.

       Quicksand          the body face          size-adjust 95.6 / 94.2 / 93.0
       Quicksand Label    the label face         size-adjust 109.3, flat
       Quicksand Display  the display face       size-adjust 93.0

   `Quicksand Label` exists because the label family had no real weight above
   400: the 500–900 range resolves to SemiBold there while the body family
   resolves the same range to Medium. `Quicksand Display` is SemiBold cut to the
   BODY face's metrics, so a display heading and the paragraph under it sit on
   the same rhythm.

   The metric overrides are load-bearing and are carried across verbatim from the
   Golden Master (`f04 §8.5`). `tests/unit/fonts.test.mjs` asserts every value.

   `font-display: swap` on all seven: the fallback is Arial, the overrides above
   are tuned so the swap does not reflow, and blocking text on a webfont is the
   one failure mode that makes a content site useless while it loads.

   Licence: SIL Open Font Licence 1.1. The licence text ships beside the fonts at
   `src/assets/fonts/quicksand/ofl.txt` and is emitted with them.
   ========================================================================== */

/* ── Quicksand — the body face ───────────────────────────────────────────── */
@font-face {
  font-family: Quicksand;
  src: url('/assets/fonts/quicksand/light.woff2') format('woff2');
  font-weight: 200 300;
  font-style: normal;
  font-display: swap;
  size-adjust: 95.6%;
  ascent-override: 99.4%;
  descent-override: 25.1%;
}

@font-face {
  font-family: Quicksand;
  src: url('/assets/fonts/quicksand/regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  size-adjust: 94.2%;
  ascent-override: 100.8%;
  descent-override: 25.5%;
}

@font-face {
  font-family: Quicksand;
  src: url('/assets/fonts/quicksand/medium.woff2') format('woff2');
  font-weight: 500 900;
  font-style: normal;
  font-display: swap;
  size-adjust: 93%;
  ascent-override: 102.1%;
  descent-override: 25.8%;
}

/* ── Quicksand Label — flat metrics, the whole point of the family ───────── */
@font-face {
  font-family: 'Quicksand Label';
  src: url('/assets/fonts/quicksand/light.woff2') format('woff2');
  font-weight: 200 300;
  font-style: normal;
  font-display: swap;
  size-adjust: 109.3%;
  ascent-override: 85.7%;
  descent-override: 21%;
}

@font-face {
  font-family: 'Quicksand Label';
  src: url('/assets/fonts/quicksand/regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  size-adjust: 109.3%;
  ascent-override: 85.7%;
  descent-override: 21%;
}

@font-face {
  font-family: 'Quicksand Label';
  src: url('/assets/fonts/quicksand/semibold.woff2') format('woff2');
  font-weight: 500 900;
  font-style: normal;
  font-display: swap;
  size-adjust: 109.3%;
  ascent-override: 85.7%;
  descent-override: 21%;
}

/* ── Quicksand Display — SemiBold on the BODY face's metrics ─────────────── */
@font-face {
  font-family: 'Quicksand Display';
  src: url('/assets/fonts/quicksand/semibold.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  size-adjust: 93%;
  ascent-override: 102.1%;
  descent-override: 25.8%;
}
}

/* ---- base · base/document.css ---- */
@layer base {
/* Element defaults. No class selectors live here — that is what `layout`,
   `components` and `sections` are for. */

html {
  /* THE LAYOUT UNIT. `1rem` on this site is a function of viewport width, and
     `rem` then owns nearly every box. The ladder itself, its two deliberate
     discontinuities and the reason none of it may be smoothed are in
     `tokens/root-font.css`. This line is the only place it is applied. */
  font-size: var(--fluid-font);

  /* The scroll owner is a runtime service (`src/runtime/scroll/`), not a CSS
     property. `scroll-behavior: smooth` here would put a second, uncancellable
     animator on the same scroll position — which is exactly the collision the
     Golden Master has between Lenis and a Webflow module over 689 anchors. */
  scroll-behavior: initial;

  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;

  /* THE BROWSER'S OWN BAR IS THE PRODUCT (P6-10), AND THIS IS THE WHOLE OF ITS
     STYLING: two standard properties and no vendor pseudo-element.

     The pseudo-element is banned by `tests/unit/chrome-contract.test.mjs`, and
     the ban is not stylistic. The shortest path from "make it thinner" to "make
     it disappear" runs through that selector, and the decision this site made
     was to delete its OWN bar and keep the browser's — so hiding the browser's
     would leave the page with none at all, which is worse than the defect.
     `thin`, therefore, and never the value that means nothing: golden:navigation
     asserts the computed value is not that, and that the document still scrolls.

     THE THUMB IS THE SITE'S OWN MID INK, and it is the one value that reads on
     both of this site's grounds. #535450 measures 6.89:1 against the paper the
     page rests at (#edf5f3) and 2.68:1 against the ink the home page's colour
     chapter scrubs to (#040504). #3b3c38 vanishes into the dark chapter at
     1.84:1 and #b9bbad shouts on it at 10.47:1. The certificate section's own
     eyebrow rule made the same measurement and reached the same value.

     THE TRACK IS TRANSPARENT, which is what makes the bar follow the chapter
     without anything subscribing to it: the gutter shows the canvas the page is
     actually painting. `visual-state/colour-bus.js` writes that as an inline
     style on <body> rather than through a token, so CSS could not have read it
     anyway — and a bus subscriber rewriting a scrollbar colour every scrubbed
     frame would be a per-frame write on the root element, for a hairline, in a
     codebase whose rest gate exists to prove the page holds zero frames.

     NO `scrollbar-gutter`. Nothing here is a scroll container that appears and
     disappears — the document always scrolls — so `stable` would buy no
     stability and would RESERVE a gutter on the overlay platforms that have
     none today, moving every centred box by half of it.

     Unsupported below Chrome 121 and Safari 18.2, where the bar renders at its
     platform default. That is the same bar this site already shipped. */
  scrollbar-width: thin;
  scrollbar-color: var(--color--dark-green-tint-2) transparent;

  /* ── THE ROOT DECLARES NO BACKGROUND, AND THAT IS A HARD RULE ────────────
     NOT A STYLE CHOICE. It is the condition on which the WebGL hero is visible
     at all, and this comment is here because the rule was broken once and the
     home page went black.

     `<body>`'s background reaches the canvas by PROPAGATION (CSS Backgrounds
     §2.11.2), and propagation happens only while the root declares neither a
     colour nor an image. While it holds, `<body>` paints NOTHING of its own: its
     background is lifted to the root and painted at step 1 of the paint order
     (CSS 2.1 Appendix E) — underneath everything, including the negative layer.

     DECLARE ANYTHING HERE AND `<body>` STARTS PAINTING ITS OWN BOX, at step 3 —
     which is ABOVE step 2, where negative-`z-index` descendants are painted.
     `components/gl-layers` hosts the hero canvas as `position: fixed; z-index: -1`
     and the tablet hero text sits at `-5`, so both go straight under an opaque
     `<body>`. On the home page that fill is `#040504`, written every frame by
     `visual-state/colour-bus.js`, and the result is a black rectangle where the
     portrait, the dot field and the wireframe used to be. Measured: the renderer
     still reported `state: "ready"` with sixty frames drawn and every texture
     loaded, because nothing was wrong with it — it was being covered.

     Keep the root transparent and keep `<body>` to a single colour. A root
     background hides those negative layers; a body background image is
     propagated to the root canvas and gives mobile Safari a separate painted
     strip behind its bottom chrome. */

  /* THE SCROLL OWNER OWNS THE BOUNCE, NOT THIS FILE. `overscroll-behavior` is
     deliberately absent: `none` would take the platform's pull-to-refresh away
     from every visitor to remove a band that this file paints correctly. */
}

body {
  min-height: 100svh;

  /* A COLOUR, NOT AN IMAGE. The body background is propagated to the root
     canvas. Keeping it to one colour lets iOS Safari composite the document
     behind its bottom controls instead of treating a viewport-sized gradient as
     an opaque browser-chrome backing layer. It also keeps the homepage colour
     bus's scroll-time writes to a flat fill rather than repainting a gradient
     over the full document. */
  background-color: var(--page-bg);

  color: var(--page-ink);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--track-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
  text-wrap: balance;
}

p {
  text-wrap: pretty;
}

a {
  color: inherit;
  text-decoration: none;
}

/* A link with no href is not a link. Making that visible in development is
   cheaper than discovering it in an audit. */
a:not([href]) {
  cursor: default;
}

button {
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
  cursor: pointer;
}

:focus-visible {
  outline: 2px solid var(--color--lime);
  outline-offset: 2px;
}

/* Images declare their own aspect ratio through width/height attributes; this
   keeps them fluid without losing the ratio the attributes reserve. */
img,
video {
  height: auto;
}

::selection {
  background-color: var(--color--lime);
  color: var(--color--dark-green);
}
}

/* ---- base · base/typography.css ---- */
@layer base {
/* ============================================================================
   THE TEXT CLASSES. One class per ROLE, and every one of them reads a token from
   `tokens/type.css`. Nothing here invents a number.

   The naming is the Golden Master's and is kept deliberately: `-mona` is the
   body/display face at a role's weight, `-brier` is the accent (display) face at
   the same size. Those two suffixes appear in the markup, in every evidence
   extract and in the system map, so renaming them would break every
   cross-reference for no reader's benefit.

   WHAT WAS DROPPED, AND WHY IT CHANGES NOTHING. The Golden Master repeats
   `font-size: var(--fs-x)` inside the 991, 767 and 479 media queries for most of
   these classes. Those repeats are no-ops: `--fs-*` already carries its own three
   bands, so re-declaring the same token at a breakpoint resolves to the same
   value. They are not reproduced. Every repeat that is NOT a no-op — where a
   breakpoint switches to a DIFFERENT token — is kept and marked.

   `font-variation-settings` is also dropped throughout. The site ships static
   instances of Quicksand, not a variable font, so every `"wght" 750` in the
   legacy sheet is inert; the real weight comes from `font-weight` selecting one
   of the three declared faces. Keeping a declaration that provably does nothing
   is how the next reader concludes the type is variable and tunes a number that
   cannot move.
   ========================================================================== */

/* ── the impact words: uppercase, tight, the loudest thing on a page ─────── */
.text-impact-reg-mona {
  font-family: var(--font-body);
  font-size: var(--fs-impact);
  font-weight: 700;
  line-height: 0.816;
  letter-spacing: -0.036em;
  text-transform: uppercase;
}

.text-impact-reg-mona.line-increase {
  line-height: 0.85;
}

.text-impact-reg-brier {
  font-family: var(--font-display);
  font-size: var(--fs-impact);
  font-weight: 700;
  line-height: 0.81;
  letter-spacing: -0.031em;
  text-transform: uppercase;
}

.text-impact-sm-mona {
  font-family: var(--font-body);
  font-size: var(--fs-display);
  font-weight: 600;
  line-height: 0.856;
  letter-spacing: -0.0625px;
  text-transform: uppercase;
}

/* Inherits at every width, on purpose. The class exists in exactly one place —
   the accent spans inside the footer statement — so its size is only ever meant
   to be the sentence's own. The Golden Master gave it a larger literal, which
   rendered the two green words bigger than the words either side of them. */
.text-impact-sm-brier {
  font-family: var(--font-display);
  font-size: inherit;
  font-weight: 700;
  line-height: 0.856;
  letter-spacing: -0.024em;
  text-transform: uppercase;
}

/* ── section titles ─────────────────────────────────────────────────────── */
.text-title-lg-mona {
  font-family: var(--font-body);
  font-size: var(--fs-h1);
  font-weight: 700;
  line-height: 0.886;
  letter-spacing: -0.028em;
  text-transform: uppercase;

  /* ONE SENTENCE, SO IT BREAKS LIKE ONE (P10). Every section title on the site
     is now a single element on a single line — `SELECTED PROJECTS` rather than
     `SELECTED` stacked over `PROJECTS` — and at every width the goldens measure
     it fits. Below about 400px the longest of them, the English
     `FIVE COURSES ONE CREDENTIAL`, needs two lines, and `balance` splits them
     evenly instead of leaving one orphaned word under a full one.

     `pretty` is the wrong tool: it optimises the LAST line of a paragraph, and
     these are never paragraphs. An engine without either wraps normally, which
     is the same line count — so this is progressive, not load-bearing.

     This class has exactly five users, the five section titles, so the rule
     reaches nothing else. */
  text-wrap: balance;
}

.text-title-reg-mona {
  font-family: var(--font-body);
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: 0.886;
  letter-spacing: -0.048em;
}

.text-title-reg-brier {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 700;
  line-height: 0.886;
  letter-spacing: -0.048em;
}

/* ── body and lede ──────────────────────────────────────────────────────── */
.text-body-reg-mona {
  font-family: var(--font-body);
  font-size: var(--fs-body);
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: var(--track-body);
  text-transform: none;
}

.text-cta-short-intro {
  font-size: var(--fs-h3);
  line-height: 1.04;
  letter-spacing: 0.02em;
}

.text-cta-short-intro.font-brier {
  letter-spacing: 0.025em;
}

/* ── labels, eyebrows and chips ─────────────────────────────────────────── */
/* A REAL weight, not `font-variation-settings: "wght" 700`. The variable-font
   setting the Golden Master inherited is a no-op on the static instances that
   actually ship, so every eyebrow on the site was rendering Regular at ~10-11px,
   uppercase, usually light-on-dark — where a rounded geometric sans goes thin
   and shimmery. `600` selects the real SemiBold face, whose three metric
   overrides are identical to Regular's, so no line box moves. */
.text-eyebrow {
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.text-eyebrow.large {
  font-size: var(--fs-lead);
  letter-spacing: -0.009em;
}

/* Pushed off the reading edge. The class is named for the physical side it had
   on the Golden Master; the declaration follows the reading direction, so an
   Arabic eyebrow is pushed from the right as a Latin one is from the left.
   (No markup carries it today — kept because the eyebrow system is transcribed
   whole, and a rule that is wrong in RTL is worse than one that is unused.) */
.text-eyebrow.auto-left {
  margin-inline-start: auto;
}

.text-eyebrow.flex {
  display: flex;
  flex: none;
  gap: calc(var(--gap) * 0.1);
  justify-content: flex-start;
  align-items: flex-start;
}

.text-title-small-label {
  font-size: var(--fs-micro);
  font-weight: 600;
  line-height: 1.25;
  text-transform: none;
}

.text-title-small-label.text {
  font-size: var(--fs-cap);
}

.text-title-small-label.date {
  color: var(--color--lime);
  font-size: var(--fs-cap);
  font-weight: 700;
}

.text-title-small-label.bold {
  font-weight: 800;
}

/* ── the face switch, usable on any role ────────────────────────────────── */
/* `.span-font-brier` was its inline twin and had exactly one user, the socials
   heading, which P10 collapsed to a single face. One class, one job. */
.font-brier {
  font-family: var(--font-display);
}

/* ── colour modifiers ───────────────────────────────────────────────────── */
.c-lime {
  color: var(--color--lime);
}

.c-lime-off {
  color: var(--color--lime-off);
}

.c-grey-on-track {
  color: var(--color--grey-on-track);
}

.c-green-off-white-2 {
  color: var(--color--green-off-white-2);
}

/* ── responsive: only the steps that are NOT no-ops ─────────────────────── */
@media screen and (max-width: 991px) {
  /* The footer statement drops a whole role rather than a percentage. */
  .text-impact-sm-mona.footer {
    font-size: var(--fs-display);
  }
}

@media screen and (max-width: 767px) {
  .text-title-small-label,
  .text-title-small-label.text,
  .text-title-small-label.date {
    line-height: var(--lh-label);
  }
}

@media screen and (max-width: 479px) {
  .text-title-small-label {
    line-height: 1;
  }

  .text-eyebrow.mob-only {
    display: block;
  }

  .text-eyebrow.dt-only {
    display: none;
  }
}

@media screen and (min-width: 480px) {
  .text-eyebrow.mob-only {
    display: none;
  }
}
}

/* ---- layout · layout/container.css ---- */
@layer layout {
/* THE CLIP THAT KEEPS THE PAGE FROM SCROLLING SIDEWAYS.

   The journey rail is deliberately wider than the viewport — 4,052 px at 1440 —
   and something has to clip it or the whole document gains a horizontal
   scrollbar. This is the rebuild's `.page-w`.

   THREE THINGS ABOUT THIS RULE ARE LOAD-BEARING.

   `clip`, not `hidden`: `overflow: hidden` creates a scroll CONTAINER, which
   can still be scrolled programmatically and by a focus jump, and which becomes
   the nearest scrollport for every `position: sticky` descendant — silently
   re-anchoring seven sticky contexts to a box that never scrolls.

   `-x` only: `overflow-x: clip` paired with `overflow-y: visible` is the one
   combination that clips one axis WITHOUT creating a scroll container at all.
   (`hidden` on one axis forces `auto` on the other; `clip` does not.)

   And it is on `<main>`, not on `<body>`. Body's overflow PROPAGATES to the
   viewport, and a propagated `clip` is treated as `hidden` there — measured:
   `window.scrollTo(9999, y)` still moved the page 2,612 px. On a real element it
   simply clips.  */
main.main-w {
  overflow-x: clip;
}

/* The one container. Every section's inner width comes from here.

   `max-width: var(--fluid-container)` is deliberate and is NOT the same as a
   fixed max-width: `--fluid-container` is `clamp(--min-width, 100vw, --max-width)`
   from the root ladder, so the measure tracks the same four bands the type unit
   does. Replacing it with a constant decouples the container from the ladder and
   every section's gutters stop agreeing with its rem-based internals. */
.c {
  width: 100%;
  max-width: var(--fluid-container);
  margin-inline: auto;
  padding-inline: var(--padding--container);
}

/* A section is a positioning context by default. Sections that need to overlap
   their neighbours raise themselves with a token from `tokens/layer.css`; none
   of them invents a number. */
.s {
  position: relative;
}

/* A run of sections that share one background. The dark chapter uses it.

   THE GROUND AND THE INK ARE DECLARED TOGETHER, on purpose. On the Golden Master
   `body` is `color: #edf5f3` — the document's default ink is WHITE, and the few
   sections that sit on paper darken it locally. The rebuild inverts that: the
   page's settled background is paper, so `--page-ink` rests dark. Both are
   defensible; what is not is a rule that paints a black ground and says nothing
   about what is written on it. Measured before this line existed: the stack-grid
   head, its accent span and the callout's intro all rendered #040504 on #040504.

   Any future rule that paints a ground owes its subtree an ink in the same
   block. */
.s-group {
  position: relative;
}

.s-group.bg-black {
  background-color: var(--color--black);
  color: var(--color--white);
}

/* THE STICKY PIN, declared once for the two tracks that use it.

   `.sticky-item` is the pinned child of a `.sticky-track`, and the track's own
   height is what decides how long the pin lasts — 200vh for the hero, 200vh for
   the split screen. That relationship is the same in both places, so the rule
   belongs here rather than being a shared class two section files each hold an
   opinion about.

   NATIVE STICKY, not a JS pin. The Golden Master pins with `position: sticky`
   everywhere and creates zero pin-spacers at runtime; a scroll-library pin
   changes document height, which changes the scroll limit, which changes every
   offset measured against it. */
.sticky-track {
  position: relative;
}

/* `position` and `top` only. The HEIGHT of a pinned item is a section decision:
   the hero's is a full viewport, the split screen's comes from its own content,
   and a shared `height: 100vh` here would silently re-price whichever one did
   not want it. */
.sticky-item {
  position: sticky;
  top: 0;
}
}

/* ---- layout · layout/page-shell.css ---- */
@layer layout {
/* ============================================================================
   THE SECTION-HEAD LAYOUT. Three homepage heads — experiments, credential,
   disciplines — plus the projects ring read as one centred column. It is shared
   scaffolding rather than a section style, so it lives in `layout`.

   `.grid-main` is the four-track grid the rest of the site's heads use;
   `.ab-mid` collapses it to one centred track. Both are here because a section
   picks one and neither belongs to a section.
   ========================================================================== */

.grid-main {
  display: grid;
  width: 100%;
  grid-template-rows: auto;
  grid-template-columns: 23.55% 23.55% 23.55% 23.55%;
  grid-auto-columns: 1fr;
  column-gap: var(--gap);
}

/* ONE ELEMENT NOW, NOT TWO (P10). Every section title used to be a pair of
   `<h2>`s stacked by the `.flex` modifier below, in two typefaces. The owner
   asked for one heading on one line in one weight, so this box holds a single
   `<h2>` whose two halves are two `<span>`s — both wrapped, deliberately; see
   `components/text-reveal.css` for the reason the unstyled one is load-bearing.
   The column axis is vestigial and kept because `.ab-mid`'s centring below is
   what shrink-wraps the heading to its own width. */
.title-headline-w {
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
}

.title-headline-w.flex {
  flex-flow: column;
}

.title-para-layout {
  display: flex;
  gap: calc(var(--gap) * 2);
}

.title-para-w.mw {
  display: flex;
  max-width: 25rem;
}

.title-para-w.mw.flex-1 {
  display: flex;
  flex-flow: column;
  gap: var(--gap);
  justify-content: flex-start;
  align-items: flex-start;
}

.eyebrow-w {
  display: flex;
  gap: calc(var(--gap) * 0.5);
  overflow: clip;
}

.ab-mid .title-headline-w,
.ab-mid .title-para-layout {
  justify-content: center;
  align-items: center;
}

.ab-mid .eyebrow-w {
  justify-content: center;
}

/* The label leads the head instead of the paragraph, and sits closer to it than
   the row gap alone would allow. */
.ab-mid-brow {
  color: var(--color--grey-on-track);
  margin-bottom: -1rem;
}

@media screen and (max-width: 991px) {
  .title-para-layout.tablet-wrap {
    flex-flow: column;
    justify-content: flex-start;
    align-items: flex-start;
  }
}

@media screen and (max-width: 479px) {
  .title-layout {
    display: grid;
    grid-template-rows: auto;
    grid-template-columns: 1fr 1fr;
    grid-auto-columns: 1fr;
    gap: 16px;
  }

  .title-layout.grid-main {
    gap: calc(var(--gap) * 2);
    margin-bottom: calc(var(--gap) * 2);
  }

  .title-para-w.mw.flex-1 {
    max-width: none;
  }
}

/* ── THE CENTRED HEAD, AND WHY IT SITS BELOW THE MEDIA QUERIES ───────────────
   `.title-layout.ab-mid` and `.title-layout.grid-main` are both TWO classes, so
   specificity cannot separate them and source order decides. The Golden Master
   emits `.ab-mid` at line 9,877 and the 479-width `.grid-main` override at
   6,687, so `row-gap: 1.75rem` wins on a phone.

   With the rules the other way round the candidate took `calc(var(--gap) * 2)`
   = 2.5rem instead, on two rows of a three-row head: 8.25px too much per gap,
   16.5px in total, which is exactly what made the certificate's head taller at
   390. Same defect as the paragraph column below, same fix, and the pair are
   kept together so a future reorder moves both. */
.title-layout.ab-mid {
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  row-gap: 1.75rem;
  text-align: center;
}

/* The four-track template spans these cells across two of four tracks. Left
   alone in a single-column grid, that would conjure a phantom second track. */
.title-layout.ab-mid > * {
  grid-column: 1;
  width: 100%;
}

/* ── THE CENTRED HEAD'S COLUMN ───────────────────────────────────────────────
   `.ab-mid .title-para-w.mw` and `.title-para-w.mw.flex-1` are BOTH three
   classes, so specificity cannot separate them and SOURCE ORDER decides. On the
   Golden Master the `.ab-mid` rule is emitted at line 9,902 and the 479-width
   override at 6,696, so the 30rem cap wins at every width — including on a
   phone.

   This file had them the other way round, and the consequence was measurable
   rather than theoretical: at 390 the certificate's lede got a 362.5px column
   against the reference's 330px, wrapped differently, and made that section's
   head 16.5px taller. That is most of the -6px the ledger carried as
   `R9-mobile-section-heights`.

   Moving the rule below the media query reproduces the reference's cascade. It
   is deliberately NOT solved with `!important` (INV-10) or with an extra class:
   the two rules disagree, one of them is meant to win, and the site says which. */
.ab-mid .title-para-w,
.ab-mid .title-para-w.mw {
  max-width: 30rem;
  margin-inline: auto;
  justify-content: center;
}
}

/* ---- components · components/skip-link.css ---- */
@layer components {
/* ============================================================================
   THE SKIP LINK — and the reason it is a component rather than a utility.

   The Golden Master homepage has none (defect L7). Where the other routes DO
   have one, four of six sit at a z-index BELOW `.nav`'s 120, so the link is
   focusable, announced, and completely covered by the header the moment it is
   focused. It is reachable and invisible, which is worse than absent because it
   passes a naive audit.

   So this one is declared against the same token the header is, and above it.
   `accessibility.md` G-1 records the missing `--z-skip-link`; it is
   `calc(var(--z-nav) + 1)`, expressed as arithmetic on the header's own token so
   the two cannot drift apart.
   ========================================================================== */

.skip-link {
  position: fixed;
  top: 0;
  left: 0;
  z-index: calc(var(--z-nav) + 1);
  padding: var(--sp-4) var(--sp-6);
  background-color: var(--color--dark-green);
  color: var(--color--white);
  border-bottom-right-radius: 0.54rem;
  font-family: var(--font-label);
  font-size: var(--fs-cap);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;

  /* Off-screen rather than `display: none`: a display-none element is not
     focusable at all, so the link has to remain in the layout to be reachable
     by Tab. It travels back on focus. */
  transform: translateY(-110%);
  transition: transform 0.2s var(--ease-signature);
}

.skip-link:focus-visible {
  transform: translateY(0);
  outline: 2px solid var(--color--lime);
  outline-offset: -4px;
}

@media (prefers-reduced-motion: reduce) {
  .skip-link {
    transition-duration: 0.01s;
  }
}
}

/* ---- components · components/scroll-lock.css ---- */
@layer components {
/* ============================================================================
   THE SCROLL LOCK'S CSS HALF — a TESTED CONTRACT, not styling.

   `ScrollController.lock()` needs two mechanisms and neither alone is enough:

     · `overflow: clip` on <html>, below. This is what stops the KEYBOARD, the
       scrollbar drag, Home/End and programmatic `window.scrollTo`.
     · `preventDefault()` inside the smoother's own wheel/touch handler. That is
       what stops wheel and touch.

   Delete this rule and the lock still LOOKS implemented — the attribute is
   still written, the method still returns, nothing errors — and the page behind
   an open menu silently scrolls. That is why `tests/unit/scroll-lock.test.mjs`
   asserts the rule exists in the emitted stylesheet.

   `clip` rather than `hidden`: `overflow: hidden` on the root creates a scroll
   container that can still be scrolled programmatically, and it resets scroll
   position on some engines. `clip` cannot be scrolled by anything.

   <body> IS NEVER TOUCHED. The `position: fixed` body-lock every modal library
   reaches for destroys scroll position; this lock provably preserves it — the
   Golden Master's own measurements show scrollY unchanged through a full
   open/close cycle, and that is the behaviour being kept.
   ========================================================================== */

html[data-scroll-locked] {
  overflow: clip;
}

/* ── the smoother's own state classes ──────────────────────────────────────
   Lenis writes `lenis`, `lenis-smooth` and `lenis-stopped` on <html> itself.
   These rules are its shipped stylesheet, reproduced here rather than linked,
   because the project emits exactly one stylesheet and a second <link> for four
   rules is not a trade worth making.

   ONE OF THEM IS FIXED IN THE COPY. Upstream ships
   `.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain }`,
   and attribute selectors do not match prefixes — so on a site whose inner
   scrollers all carry `data-lenis-prevent-TOUCH`, that rule matches nothing.
   The Golden Master ships the same no-op: 126 `data-lenis-prevent-touch`
   attributes across 36 files, and ZERO bare `data-lenis-prevent`. Porting the
   shipped selector would port the bug, so all three attribute names are listed. */
html.lenis,
html.lenis body {
  height: auto;
}

html.lenis.lenis-smooth [data-lenis-prevent],
html.lenis.lenis-smooth [data-lenis-prevent-touch],
html.lenis.lenis-smooth [data-lenis-prevent-wheel] {
  overscroll-behavior: contain;
}

html.lenis.lenis-stopped {
  overflow: clip;
}

/* An iframe swallows wheel events, which strands the smoother mid-gesture. */
html.lenis.lenis-smooth iframe {
  pointer-events: none;
}
}

/* ---- components · components/site-nav.css ---- */
@layer components {
/* ============================================================================
   THE GLOBAL HEADER — settled state only.

   THE INK MODEL, which is the part that is easy to get wrong. The header carries
   exactly two inks and every control paints from that pair, never from a colour
   of its own:

       --nav-ink       the mark, the centre mark and the menu glyph
       --nav-ink-soft  the small "Abbod" half of the wordmark

   Who DECLARES them is the whole mechanism. They are inherited custom
   properties, so the nearest declaring ancestor wins for its subtree with no
   specificity contest anywhere:

       .nav[data-nav-theme]  the FLOOR — page- and section-level. This is what
                             first paint, a no-JS page, and any backdrop that
                             cannot be measured all get.
       [data-nav-ink]        ONE control, set later from the colour actually
                             painted behind that control's own box.

   The left mark and the right buttons must be able to disagree: measured on the
   Golden Master, the mark can sit on black while the buttons sit on white, and
   one attribute on the bar cannot be right for both. Removing `data-nav-ink` is
   the well-defined "no reading" state — the control falls back to the section
   floor rather than to a guess. `runtime/navigation/nav-ink.js` (stage 14) is
   the only writer, and it is an ENHANCEMENT over the floor: with it dead the
   floor is still correct, which is why it is its own subsystem row.
   ========================================================================== */

[data-nav-theme='dark'],
[data-nav-ink='dark'] {
  --nav-ink: var(--color--dark-green);
  --nav-ink-soft: var(--color--dark-green);
}

[data-nav-theme='light'],
[data-nav-ink='light'] {
  --nav-ink: var(--color--white);
  --nav-ink-soft: var(--color--grey-2);
}

/* ── the bar ─────────────────────────────────────────────────────────────
   `pointer-events: none` on the bar and `auto` on each control: the header is
   a fixed strip across the whole top of the page, and without this it would
   swallow every click in the first ~5rem of every section. */
.nav {
  position: fixed;
  inset: 0 0 auto;
  z-index: var(--z-nav);
  display: flex;
  justify-content: space-between;
  align-items: center;
  pointer-events: none;
}

/* The control row. `z-index` from the token file rather than a literal, because
   the number is only meaningful next to `--z-menu` — the panel is this row's
   sibling and has to stay under it (see `tokens/layer.css`). */
.nav-inner {
  position: relative;
  inset: 0 0 auto;
  z-index: var(--z-nav-bar);
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  padding: var(--gap);
}

/* ── the header and the menu keep the Latin arrangement, on every document ──
   Owner decision. The bar is a brand object rather than prose: wordmark on the
   left, controls on the right, on `/`, `/de/` and `/ar/` alike. The PAGE inside
   it still reads right to left.

   `direction`, not a `flex-direction: row-reverse` per container, and the
   difference matters. There is no explicit RTL rule in this file today — the
   flip was purely implicit, out of `dir="rtl"` on `<html>` plus
   `justify-content: space-between` — so turning the CAUSE off is a smaller
   change than turning seven effects off one at a time, and the eighth container
   somebody adds here later is right by default instead of wrong by omission.
   `direction` is the one property every implicit flip below it reads: the flex
   main axis, `text-align: start`, every logical property, and inline-box order.

   IT ALSO PUTS DOM ORDER AND VISUAL ORDER BACK IN AGREEMENT. Tab order is DOM
   order and does not follow `direction`, so under the implicit flip a keyboard
   visitor on `/ar/` walked left to right through controls that painted right to
   left. */
html[dir='rtl'] .nav,
html[dir='rtl'] .nav-menu-w {
  direction: ltr;
}

/* ...and the Arabic inside it is still Arabic. `direction: rtl` restores the
   bidi BASE direction for each run, so a trailing comma or a mixed run orders
   correctly; `unicode-bidi: isolate` stops that run reordering its neighbours
   inside the left-to-right box that now contains it. Shaping is a property of
   the font and the codepoints and is unaffected by either — the joins are
   `animation/split-registry.js`'s problem, not this rule's.

   `.btn-text.is-lang-ar` is named explicitly: its anchor carries `dir="rtl"` in
   the markup, but that is a presentational hint and loses to any author
   declaration. */
html[dir='rtl']
  :is(.nav, .nav-menu-w)
  :is(.text-nav-link, .text-eyebrow, .nav-lang-label, .visually-hidden, .btn-text.is-lang-ar) {
  direction: rtl;
  unicode-bidi: isolate;
}

/* ── the wordmark ───────────────────────────────────────────────────────── */
.nav-brand-link {
  pointer-events: auto;
  /* Physical, and correct on every document BECAUSE of the rule above: the
     wordmark is physically left everywhere, so it scales from its own outer
     corner into the page. `transform-origin` does not read `direction`, so
     without that rule this grew away from the edge it sits on. Read by
     `runtime/navigation/nav-group-scale.js`. */
  transform-origin: 0 0;
}

.nav-brand {
  display: block;
  width: 8.05rem;
  height: 3.75rem;
}

.nav-brand svg {
  display: block;
  width: 100%;
  height: 100%;
}

.nav-brand path {
  transition: fill var(--animation-default);
}

.nav-brand [data-brand-ink='1'] {
  fill: var(--nav-ink-soft);
}

.nav-brand [data-brand-ink='2'] {
  fill: var(--nav-ink);
}

/* ── the centre mark ────────────────────────────────────────────────────── */
.page-ln4-pos {
  position: absolute;
  inset: 0 0 auto;
  z-index: 1;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  pointer-events: none;
  /* The Golden Master's own pair, not `--animation-default`: `.3s ease` on both
     properties. Measured on the reference at scroll 120, the settled hidden
     state is `matrix(1, 0, 0, 1, 0, -6.66667)` — exactly -0.5rem at a 13.3333px
     root. */
  transition:
    opacity 0.3s ease,
    transform 0.3s ease;
}

/* THE MARK LEAVES WITH THE HERO (P6-08).
   `navigation/centre-mark.js` has always written this class at the right
   moment — measured, it appears between scroll 60 and 120 on both targets,
   which is the same threshold the reference uses. It had NO RULE ANYWHERE in
   the emitted stylesheet, so the class was inert and the mark rode the whole
   18,700 px document. That is the "centre mark should disappear when scrolling
   down from the hero" the owner reported, and the fix is the declaration the
   class was always asking for.

   ON THE POSITIONER, NOT THE ANCHOR. Hiding `.nav-middle` would pull a link out
   of the tab order on scroll; fading its positioner leaves the link reachable
   and `pointer-events: none` keeps it from being clicked while invisible. */
.page-ln4-pos.is-hidden {
  opacity: 0;
  transform: translateY(-0.5rem);
  pointer-events: none;
}

.nav-middle {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--gap);
  color: var(--nav-ink);
  pointer-events: auto;
  transition: color var(--animation-default);
}

.page-ln4-svg {
  position: relative;
  display: block;
  width: 2.75rem;
  height: 2.75rem;
  color: currentcolor;
}

.page-ln4-svg svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* Two stacked copies of one glyph. The mint copy is clipped away and wipes up
   on hover — a reveal, not travel, so it stays legitimate under `reduce` at a
   shortened duration rather than being switched off. */
.page-ln4-svg .mark-fill {
  position: absolute;
  inset: 0;
  clip-path: inset(100% 0 0 0);
  transition: clip-path 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}

.nav-middle:hover .page-ln4-svg .mark-fill,
.nav-middle:focus-visible .page-ln4-svg .mark-fill {
  clip-path: inset(0 0 0 0);
}

/* ── the right-hand controls ────────────────────────────────────────────── */
.btn-layout {
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
  gap: var(--gap);
}

.btn-layout.is-nav {
  gap: calc(var(--gap) * 0.5);
  pointer-events: auto;
  /* The wordmark's opposite number, and physical for the same reason it is. */
  transform-origin: 100% 0;
}

/* ── the menu button ────────────────────────────────────────────────────── */
.nav-ham {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  aspect-ratio: 1;
  width: 3.75rem;
  height: 3.75rem;
  border-radius: 0.74rem;
  border: 2px solid var(--nav-ink);
  color: var(--nav-ink);
  overflow: hidden;
  /* THE INK RAMPS, IT DOES NOT SNAP. Measured on the reference:
     `background-color 0.28s, color 0.75s, border-color 0.75s`; the rebuild
     reported `all / 0s`, so its ring and glyph changed colour in one frame
     wherever the theme floor flipped. The 0.75s legs are `--animation-default`,
     the Golden Master's own `:root` pair, and the 0.28s is written out because
     it is this control's only use of that number. */
  transition:
    background-color 0.28s linear,
    color var(--animation-default),
    border-color var(--animation-default);
}

/* The faint wash the button wears on a dark backdrop — 2.7% white, measured as
   `rgba(237, 245, 243, 0.027)` on the reference at scroll 900 and absent here.
   It is what keeps the ring from reading as a floating outline once the page
   behind it goes to ink. */
[data-nav-theme='light'] .nav-ham {
  background-color: rgb(237 245 243 / 2.6%);
}

/* The lime wipe. `inset: -2px` so it covers the rim edge to edge and the button
   reads as one lime shape rather than a fill inside a border. */
.nav-ham::before {
  content: '';
  position: absolute;
  inset: -2px;
  background-color: var(--color--lime);
  clip-path: ellipse(120% 0% at 50% 0%);
  transition: clip-path 0.34s cubic-bezier(0.22, 0.61, 0.36, 1);
}

.nav-ham:hover::before,
.nav-ham:focus-visible::before {
  clip-path: ellipse(120% 132% at 50% 0%);
}

/* While the menu is open the button is a solid light tile over a dark overlay,
   so its ink is a property of the button, not of the page behind it. */
.nav-ham[aria-expanded='true'] {
  --nav-ink: var(--color--dark-green);

  background-color: var(--color--white);
}

.nav-ham-mark {
  position: absolute;
  inset: 0;
  display: block;
  pointer-events: none;
}

.nav-ham-mark svg {
  display: block;
  width: 100%;
  height: 100%;
}

.nav-ham-mark path {
  fill: none;
  stroke: currentcolor;
  stroke-width: 3.8;
  stroke-linecap: butt;
  stroke-linejoin: round;
}

/* THE SLASH DRAWS ITSELF, AND WITHOUT THIS IT ONLY FADED (P6-04).

   `ham-mark.js` animates `stroke-dashoffset` from 27.2 to 0 to stroke the `/`
   on as the glyph passes through `</>`, and it always has. `stroke-dashoffset`
   DOES NOTHING WITHOUT A DASH ARRAY — measured, the rebuild reported
   `stroke-dasharray: none` at every point of the morph while the reference
   reported `27.2px` — so the offset the runtime was writing had no effect and
   the slash arrived by opacity alone.

   27.2 IS THE SAME NUMBER AS `SLASH_DASH` in ham-mark.js. Two files have to
   agree on one measurement and the build has no way to emit it once, so each
   names the other; changing one without the other silently un-draws the stroke
   again, which is what this comment exists to prevent. */
.nav-ham-mark [data-ham-s] {
  stroke-dasharray: 27.2;
  stroke-dashoffset: 27.2;
  opacity: 0;
}

/* Two rings, because no single colour serves this control. Measured against the
   two backdrops it actually sits on — a light page while closed (#f2f7f3-#fafcfb)
   and the dark overlay while open (#040504):

       lime alone   1.36-1.43 closed / 13.82 open   -> fails closed
       dark alone  19.57      closed /  1.00 open   -> fails open
       white alone  1.02-1.08 closed / 18.43 open   -> fails closed

   So: a lime 2px core contiguous with the button's own lime fill, plus a dark
   4px halo that is crisp against the light backdrop. Measured on the shipped
   build: 19.57:1 closed, 13.82:1 open, against WCAG 2.1 SC 1.4.11's 3:1.
   Neither outline nor box-shadow participates in layout, so nothing moves. */
button.nav-ham:focus-visible {
  outline: 2px solid var(--color--lime);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px var(--color--dark-green);
}

/* ── the menu panel ─────────────────────────────────────────────────────────
   `display: none` is the settled state and the emitted state: `aria-expanded`
   on the button says `false`, and this says the same thing in CSS. Stage 6 owns
   the transition between the two; nothing in this phase opens it.

   `--z-menu` IS LOWER THAN `--z-nav-bar` ON PURPOSE. This element and
   `.nav-inner` are siblings inside `.nav`, and the bar — which carries the
   close button — has to be painted over the panel. See `tokens/layer.css`. */
.nav-menu-w {
  position: fixed;
  inset: 0;
  z-index: var(--z-menu);
  display: none;
  justify-content: center;
  align-items: center;
  width: 100vw;

  /* `dvh` HERE AND NOWHERE ELSE. This panel is opened with document scroll
     already locked (`scroll-controller.js` holds the lock while the menu is
     open), so the address bar cannot move while it is up — which removes the
     only reason to avoid a unit that tracks the bar, and leaves the reason to
     want one: the panel is an opaque cover, and a visitor who opens it after
     scrolling has a bar that is already RETRACTED. `svh` would then leave a
     strip of the page showing under an overlay that is supposed to be the whole
     screen. `--vh` is the fallback for a browser with no `dvh`. */
  height: calc(var(--vh, 1vh) * 100);
  height: 100dvh;
  background-color: var(--color--dark-green);
  color: var(--color--white);
  clip-path: ellipse(120% 100% at 50% 20%);
  pointer-events: auto;
  overflow: clip;
}

.nav-menu-inner {
  display: flex;
  height: 100%;
  padding: 1.25rem;
}

.nav-menu-inner.c {
  justify-content: center;
  align-items: center;
  max-height: 80rem;
  margin-block: auto;
  padding-block: 2.5rem 2rem;
}

.nav-menu-grid {
  display: flex;
  justify-content: space-between;
  width: 100%;
  height: 100%;
  gap: var(--gap);
  /* Physical, and correct on every document because the panel does not mirror
     (see the top of this file). The 8rem is breathing space beside the images,
     which are physically on the right in all three locales. */
  padding-left: 2.25rem;
  padding-right: 8rem;
}

.nav-menu-images-w {
  position: relative;
  display: flex;
  flex-flow: column;
  justify-content: flex-start;
  align-items: flex-start;
  max-height: 100%;
  height: calc(calc(var(--vh, 1vh) * 100) - var(--gap) * 2);
}

.nav-menu-images-track {
  display: flex;
  flex: none;
  justify-content: flex-start;
  align-items: center;
  gap: calc(var(--gap) * 3);
  height: 100%;
  max-height: 100%;
}

.nav-menu-images-col {
  display: flex;
  flex-flow: column;
  flex: none;
  justify-content: center;
  align-items: center;
  gap: calc(var(--gap) * 3);
  height: auto;
}

.nav-menu-img-w {
  position: relative;
  flex: none;
  width: 23rem;
  height: 25rem;
  border-radius: 3rem;
  clip-path: ellipse(120% 100% at 50% 20%);
  overflow: hidden;
}

/* Four layers, in one stacking context: the untreated photograph at the bottom,
   three blend layers over it, and a full-colour copy on top that a later stage
   crossfades in. `opacity: 0` on `.menu-img-top` is the SETTLED state, not a
   hidden start state — the treated stack is what the menu is meant to show. */
.menu-img-top {
  position: absolute;
  inset: 0;
  z-index: 10;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
}

.menu-img-blends-w {
  position: relative;
  inset: 0;
  z-index: 5;
  width: 100%;
  height: 100%;
}

.menu-img-blend-1 {
  position: absolute;
  inset: 0;
  background-color: #000;
  mix-blend-mode: saturation;
}

.menu-img-blend-2 {
  position: absolute;
  inset: 0;
  background-color: #040504;
  mix-blend-mode: plus-lighter;
}

.menu-img-blend-3 {
  position: absolute;
  inset: 0;
  background-color: #2a2c2c;
  opacity: 0.4;
}

.menu-img-no-blend {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.nav-menu-links-w {
  display: flex;
  flex-flow: column;
  justify-content: space-between;
  align-items: center;
  text-align: center;
}

.nav-menu-spacer {
  height: 1.25rem;
}

.nav-menu-links-col {
  display: flex;
  flex-flow: column;
  justify-content: flex-start;
  align-items: center;
  gap: calc(var(--gap) * 2);
  width: 100%;
}

.nav-menu-links-list {
  display: flex;
  flex-flow: column;
  justify-content: flex-start;
  align-items: center;
  color: var(--color--green-off-white-1);
}

.text-nav-link {
  display: block;
  font-family: var(--font-body);
  font-size: var(--fs-display);
  font-weight: 500;
  line-height: 0.94;
  text-transform: uppercase;
  /* THE MENU LINK'S TWIN. See `components/button.css` for what a twin is and why
     the roll is empty without one. This one is a LITERAL `5.25rem` rather than
     the em the buttons use, and the two numbers have to be the same literal:
     `text-hover.js` rolls a menu link by `NAV_LINK_ROLL = '-5.25rem'`, and this
     face is `line-height: 0.94` on `--fs-display`, so an em would land the
     shadow 6% short of where the letters go. Measured on the reference:
     `0px 70px` at a 13.3333px root, which is exactly 5.25rem.

     Colour omitted, exactly as the Golden Master writes it, so the twin is
     `currentColor` — it inherits the hover tint from `.nav-menu-link-w:hover`
     below and arrives already the right colour rather than changing after it
     lands. That is why this declaration belongs in the same rule as the
     `color` transition and not in a block of its own. */
  text-shadow: 0 5.25rem;
  transition: color 0.6s cubic-bezier(0.19, 1, 0.22, 1);
}

.nav-menu-link-w {
  position: relative;
  cursor: pointer;
  overflow: clip;
}

.nav-menu-link-w:hover {
  color: var(--color--lime-off);
}

/* The page you are on. `is-current` is authored in the markup beside
   `aria-current="page"`; the Golden Master reads `w--current`, a class
   maintained by a THIRD-PARTY module, which is an undeclared cross-vendor
   dependency on a class name (INV-05). */
.nav-menu-link-w.is-current {
  color: var(--color--green-off-white-2);
  pointer-events: none;
  cursor: auto;
}

.nav-helmet-rive-w {
  position: relative;
  display: flex;
  flex-flow: column;
  gap: var(--gap);
  color: var(--color--green-off-white-2);
}

.nav-menu-mark {
  position: relative;
  display: block;
  width: 3.5rem;
  height: 3.5rem;
  margin-inline: auto;
}

.nav-menu-mark svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.nav-menu-social-w {
  display: flex;
  flex-flow: column;
  justify-content: flex-start;
  align-items: center;
  gap: var(--padding--small);
}

/* ── the language switcher ──────────────────────────────────────────────── */
.nav-lang-label {
  color: var(--color--green-off-white-2);
}

.nav-lang-label .btn-text {
  font-weight: 700;
  letter-spacing: 0.06em;
}

html[lang='ar'] .nav-lang-label .btn-text {
  letter-spacing: 0;
}

/* EN DE AR, in that order, on every document — the panel does not mirror. */
.nav-lang-links {
  display: flex;
  gap: calc(var(--gap) * 1.5);
}

.nav-lang-w .text-link-w[aria-current] {
  pointer-events: none;
  cursor: default;
}

.nav-lang-w .is-lang-active .btn-text {
  color: var(--color--lime);
}

.nav-lang-w .text-link-w:focus-visible {
  outline: 2px solid var(--color--lime);
  outline-offset: 3px;
  border-radius: 0.25rem;
}

/* ── responsive ─────────────────────────────────────────────────────────── */
@media screen and (max-width: 991px) {
  /* Below the desktop branch the wordmark goes and the controls take the bar. */
  .nav-brand-link {
    display: none;
  }

  .btn-layout.is-nav {
    justify-content: space-between;
    width: 100%;
  }

  .nav-menu-grid {
    padding-inline: 0;
  }

  .nav-menu-links-w {
    margin-inline: auto;
  }

  .nav-menu-images-w {
    display: none;
  }

  .nav-menu-mark {
    width: 2.75rem;
    height: 2.75rem;
  }
}

@media screen and (max-width: 479px) {
  .btn-layout {
    flex-flow: wrap;
  }

  .nav-ham {
    width: 4.7rem;
    height: 4.7rem;
    pointer-events: auto;
  }

  .nav-menu-w {
    height: 100svh;
    height: 100dvh;
  }

  .nav-lang-links {
    gap: var(--gap);
  }
}

@media (prefers-reduced-motion: reduce) {
  .nav-ham,
  .nav-ham::before,
  .page-ln4-svg .mark-fill,
  .nav-brand path,
  .nav-middle,
  .text-nav-link {
    transition-duration: 0.01s;
  }
}
}

/* ---- components · components/text-reveal.css ---- */
@layer components {
/* TEXT REVEAL — the appearance of everything the motion layer injects.

   Three effects build DOM at runtime: the line reveal appends a bar into each
   `.line`, and the oval reveal wraps lines in clip boxes inside one clipped
   parent. Their APPEARANCE belongs here rather than in a JavaScript style
   string. On the Golden Master these same nine declarations are written as
   `el.style.position = "absolute"; el.style.top = "0"; …` inside `AE()`, which
   is how a stylesheet ends up living in a bundle nobody can read.

   What stays inline, and why: the bar's `background-color` and
   `transform-origin` come from that element's own `data-anim-high` value, so
   they are per-instance DATA rather than styling. Six colours and two origins
   would otherwise become fourteen modifier classes describing one attribute.

   NONE OF THIS PAINTS UNTIL JAVASCRIPT BUILDS IT. Every selector below matches
   only nodes the motion layer created, so with the runtime absent this file
   contributes nothing and the page is its settled self — which is the property
   `src/runtime/animation/motion.js` exists to protect. */

/* THE LINE BOX ITSELF.

   SplitText 3.13 sets `display` on the wrappers it builds ONLY when the tag is
   not a span:

       if (tag !== "span") { el.style.position = "relative"; el.style.display = display }

   Every split on this site uses spans, so nothing sets it and a `.line` stays
   `display: inline`. Inline boxes do not stack — measured on the candidate
   before this rule existed, five line boxes overlapped inside a 101 px
   paragraph, three of them reporting 19 px against the 20.209 px line-height,
   because they were fragments of one inline flow rather than five blocks.

   The Golden Master supplies the missing half in an inline <style> block, and
   this is that rule, verbatim:

       [data-anim-high] .line { width: auto !important; display: inline-block !important; }

   `inline-block` rather than `block` because the reveal's clip-path must be
   measured against the TEXT, not against the column: a block line box is as
   wide as its container, so `inset(0 100% 0 0)` would wipe from the far right
   edge of the paragraph rather than from the end of the words. `width: auto`
   is the same decision said twice, and both carry `!important` because they
   have to beat the inline `display` SplitText writes for any non-span tag
   (INV-10: the constraint is named).

   NOT REPRODUCED, deliberately: the Golden Master's sibling rule

       [split-text]:not[data-oval-scroll] .line { clip-path: polygon(...); white-space: nowrap }

   is a PARSE ERROR — `:not[...]` without parentheses — so every browser
   discards it, and the site has never rendered with that mask. Repairing the
   selector would switch a clip-path on sitewide for the first time. The effect
   is reproduced (no mask), not the typo (motion-architecture 7.3). */
[data-anim-high] .line {
  /* Both !important: they must beat the inline `display` SplitText writes for a
     non-span tag, and an inline style outranks any normal declaration. */
  width: auto !important;
  display: inline-block !important;
}

/* The wiper. One per `.line`, absolutely positioned inside a line the reveal has
   already made `position: relative`, and animated from `scaleX: 1` to `0`. */
.high-line-reveal {
  position: absolute;
  inset: 0;
  z-index: 5;

  /* The bar is decoration behind a state change, never a target. Without this a
     stray pointer event lands on the wiper rather than on the link it covers
     during the 0.6s it is on screen. */
  pointer-events: none;
}

/* The oval reveal's per-line clip box. It gets its `clip-path` from the
   timeline; what it needs from CSS is to be a block that wraps its line exactly,
   so the ellipse is measured against the text and not against the column. */
.oval-line-clip-wrap {
  display: block;
}

/* The one parent all the clip boxes are moved into.

   `overflow: clip` rather than `hidden`: `hidden` makes the element a scroll
   container, so a focus move inside it can scroll the box and leave the heading
   visibly offset with no scrollbar to explain why. `clip` clips without that
   side effect, which is what the Golden Master's inline style also asks for. */
.oval-outer-wrapper {
  display: block;
  overflow: clip;
}

/* SplitText's generated wrappers.

   The plugin sets `display: inline-block` on words and characters inline, and
   `display: block` on lines, so their layout is already correct. What it does
   NOT set is the transform hint, and the character roll animates `y` on up to
   215 elements at once. Declaring `will-change` here permanently would hold 215
   compositor layers for the life of the page; it is therefore scoped to the
   host being hovered, so the promotion exists for the duration of the roll and
   not a moment longer.

   BOTH UNITS, because the roll has two. A joined script is never split into
   characters — a per-letter box stops the shaper joining them — so on `/ar/`
   `animation/text-hover.js` rolls WORDS and there is no `.char` under the host
   to promote. One selector covers both rather than two rules that could drift. */
[data-anim='text-hover']:hover :is(.char, .word) {
  will-change: transform;
}

/* REDUCED MOTION.

   The reveal systems are not installed at all under `reduce` — the decision is
   made in `src/runtime/animation/index.js`, before anything is hidden, which is
   stronger than overriding a hidden state after the fact. These two rules exist
   for the window between a preference changing and the teardown completing, and
   for the case where a `clip-path` was written by a timeline that is now
   stopped. `!important` is required because both properties are set as inline
   styles by GSAP, and an inline style outranks any normal declaration
   (INV-10: the constraint is named, not assumed). */
@media (prefers-reduced-motion: reduce) {
  .line,
  .oval-line-clip-wrap {
    /* !important because GSAP writes clip-path as an INLINE style, and the user
       preference has to outrank a stopped timeline's last frame. */
    clip-path: none !important;
  }

  .high-line-reveal {
    /* Same reason: the wiper's transform is inline, so hiding it needs to win. */
    display: none !important;
  }
}

/* PRESERVE THE AUTHORED LINE BREAK — ON THE THREE SOURCES THAT HAVE ONE.

   The Golden Master carries `[split-text] { white-space: pre-line }` in an inline
   <style> block with the note "Preserves line breaks", and it is load-bearing:
   without it a trailing `<br>` is collapsed, SplitText folds the heading into ONE
   line against the reference's TWO, and the reveal loses a wiper and 0.15s.

   IT ALSO MAKES EVERY NEWLINE IN THE SOURCE A RENDERED BREAK, which is safe on a
   minified donor and is NOT safe here: this markup is pretty-printed, so Prettier
   decides where the breaks fall. The previous version of this rule applied to all
   54 sources with exactly that hazard written down and measured at 991, 992 and
   1440 — where it happened to hold, because Prettier's wrap points fell where the
   text would wrap anyway.

   THE HAZARD IS REAL AND THE MEASURED NOTE IS NOT A GUARANTEE, so the rule is
   SCOPED to the two sources that actually carry an authored `<br>`
   (`data-split-keep-breaks`) and every other source wraps naturally, as the
   reference does. A copy edit can no longer move a line break by accident, which
   is a better property than a note saying it has not yet.

   IT WAS NOT, HOWEVER, THE CAUSE OF THE 1920 DIFFERENCE — recorded because that
   is what this scoping was first reached for. That difference was the
   certificate's lede taking a third line at 1920 against the reference's two, at
   the natural wrap, because `font-weight: 500` here is an inert
   `font-variation-settings` there. It was `R9-body-lede-weight-1920`, retired in
   Phase 6 once P6-11 replaced the text it was measured on — the new lede takes
   four lines at both weights. Scoping this rule changed no line count at 991,
   992, 1440 or 1920 then and changes none now. It is kept on its own merits. */
[split-text][data-split-keep-breaks] {
  white-space: pre-line;
}

/* THE SPLIT'S BOX MODEL, AND IT IS WHY NOTHING ROLLED (P6-02).

   `transform` DOES NOT APPLY TO A NON-REPLACED INLINE ELEMENT. Not "resolves to
   zero" — CSS Transforms L1 makes only block-level and atomic-inline-level
   elements transformable, so the property is dropped on the floor. Chrome still
   reports it from `getComputedStyle`, which is what made this invisible for
   three phases: every char on the page carried `matrix(1, 0, 0, 1, 0, -70)` and
   not one of them had moved a pixel.

   MEASURED in the built rebuild at 1440, hovering and reading the char's
   `getBoundingClientRect().top` before and after — the only honest instrument
   here, because the matrix lies:

                        legacy    rebuild    rebuild + these two rules
     .btn-w             -24.00      0.00       -20.00
     .nav-menu-link-w   -70.00      0.00       -70.00
     .text-link-w       -15.00      0.00       -15.00

   Both rules come from the Golden Master's `.css-split-type` embed
   (`site/partials/css-main.html`), which this project never ported. `.word` is
   inline-block AND `nowrap` because an inline-block char is a legal break
   opportunity; the word wrapper is what keeps a word atomic. Measured: per-source
   `.line` counts are identical with and without these rules at 991, 992, 1440
   and 1920, so the break opportunity never materialises — every split source
   here ships `words`, so the wrapper is always there.

   Scoped on `[split-text]`, exactly as the Golden Master scopes it, so a
   line-only split and any future non-split `.char` are untouched. */
[split-text] .word {
  display: inline-block;
  white-space: nowrap;
}

/* WHY A TWO-TONE HEADING WRAPS BOTH HALVES, NOT JUST THE COLOURED ONE.
   (Not a rule — a note, kept beside the box model that causes it.)

   A section title is `<span>five courses</span> <span class="c-lime-off">one
   credential</span>`, and the first span looks redundant. It is not. SplitText
   decides where a line ends by walking the source's direct children and asking
   whether each one is LOWER than the last and no further along the inline axis
   (`src/vendor/gsap/SplitText.js`). The words it generates are `inline-block`,
   by the rule above; an authored `<span>` left among them is plain `inline`, so
   the two have different box heights, and at Arabic's `line-height: 1.45` the
   inline box's top sits BELOW the taller inline-block's. In an RTL document the
   accent span is also further left. Both halves of the test are then true and
   SplitText reports a line break that the layout does not have.

   Measured on `/ar/`: four of the five section titles split into two `.line`
   elements sitting side by side on one visual line, so the wipe crossed the
   heading in two staggered halves. `identities with presence`, the one heading
   with no nested span, was the one that was right.

   Wrapping BOTH halves makes the two children the same kind of box, so their
   tops agree and the comparison cannot fire. Latin never showed this because
   `line-height: 0.886` puts the inline-block's top BELOW the inline's, which
   fails the same test from the other side. */

[split-text] .char {
  display: inline-block;
}

/* THE ACCESSIBLE TWIN IS NOT COPY.

   `animation/split-registry.js` puts a `.visually-hidden` span carrying the
   intact sentence beside every split source, because the source itself is
   `aria-hidden` once SplitText has taken it apart. That span is CLIPPED, not
   removed — which is exactly what makes it readable to assistive technology —
   and clipped text is still SELECTABLE. So a drag across a section heading
   selected the twin and the source both, and pasting gave the heading twice.
   Reported by the owner, with the paste to prove it.

   `user-select` is a SELECTION property and has no effect on the accessibility
   tree: VoiceOver, NVDA and JAWS still read this span. The visible source
   carries no `user-select` of its own, so a reader still copies the heading —
   once.

   SCOPED TO THE INJECTED NODE, NEVER TO `.visually-hidden`. That utility is
   used by twenty-odd AUTHORED spans — the page's only `<h1>`, the certificate
   cards' English course titles, ten project names, every "opens in a new tab"
   note — and widening this rule to the class would quietly make all of them
   uncopyable to fix a bug in one of them. It lives here rather than in
   `utilities/visually-hidden.css` for the same reason this file's header gives:
   every selector in it matches only nodes the motion layer created. */
[data-split-text-copy] {
  user-select: none;
}
}

/* ---- components · components/orientation-notice.css ---- */
@layer components {
/* THE LANDSCAPE INTERSTITIAL.

   The panel's appearance is the Golden Master's, read from its own stylesheet:

     .mob-landscape-block { z-index: 99999; background: var(--color--lime);
                            color: var(--color--black); text-align: center;
                            gap: calc(var(--gap) * 3); flex-flow: column;
                            justify-content: center; align-items: center;
                            display: none; position: fixed; inset: 0% }

   What is NOT the Golden Master's is the trigger. There, `display: none` is the
   only rule and JavaScript is the only thing that ever changes it — so the
   panel is invisible to a visitor whose script failed, and unremovable by a
   stylesheet for one whose script ran. Here the media query below IS the
   mechanism, and the runtime only does what CSS cannot: focus, inert, and
   keeping this panel and the menu from both owning the screen.

   THE QUERY, AND WHY IT IS NOT JUST `orientation: landscape`.

   A desktop window is landscape. So is a tablet. Neither wants this panel, and
   a bare orientation query would show it to both — which is why the height
   bound is doing the real work: a viewport short enough that a vertical
   composition has nowhere to go. 30rem tracks the root ladder rather than
   fixing a pixel height, so the threshold moves with the type scale it exists
   to protect.

   `pointer-events` is inherited from the default rather than declared: when the
   panel is `display: none` there is nothing to hit, and when it is shown it
   covers everything. */
.mob-landscape-block {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);

  display: none;
  flex-flow: column;
  align-items: center;
  justify-content: center;
  gap: calc(var(--gap) * 3);

  background-color: var(--color--lime);
  color: var(--color--black);
  text-align: center;
}

/* ── the mark ─────────────────────────────────────────────────────────────
   ONE IDEA, THREE MOVING PARTS, AND THE COUNTER-ROTATION IS THE POINT (P8-17).

   The device turns from landscape to portrait; the owner's mark inside it holds
   still against that turn; the arc brightens only while the turn is happening.
   That third part is what keeps the RESTING frame a phone standing up rather
   than a diagram of a rotation — the drawing is at its clearest exactly when it
   has finished saying something.

   ONE CLOCK FOR ALL THREE. `--olm-turn: 5s`, and every keyframe list below is
   cut on the same four beats, so the mark, the frame and the arc cannot drift
   out of phase the way three separately-tuned durations would:

       0-12%    lying down. The arc draws itself in, leading the turn.
       12-32%   the turn. The device comes up, the mark holds still against it.
       32-82%   THE POSE — half the loop, a phone standing up, held long enough
                to be read rather than watched.
       82-100%  back down, eased, so the loop closes instead of snapping.

   `transform-box: fill-box` + `transform-origin: center` on both rotating groups
   rather than a `transform-origin` in user units: the two groups have different
   bounding boxes and the same instruction, and a pair of hard-coded coordinates
   is two more numbers to keep in step with a `viewBox` nobody expects to
   change. */
.mob-landscape-mark {
  --olm-turn: 5s;

  /* SIZED FROM THE VIEWPORT'S HEIGHT, NOT FROM THE ROOT UNIT. This panel only
     ever shows on a SHORT screen, and `1rem` here is `--fluid-font` — derived
     from viewport WIDTH, floored at 11px, so a phone held sideways reports a
     wide viewport and a small rem at the same time. A rem-sized mark therefore
     shrank exactly where there was room for it: 10rem measured 85px on a
     844x390 screen with 390px of height to spend. The Golden Master sizes its
     own version `height: 40vh`, and it is right to. The `rem` half of the `min`
     is the ceiling for a tablet-shaped viewport that slips past the query. */
  aspect-ratio: 1;
  width: auto;
  height: min(42vh, 14rem);
  color: var(--color--black);
}

.mob-landscape-mark svg {
  display: block;
  width: 100%;
  height: 100%;
}

.olm-device,
.olm-glyph {
  transform-box: fill-box;
  transform-origin: center;
}

/* The device starts on its side — which is how the visitor is holding it — and
   comes upright. `-90deg` rather than `90deg` so the turn runs anticlockwise,
   the same direction the arc's head points. */
.olm-device {
  animation: olm-stand var(--olm-turn) cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

/* The mark does the exact opposite, so it never leaves the upright. Its own
   `transform-origin` is its own centre, not the device's, which is why it turns
   in place rather than orbiting. */
.olm-glyph {
  animation: olm-hold var(--olm-turn) cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

/* The arc is drawn on and off rather than faded, so it arrives leading with its
   head instead of appearing all at once.

   `pathLength="1"` ON THE PATH is what makes that one number instead of three.
   It re-declares the path's length as 1 for every dash calculation, so the dash
   array and the offset are FRACTIONS and neither has to know the real arc length
   — which is 93.4 units today and would change the moment anyone touched the
   radius or the endpoints. The disciplines hero draws its silhouettes the same
   way for the same reason. The head follows a beat behind on plain opacity,
   because a three-point triangle has no length to draw. */
.olm-arc {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: olm-draw var(--olm-turn) cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

.olm-head {
  opacity: 0;
  animation: olm-point var(--olm-turn) linear infinite;
}

/* The home bar is the one part that does not move. It is what makes the rounded
   rectangle read as a device rather than a window, and it is drawn at 0.55 alpha
   so it sits behind the mark in the hierarchy rather than beside it. */
.olm-bar {
  opacity: 0.55;
}

@keyframes olm-stand {
  0%,
  12% {
    transform: rotate(-90deg);
  }
  32%,
  82% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(-90deg);
  }
}

@keyframes olm-hold {
  0%,
  12% {
    transform: rotate(90deg);
  }
  32%,
  82% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(90deg);
  }
}

@keyframes olm-draw {
  0% {
    stroke-dashoffset: 1;
  }
  12%,
  82% {
    stroke-dashoffset: 0;
  }
  100% {
    stroke-dashoffset: 1;
  }
}

@keyframes olm-point {
  0%,
  8% {
    opacity: 0;
  }
  18%,
  80% {
    opacity: 1;
  }
  92%,
  100% {
    opacity: 0;
  }
}

/* THE BACKSTOP. This is the whole system for a visitor with no JavaScript, and
   the whole system for a visitor with it — the runtime never writes `display`.

   `pointer: coarse` keeps a desktop browser resized to an odd shape out of it:
   the panel is for a device being HELD sideways, not for a short window. */
@media (orientation: landscape) and (max-height: 30rem) and (pointer: coarse) {
  .mob-landscape-block {
    display: flex;
  }

  /* Nothing behind the panel may scroll while it is up. `overflow: clip` rather
     than `hidden` for the same reason the scroll lock uses it: `hidden` makes
     the element a scroll container, so a focus move inside it can shift the
     page underneath and leave the visitor somewhere else when they rotate back.
     `!important` because the scroll lock's own attribute rule and this one can
     both be in force, and the interstitial must win — it is showing precisely
     because there is nowhere to scroll to (INV-10: the constraint is named). */
  html:has(.mob-landscape-block) {
    overflow: clip !important;
  }
}

/* The mark takes less of the shortest viewports, where the two lines of type are
   what has to survive. `vh` already scales it; this changes the SHARE. */
@media (orientation: landscape) and (max-height: 22rem) and (pointer: coarse) {
  .mob-landscape-mark {
    height: 34vh;
  }
}

/* REDUCED MOTION: THE SETTLED POSE, WHICH IS ALREADY THE MESSAGE.

   `base/reset.css` kills animations site-wide in the first cascade layer, so
   these three would stop wherever their first frame put them — and their first
   frame is the device lying down with no arc, which is the one pose that says
   nothing. This parks all three at their finished state instead: the phone
   upright, the mark upright with it, the arc fully drawn and its head lit. That
   is `end-state` in the motion vocabulary — the information is a destination,
   not a journey, so a reader who asked for less motion loses the gesture and
   keeps the meaning. */
@media (prefers-reduced-motion: reduce) {
  .olm-device,
  .olm-glyph {
    transform: rotate(0deg);
  }

  .olm-arc {
    stroke-dashoffset: 0;
  }

  .olm-head {
    opacity: 1;
  }
}
}

/* ---- components · components/button.css ---- */
@layer components {
/* ============================================================================
   THE BUTTON SYSTEM. In-degree 6 in the dependency graph — six homepage buttons
   plus the nav "Connect" — so it is substrate and is frozen before the sections
   that call it are authored.

   ONE STRUCTURAL FIX travels with it. Every `.btn-w` on the Golden Master nests
   `.btn-icon-w` INSIDE `.btn-inner`, except the projects-ring CTA, where a
   surplus `</div>` makes it a sibling. The rebuild nests it everywhere (defect
   L4), so the gap between label and icon comes from one rule rather than from
   two that happen to agree.

   The icon slot holds BOTH marks: the inline SVG arrow and, since stage 15, the
   `btn-ui.riv` canvas that animates on hover. The SVG is the floor and the
   canvas retires it only once a real artboard is mounted — see
   `components/btn-arrow.html`. The two are the same size in the same slot, so
   the swap moves no box.
   ========================================================================== */

.btn-w {
  display: flex;
  flex: none;
  justify-content: center;
  align-items: center;
  height: 3rem;
  padding-inline: 1rem;
  border: 1px solid var(--color--lime);
  border-radius: 0.54rem;
  background-color: var(--color--lime);
  color: var(--color--dark-green);
}

.btn-w.is-nav {
  height: 3.75rem;
}

.btn-w.icon-only {
  aspect-ratio: 1;
  width: 3.75rem;
  height: 3.75rem;
  padding-inline: 0;
}

/* An outline button on whatever ink it inherits. `--color--lime-zero` is the
   accent at zero alpha rather than `transparent`, so a transition between the
   two states interpolates through the accent instead of through grey. */
.btn-w.tertiary {
  height: 2.5rem;
  border-color: currentcolor;
  border-radius: 0.411875rem;
  background-color: var(--color--lime-zero);
  color: currentcolor;
}

.btn-inner {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: calc(var(--gap) * 0.5);
}

.btn-inner-text-w {
  overflow: clip;
}

/* `display: block` for the same reason `.text-clip-w` has it: an inline label
   leaves its containing box generating a line box from the INHERITED
   line-height, so a 15px label produced a 26px row. As a block, the label's own
   font-size and line-height are the box. The Golden Master uses `<div>` here. */
.btn-text {
  display: block;
  font-family: var(--font-label);
  font-size: var(--text--btn-primary, var(--fs-sm));
  font-weight: 400;
  line-height: 1;
  text-transform: uppercase;
}

.btn-text.is-nav {
  font-size: var(--text--btn-nav, var(--fs-lead));
}

.btn-text.is-footer {
  font-size: var(--text--btn-footer, var(--fs-h3));
}

/* THE TWIN — the half of the roll that a reader of the JavaScript cannot see.

   `text-hover.js` rolls the characters UP by their own height. On its own that
   empties the box: the letters leave through the top of an `overflow: clip`
   window and nothing arrives. What arrives is a `text-shadow` sitting exactly
   ONE LINE BOX BELOW the text, so as the real letters leave the top the shadow
   copy climbs into the same place and the word appears to turn over.

   The Golden Master carries it in an inline `<style>` this project never ported
   (`site/partials/css-main.html`, the `.css-split-type` embed). Without it the
   effect ran, moved nothing (see `components/text-reveal.css` for why), and
   would have rolled the text away into an empty box once it did.

   `--text-offset` KEEPS ITS NAME rather than collapsing to `1em`. The two are
   numerically identical here — measured `0px 20.0005px` on a 20.0005px face and
   `0px 26px` on a 26px face — but `--text-offset` is the property the Arabic
   sheet overrides, and this project already carries `html[lang='ar']` hooks in
   `site-nav.css` and `tokens/type.css`. Hard-coding the em would remove a hook
   the next locale needs.

   The `[split-text]` qualifier is required, not decorative: an UNSPLIT
   `.btn-text` — the language switcher's label is one — must not paint a ghost
   under itself with nothing to roll it away. */
.btn-text {
  --text-offset: var(--text--btn-primary, var(--fs-sm));
}

.btn-text.is-nav {
  --text-offset: var(--text--btn-nav, var(--fs-lead));
}

.btn-text.is-footer {
  --text-offset: var(--text--btn-footer, var(--fs-h3));
}

[split-text].btn-text {
  text-shadow: 0 var(--text-offset) currentColor;
}

.btn-icon-w {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  aspect-ratio: 1;
  width: 1.2rem;
}

.btn-icon-w svg,
.btn-icon-w .btn-rive {
  display: block;
  width: 0.78125rem;
  height: 0.78125rem;
}

.btn-icon-w.is-large svg,
.btn-icon-w.is-large .btn-rive {
  width: 1rem;
  height: 1rem;
}

/* ── the two marks in one slot ─────────────────────────────────────────────
   The SVG is the FLOOR and the canvas is the enhancement. Both are laid out at
   the same size in the same centred slot; the canvas is stacked over the SVG so
   that whichever is showing occupies the identical box, and `.btn-icon-w`'s
   `position: relative` above is what it stacks against.

   The canvas ships with `width="0" height="0"` attributes on purpose: an unsized
   canvas is 300 x 150 by specification, and one that never mounts — because the
   WASM was blocked, or the visitor never scrolled far enough for the lazy gate
   to open — would hold a 45,000-pixel buffer for a glyph it never draws.
   `RiveInstance.resize()` sets the backing store from this slot's own box at
   mount and back to zero on dispose.

   `data-rive-live` is written by `runtime/rive` on the SLOT, and only after an
   artboard has actually mounted — not when the module loads, not when the fetch
   starts. So every way this can go wrong (no JavaScript, blocked WASM, a 404, a
   contract that does not match the binary, a visitor who never scrolls far
   enough for the lazy gate to open) leaves the authored arrow in place. The
   Golden Master has no floor at all here: its slot holds the canvas and nothing
   else, so a Rive failure is an empty button. */
.btn-icon-w .btn-rive {
  position: absolute;
  pointer-events: none;
}

.btn-icon-w[data-rive-live] > .btn-arrow {
  display: none;
}

/* ── the arrow ────────────────────────────────────────────────────────────
   On the Golden Master this mark is ONLY a Rive state machine (`btn-ui.riv`,
   artboard `arrow`) painted into a `<canvas>`. Here the artboard's SETTLED shape
   is also drawn as inline SVG — a hooked arrow that turns back on itself and
   leaves up-and-right — because a button whose glyph depends on 611 KB of WASM
   arriving is a button that can be empty.

   The geometry in `components/btn-arrow.html` is TRACED from an 8x
   device-pixel capture of the shipped button — it is a trace, not the source,
   and the Rive artboard remains the authority. Stage 15 landed and the artboard
   now paints over it in the same slot; the trace stayed, as the floor.

   The hook is a stroke and the head is a SOLID triangle, because that is how
   the artboard draws it: a stroked chevron reads noticeably lighter beside the
   uppercase label at 12 px. One file owns the shape so that four sections
   cannot ship four slightly different arrows — which they did, before this
   component existed. */
.btn-arrow-hook {
  fill: none;
  stroke: currentcolor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.btn-arrow-head {
  fill: currentcolor;
  stroke: none;
}

/* The arrow turns a quarter-turn on a button that points DOWN the page rather
   than along it. A modifier on the button, so a section never reaches inside the
   component to rotate its glyph. */
.btn-w[data-btn-icon-rotate='true'] .btn-icon-w svg {
  transform: rotate(90deg);
}

.btn-auto-top {
  margin-top: auto;
}

/* A text link that rolls its label. The clip is the frame the roll happens
   inside; the roll itself is a later stage, and the clipped box is correct
   standing still.

   `display: block` is load-bearing and was measured. As an inline box the clip
   frame leaves its parent anchor generating its OWN line box, whose strut comes
   from the inherited body line-height — so a 15px label sat in a 26px row and
   every link list on the page was 11px too tall. As a block box the anchor has
   no strut and its height is the label's. The Golden Master gets this for free
   by using `<div>` here; this says the same thing in CSS, where a reader can see
   why it matters. */
.text-clip-w {
  display: block;
  clip-path: polygon(0 -2%, 0 94%, 100% 94%, 100% -2%);
}

.text-link-w.c-lime {
  color: var(--color--lime);
}

@media screen and (max-width: 479px) {
  .btn-w.is-nav {
    height: 4.7rem;
  }

  .btn-w.icon-only {
    width: 3.3rem;
    height: 3.3rem;
  }

  .btn-icon-w {
    width: 1.5rem;
  }
}
}

/* ---- components · components/site-footer.css ---- */
@layer components {
/* ============================================================================
   THE SITE FOOTER (`footer-`, `ftr-`).

   The band is masked into a shape rather than being a rectangle: an SVG mask on
   `.footer-layout-clip`, with a different mask below 480px because the aspect
   ratio changes from landscape to portrait and one mask cannot serve both.
   ========================================================================== */

.s.is-footer {
  z-index: var(--z-base);
  color: var(--color--dark-green);
}

/* THE DECORATION IS CLIPPED HERE, AND THE PLACEMENT IS WORTH 24 PIXELS AND A
   MARGIN COLLAPSE.

   The background portrait sits 40px below its wrapper and overflows the band by
   24px. On the Golden Master that overflow is absorbed by `div.page-w`, a
   wrapper with `overflow: clip` around the whole page — which is why its
   `documentElement.scrollHeight` equals its body height exactly. This rebuild
   has no such wrapper: the footer is a `<footer>` landmark and a sibling of
   `<main>` (phase 2, decision 6), which is better markup and left the overflow
   with nowhere to go. Measured: every box from the image upward matched the
   reference to a tenth of a pixel and the document was still 24px longer.

   IT IS ON `.c.is-footer`, NOT ON THE SECTION. Putting it on `.s.is-footer`
   works at 1440 and costs 9px at 390: `overflow` other than `visible`
   establishes a block formatting context, which stops a child's margin
   collapsing through the section — a layout change made accidentally while
   fixing a paint problem. `.c.is-footer` has the same bottom edge (18,709.3 at
   1440, so the same clip line) and already sits in its own context, so the clip
   costs nothing.

   Clipping at the IMAGE's own wrapper would be tighter still — and wrong: its
   bottom is 16px above the section's, so it would hide a sliver the Golden
   Master shows.

   `clip` rather than `hidden`, as everywhere in this project: `hidden` would
   make the footer a scroll container, and a focus move inside it could scroll
   the band independently of the page. */
.c.is-footer {
  overflow: clip;
}

/* The footer band is the LAST thing on every page, so its colour has to be the
   page's own rather than a second, nearly-identical white. A page that picks a
   different paper publishes it as `--page-bg`; the footer inherits it, so the
   seam at the top of the band cannot exist by construction. */
[data-footer-theme='white'] {
  background-color: var(--page-bg, var(--color--white));
}

[data-footer-theme='black'] {
  background-color: var(--color--black);
}

[data-footer-theme='green'] {
  background-color: var(--color--dark-green);
}

/* THE LAST ROW ON THE SITE CLEARS THE PHONE'S BOTTOM BAR. `viewport-fit=cover`
   (`src/layouts/base.html`) buys the page the whole screen and hands back the
   obligation to keep content out of the strip the browser draws over — and
   nothing on this site was reserving it: `--sa-b` is declared once in
   `tokens/motion.css` and had exactly one consumer, the hero's corner board. The
   copyright line and the two legal links are the bottom-most content in the
   document, so this is where the inset is owed. It is `0px` on every device that
   has no such bar, which is what the token's own fallback guarantees. */
.c.is-footer {
  position: relative;
  z-index: var(--z-section);
  padding-block: var(--gap) calc(var(--gap) + var(--sa-b));
}

.footer-layout-w {
  position: relative;
}

.footer-layout-clip {
  --mask-url: url('/assets/svg/footer/clip-desktop.svg');

  position: relative;
  width: 100%;
  aspect-ratio: 1688 / 896;
  background-color: var(--color--dark-green);
  color: var(--color--white);
  mask-image: var(--mask-url);
  mask-size: cover;
  mask-repeat: no-repeat;
  mask-position: center;
}

.footer-inner-layout {
  display: flex;
  flex-flow: column;
  height: 100%;
}

/* ── the two link columns ───────────────────────────────────────────────── */
.footer-links-w {
  position: relative;
  display: flex;
  justify-content: space-between;
  width: 100%;
  margin-top: auto;
  margin-bottom: 14rem;
  padding-inline: 12.8rem;
}

.footer-links-col {
  display: flex;
  flex-flow: column;
  justify-content: flex-start;
  align-items: center;
}

.footer-links-layout {
  display: flex;
  flex-flow: column;
  justify-content: flex-start;
  align-items: center;
  gap: calc(var(--gap) * 0.2);
  margin-top: 1rem;
  margin-bottom: 2rem;
}

/* One hover system for every footer link, declared once at the component root.
   The colour is a confirmation, never the only signal — `:focus-visible` and
   `aria-current` still work with colour removed. #21f1a8 on the footer's
   #040504 panel measures 13.8:1. */
.s.is-footer .text-link-w,
.s.is-footer .text-link-w *,
.s.is-footer .ftr-legal-link,
.s.is-footer .ftr-legal-link::after {
  transition-property: color, opacity, transform;
  transition-duration: 0.35s;
  transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}

.s.is-footer .text-link-w:hover,
.s.is-footer .text-link-w:focus-visible {
  color: var(--color--lime);
}

/* "Email me" is already painted the accent, so green-on-green would be no
   feedback at all. It inverts to the page's off-white instead — the same two
   colours, swapped, which reads as the same system rather than an exception. */
.s.is-footer .text-link-w.c-lime:hover,
.s.is-footer .text-link-w.c-lime:focus-visible {
  color: var(--color--white);
}

/* ── the statement ────────────────────────────────────────────────────────
   A full-bleed absolute overlay: `inset` pins it to both edges, so its box spans
   the whole footer width even though the words are centred in 48rem. Without
   `pointer-events: none` it swallows every hover in the band it covers — and the
   link columns sit right under that band. Measured at 1440px on the Golden
   Master, six footer links hit-tested to this element instead of their own
   anchor. Nothing inside is interactive, so it opts out of the pointer. */
.footer-statement-w {
  position: absolute;
  inset: 12rem 0 auto;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  text-align: center;
  pointer-events: none;
}

.footer-statement-layout {
  position: relative;
  display: flex;
  flex-flow: column;
  justify-content: flex-start;
  align-items: center;
  width: 48rem;
  margin-inline: auto;
}

.footer-rive-w {
  position: absolute;
  top: -6.8rem;
  z-index: var(--z-section);
  width: 13.0625rem;
  height: 8.5rem;
}

.image.is-footer-rive {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* ── the portrait and its CTA ───────────────────────────────────────────── */
.footer-bg-helmet-w {
  position: absolute;
  inset: auto 0 0;
  z-index: var(--z-section);
  display: flex;
  justify-content: center;
  align-items: flex-end;
  pointer-events: none;
}

.footer-bg-helmet-layout {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: flex-end;
}

.footer-bg-img {
  position: relative;
  bottom: -3rem;
  flex: none;
  width: 48.75rem;
  height: 39.875rem;
  object-fit: cover;
}

.footer-bg-bottom-btn-w {
  position: absolute;
  bottom: 1.75rem;
  pointer-events: auto;
}

/* ── the icon dock ────────────────────────────────────────────────────────
   Twenty-two glyph circles in a row that deliberately overflows and is clipped.
   A later stage rides them on a travelling sine wave; straight and centred is
   what they look like at rest, so this IS the settled state. */
.ftr-dock {
  position: relative;
  z-index: var(--z-raise);
  padding: 3rem 0 1.5rem;
  overflow-x: clip;
}

.ftr-dock-list {
  display: flex;
  flex-flow: row nowrap;
  justify-content: center;
  gap: 8px;
  margin: 0;
  padding: 0;
}

.ftr-dock-item {
  padding: 0;
  list-style: none;
}

.ftr-dock-circle {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  aspect-ratio: 1;
  width: 98px;
  border: 1px solid rgb(237 245 243 / 10%);
  border-radius: 50%;
  background: rgb(183 191 217 / 7%);
  backface-visibility: hidden;
}

.ftr-dock-ico {
  width: 36px;
  height: 36px;
  fill: currentcolor;
  color: var(--color--lime);
}

/* ── the legal row ────────────────────────────────────────────────────────
   THE INK HERE IS NOT THE ONE YOU WOULD EXPECT, and the reason is measured. This
   row does not sit on the black panel: `.footer-bg-gradient` paints the site
   green underneath it, and sampling the rendered pixels behind the row returns
   rgb(33,241,168) at 1440, 991 and 390 alike. So a green hover would be green on
   green, and the secondary grey first used here measured 1.37:1 and was
   effectively invisible. The accent is therefore expressed the way the site
   expresses it on every lime ground — as the dark ink lime is paired with. At
   rest the links carry that ink at .72, which measures 6.3:1.

   IT IS IN FLOW NOW, AT EVERY WIDTH (P8-10). The rule above was written for a
   row that sat on the lime — and the row did not. It was `position: absolute;
   inset: auto 0 0` inside `.footer-layout-clip`, so it resolved against the
   BLACK PANEL: dark-green type in the panel's last 16 px, under the icon dock,
   inside a `mask-image`. Every one of the three would have hidden it on its
   own. The markup moved out to the container (see `site-footer.html`); this is
   the row it now needs — a real band under the panel with its own air, which is
   also what makes the two ends legible enough to be worth having.

   ONE RULE FOR ALL WIDTHS. It used to be absolute above 991 and relative below,
   which is two layouts to keep in step for a row that is a copyright line and
   two links. `flex-wrap` does the whole of it: side by side while there is room,
   stacked when there is not.

   `--sp-*` rather than the layout gap: this is spacing INSIDE a component and
   the type scale is the right reference for a row this small. */
.footer-legal-links-w {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-3) var(--sp-6);
  padding-block: var(--sp-6) var(--sp-2);
}

.footer-legal-links-col {
  display: flex;
  gap: var(--gap);
}

.footer-legal-links-col.is-legal {
  flex-wrap: wrap;
  justify-content: flex-end;
  column-gap: calc(var(--gap) * 0.75);
}

/* `--fluid-font` resolves .75rem to 10px at 1440 and 7px at 390. Seven pixels is
   below any usable floor, and this is the one row a visitor may be legally
   entitled to read, so the whole row takes a floor. Rank is carried by ink, not
   by size. */
.footer-legal-links-w .text-title-small-label,
.s.is-footer .ftr-legal-link {
  font-size: var(--fs-micro);
}

.s.is-footer .ftr-legal-link {
  position: relative;
  color: var(--color--dark-green);
  opacity: 0.72;
  text-decoration: none;
}

.s.is-footer .ftr-legal-link::after {
  content: '';
  position: absolute;
  inset: auto 0 -0.25rem 0;
  height: 1px;
  background-color: currentcolor;
  transform: scaleX(0);
  /* The rule grows from the edge the word STARTS at. `transform-origin` is
     physical and does not read `direction`, so this is the one directional thing
     in a band that is otherwise entirely logical: on `/ar/` the underline drew
     itself backwards, from the end of the word to its beginning. */
  transform-origin: 0 50%;
}

html[dir='rtl'] .s.is-footer .ftr-legal-link::after {
  transform-origin: 100% 50%;
}

.s.is-footer .ftr-legal-link:hover,
.s.is-footer .ftr-legal-link:focus-visible,
.s.is-footer .ftr-legal-link.is-current {
  opacity: 1;
}

.s.is-footer .ftr-legal-link:hover::after,
.s.is-footer .ftr-legal-link:focus-visible::after {
  transform: scaleX(1);
}

/* ── THE OVERSCROLL GROUND, AND WHY THERE IS NO LONGER A LAYER FOR IT ──────
   Defect P11-05 was real — pulling past the end of a page on a phone showed a
   pale band under the footer — and the fix written for it was worse than the
   defect it removed. It is recorded here rather than deleted silently, because
   the next person to see that band will reach for the same thing.

   WHAT IT WAS. `.s.is-footer::after`, `position: fixed`, `inset: 100% 0 auto`,
   `height: 50svh`, `background-color: var(--color--lime)`. The reasoning was
   that `top: 100%` puts a band exactly one viewport down — invisible at rest,
   outside the scrollable overflow — and that the rubber band would reveal it.

   WHY IT WAS WRONG TWICE OVER.

   ·  IT WAS NOT INVISIBLE AT REST. `src/layouts/base.html` ships
      `viewport-fit=cover`, so on a phone the document paints edge to edge
      BEHIND the browser's bottom bar while a fixed box resolves against the
      visible region above it. `top: 100%` therefore lands the band precisely in
      the strip behind that bar — on every page, at every scroll position, not
      only during a bounce. The reported symptom was a green stripe under the
      URL field wherever you were on the site.

   ·  IT WAS A FIXED LAYER ON EVERY PAGE. Full width, composited on every scroll
      frame, for a colour that is visible for the length of a bounce.

   THE COLOUR ITSELF WAS RIGHT, and that part is worth keeping rather than
   re-deriving: the section's own ground is `[data-footer-theme='white']` forty
   lines above, but `.footer-bg-gradient` below paints `--color--lime` at its
   outer ring, and that ring is what the last band of every page — the copyright
   row and the two legal links — actually sits on. The document really does
   start on the paper and end on the lime.

   WHERE IT WENT: `base/document.css`, onto the canvas, as a two-stop gradient
   over the root's own box. Both ends of the document get their own colour, and
   there is no element to place, to composite, or to put behind a browser's
   bar. The full reasoning is in that file, beside the rule. */

.footer-bg-gradient {
  position: absolute;
  inset: auto 0 0;
  height: 100%;
  background-image: radial-gradient(
    circle farthest-corner at 50% -190%,
    var(--color--lime-zero) 68%,
    var(--color--lime) 83%
  );
  transform-origin: 50% 100%;
  pointer-events: none;
  overflow: clip;
}

/* ── responsive ─────────────────────────────────────────────────────────── */
@media screen and (max-width: 1024px) {
  .ftr-dock-list {
    gap: 6px;
  }

  .ftr-dock-circle {
    width: 70px;
  }

  .ftr-dock-ico {
    width: 28px;
    height: 28px;
  }
}

@media screen and (max-width: 991px) {
  .footer-bg-img {
    aspect-ratio: 48.75 / 39.875;
    width: auto;
    height: 31rem;
  }

  .footer-links-w {
    padding-inline: 4.1rem;
  }

  .footer-statement-w {
    top: 10rem;
  }

  .footer-statement-layout {
    width: 42rem;
  }

  /* Below the desktop split the two ends of the utility row stop being two ends
     — there is no width to hold them apart — so they centre and stack in reading
     order instead: copyright first, legal second, matching left-then-right
     above. The `position` and `margin-top` this rule used to carry are gone with
     the absolute base rule they were correcting (P8-10); wrapping is what
     stacks them now, so this only has to say where the wrapped rows sit. */
  .footer-legal-links-w {
    justify-content: center;
    text-align: center;
  }

  .footer-legal-links-col,
  .footer-legal-links-col.is-legal {
    justify-content: center;
  }
}

@media screen and (max-width: 767px) {
  /* The signature sits above the portrait; `top` tracks the width so the gap
     between them stays the same proportion (base is -6.8 / 13.0625 = -0.52). */
  .footer-rive-w {
    width: 8rem;
    top: -4.2rem;
  }
}

@media screen and (max-width: 479px) {
  .footer-layout-clip {
    --mask-url: url('/assets/svg/footer/clip-mobile.svg');

    aspect-ratio: 381.94 / 627.49;
  }

  .footer-bg-img {
    max-width: none;
    height: 24rem;
  }

  .footer-bg-bottom-btn-w {
    bottom: 1.1rem;
  }

  .footer-links-w {
    margin-top: 9rem;
    margin-bottom: 5rem;
  }

  .footer-links-col {
    width: 50%;
  }

  .footer-statement-w {
    position: relative;
    inset: auto;
  }

  .footer-statement-layout {
    width: auto;
    max-width: 90%;
    margin-top: 1.2rem;
  }

  .footer-rive-w {
    aspect-ratio: 13.0625 / 8.5;
    width: 7.5rem;
    height: auto;
    top: -3.6rem;
  }

  .footer-legal-links-w {
    gap: var(--sp-2) var(--sp-4);
    padding-block: var(--sp-5) var(--sp-2);
  }

  .footer-legal-links-col {
    gap: calc(var(--gap) * 0.5);
  }

  /* At 320-479px "Impressum Datenschutz" side by side is the widest this row
     ever gets. It wraps rather than overflows, and each link keeps a 24px
     minimum hit box — the smallest interactive target on the page, and the one
     most likely to be tapped by someone who needs it. */
  .footer-legal-links-col.is-legal {
    flex-wrap: wrap;
    justify-content: center;
  }

  .s.is-footer .ftr-legal-link {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
  }

  .ftr-dock-list {
    gap: 4px;
  }

  .ftr-dock-circle {
    width: 44px;
  }

  .ftr-dock-ico {
    width: 18px;
    height: 18px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .s.is-footer .text-link-w,
  .s.is-footer .text-link-w *,
  .s.is-footer .ftr-legal-link,
  .s.is-footer .ftr-legal-link::after {
    transition: none;
  }
}
}

/* ---- components · components/page-loader.css ---- */
@layer components {
/* ============================================================================
   THE PAGE LOADER — the lime field, the mark that draws itself, and the burst
   that opens the page THROUGH the mark.

   TRANSCRIBED FROM, AND THEN MEASURED ON, THE GOLDEN MASTER. The source is
   `.transition-w` in `site/partials/shell-standard.html`, its base rules at
   `site/assets/css/site.shared.043b62fef.css:5165` and the hand-authored AB
   override block at `:8006-8118`, plus `OVERLAY_LIFT` in its `tools/build.mjs`.
   Every number below was then read back off the reference at 1440x900 on a
   requestAnimationFrame sampler running from navigation start (P6-01):

     t=26      display flex · visibility visible · opacity 1
               background rgb(33, 241, 168) · mask-size "100% 100%, 0px 0px"
               path stroke-dashoffset 1px · fill-opacity 0 · stroke rgb(4, 5, 4)
     t=1114    dash-offset 0px · fill-opacity 0.524   (the draw past its 55% stop)
     lift      mask layer 2 jumps to 120px — `min(18vw, 9rem)` at this root size
     +680      mask layer 2 at 4032px = 280vmax · opacity 0 · visibility hidden

   WHY FIVE PHASES DID NOT SEE IT. The reference's overlay is made visible by an
   inline `<style>` in every emitted document (`.transition-w { display: flex }`)
   and taken away again 900 ms later. Every capture this project has ever taken
   waits for `load` first, so the harness has only ever photographed the page
   AFTER the lift. The loader was never absent from the Golden Master; it was
   never sampled.

   ─────────────────────────────────────────────────────────────────────────────
   THE MASK IS THE WHOLE EFFECT.

   Two mask layers composited with `exclude`: an opaque gradient covering the
   field, MINUS the brand glyph. The glyph layer rests at `0 0` — no hole, solid
   field — and the burst grows it to `280vmax`, punching an expanding hole in the
   shape of the mark. The page therefore arrives OUT OF THE LOGO rather than from
   behind a fade, which is the thing the user described and the thing a plain
   green overlay would not reproduce.

   The burst's 0% keyframe is exactly the drawn mark's size and position, so the
   hole opens underneath the mark rather than beside it, and the 160 ms mark fade
   covers the hand-off. At 900 ms the 1.6 s draw is at 56% — the outline has just
   closed and the fill has just begun — which is the frame it hands off from.

   ─────────────────────────────────────────────────────────────────────────────
   THE REVEAL IS PURE CSS, AND THAT IS A DELIBERATE IMPROVEMENT (brief §4/§48).

   The Golden Master's lift is an inline script; if it does not run, the overlay
   covers the site forever — the "16-second lime screen" its own build comments
   describe. Nothing here depends on JavaScript: the hold, the burst and the
   final `visibility: hidden` are one CSS animation with a 900 ms delay. The page
   reveals with no script at all, with a broken module graph, with the runtime
   disabled, and on a document whose boot threw. There is no state a failed
   enhancement can strand this in, so no hard-cap timer is needed — the cap IS
   the mechanism.

   NAMED DIVERGENCE, with its measurement. The reference anchors its 900 ms to
   NAVIGATION START (`Math.max(0, 900 - performance.now())`, so a slow document
   shortens the intro). A CSS `animation-delay` anchors to the element's first
   render instead, so this holds for 900 ms of VISIBLE time however long the
   document took. Restoring the reference's anchor needs a second pre-paint
   inline script, which INV-04 forbids (`layouts/base.html`: one module, one
   entry point) and which would put the reveal back on the JavaScript path this
   note exists to keep it off. The difference is bounded by time-to-first-render;
   on this build's one small render-blocking stylesheet that is tens of
   milliseconds, against a reference whose own lift was measured firing at
   2042 ms because 1.33 MB of engine was blocking the main thread.

   `runtime/loader/page-loader.js` therefore CANNOT reveal the page. It only
   REMOVES this element once the animation is over, so no overlay, listener or
   frame subscription outlives the intro (brief §51).

   ─────────────────────────────────────────────────────────────────────────────
   REDUCED MOTION IS ALREADY HANDLED, AND NOT BY THIS FILE.

   `base/reset.css` sets `animation-duration: 0.01ms !important` inside the
   reduce query, and an `!important` in the FIRST layer out-ranks every later
   layer (ADR-006, and `order.json` states the reversal). So under `reduce`: the
   mark is settled rather than drawn, the burst is instantaneous, and the 900 ms
   hold and the final `visibility: hidden` both still land — a branded loading
   state with the expansion removed and no way to trap the page.

   The reference specifies `.3s` there instead of instantaneous. That cannot be
   restored from `components`: a normal declaration loses to an important one,
   and an important one in a LATER layer is weaker still. It is left as it is on
   purpose — the user's preference outranking the design is the correct outcome,
   and it is recorded here rather than worked around.
   ============================================================================ */

.page-loader {
  /* The mark's rendered size. THREE things must agree on it — the SVG's own
     width and both ends of the burst's opening mask — so it is named once.
     `min(18vw, 9rem)` resolves to 120px at 1440, measured on the reference. */
  --loader-mark-size: min(18vw, 9rem);

  /* The hold. The reference's OVERLAY_LIFT constant, verbatim. */
  --loader-hold: 900ms;

  /* The draw, and the two fades that cover the hand-off at the lift. Measured:
     the label's inline `transition: opacity 300ms` is set at t=79 and its
     `opacity: 0` lands with the lift, then `display: none` ~370 ms later. */
  --loader-draw: 1600ms;
  --loader-mark-out: 160ms;
  --loader-label-out: 300ms;

  /* ABSOLUTE, NOT FIXED. Safari 26 treats a viewport-constrained fixed/sticky
     element touching an obscured edge as browser-chrome backing material. A
     full-screen lime loader therefore left the bottom toolbar on a solid lime
     strip even after this node was removed; routes whose normal document flow
     reached below the toolbar (notably blog detail) did not expose the defect.

     The loader is the first child of <body> and every navigation begins at the
     document origin, so absolute positioning produces the same first-paint box
     and the same mask animation without enrolling the field in Safari's edge
     extension heuristic. Keep the height below: it is still the visible small
     viewport, so the loader itself neither scrolls nor grows when the bar moves. */
  position: absolute;
  inset: 0;
  z-index: var(--z-loader);

  /* `100vw` and the measured `--vh`, exactly as the reference: on a phone the
     dynamic toolbar makes `100vh` taller than the visible box, and a loading
     field that overflows its own viewport scrolls the document under it. */
  width: 100vw;
  height: calc(var(--vh) * 100);

  display: flex;
  justify-content: center;
  align-items: flex-end;

  background-color: var(--color--lime);
  color: var(--color--dark-green);

  mask-image: linear-gradient(#000, #000), url('/assets/svg/brand/logo.svg');
  mask-repeat: no-repeat, no-repeat;
  mask-position: center, center;
  mask-size:
    100% 100%,
    0 0;
  mask-composite: exclude;
  -webkit-mask-image: linear-gradient(#000, #000), url('/assets/svg/brand/logo.svg');
  -webkit-mask-repeat: no-repeat, no-repeat;
  -webkit-mask-position: center, center;
  -webkit-mask-size:
    100% 100%,
    0 0;
  -webkit-mask-composite: xor;

  /* `forwards`, NOT `both`. A backwards fill would apply the 0% keyframe during
     the 900 ms hold and open the mark-sized hole immediately. */
  animation: ab-loader-burst var(--duration-slow) var(--ease-burst) var(--loader-hold) forwards;
}

/* The reference's own answer to the phone toolbar, at its own breakpoint:
   `site.shared.043b62fef.css:7143`, inside `@media screen and (max-width: 479px)`. */
@media screen and (max-width: 479px) {
  .page-loader {
    height: 100svh;
  }
}

.page-loader-mark {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  pointer-events: none;
  animation: ab-loader-mark-out var(--loader-mark-out) linear var(--loader-hold) forwards;
}

.page-loader-mark svg {
  width: var(--loader-mark-size);
  height: auto;
}

/* The draw. One dash unit is the WHOLE compound path because the markup sets
   `pathLength="1"`, so the offset animates 1 -> 0 without anyone measuring the
   real path length. The fill blooms only after the outline has closed. */
.page-loader-mark path {
  fill: currentColor;
  fill-opacity: 0;
  stroke: currentColor;
  stroke-width: 3;
  stroke-linecap: butt;
  stroke-linejoin: round;
  stroke-dasharray: 1;
  animation: ab-loader-mark-draw var(--loader-draw) var(--ease-mark-draw) both;
}

.page-loader-label {
  position: relative;
  z-index: var(--z-raise);
  bottom: var(--gap);
  font-family: var(--font-label);
  font-size: var(--text--btn-primary, var(--fs-sm));
  font-weight: 400;
  line-height: 1;
  text-transform: uppercase;
  animation: ab-loader-label-out var(--loader-label-out) linear var(--loader-hold) forwards;
}

@keyframes ab-loader-mark-draw {
  0% {
    stroke-dashoffset: 1;
    fill-opacity: 0;
  }
  55% {
    stroke-dashoffset: 0;
    fill-opacity: 0;
  }
  80% {
    fill-opacity: 1;
  }
  100% {
    stroke-dashoffset: 0;
    fill-opacity: 1;
  }
}

@keyframes ab-loader-mark-out {
  to {
    opacity: 0;
  }
}

@keyframes ab-loader-label-out {
  to {
    opacity: 0;
  }
}

@keyframes ab-loader-burst {
  0% {
    mask-size:
      100% 100%,
      var(--loader-mark-size) var(--loader-mark-size);
    -webkit-mask-size:
      100% 100%,
      var(--loader-mark-size) var(--loader-mark-size);
    opacity: 1;
    visibility: visible;
  }
  55% {
    opacity: 1;
  }
  99% {
    visibility: visible;
  }
  100% {
    mask-size:
      100% 100%,
      280vmax 280vmax;
    -webkit-mask-size:
      100% 100%,
      280vmax 280vmax;
    opacity: 0;
    /* The reference writes `pointer-events: none` alongside this. It does not
       need to: a `visibility: hidden` box is not a hit-test target, and
       `pointer-events` is not interpolable, so putting it in a keyframe would
       be a declaration that reads as a guarantee and is not one. */
    visibility: hidden;
  }
}
}

/* ---- components · components/blog-kit.css ---- */
@layer components {
/* ============================================================================
   THE BLOG'S SHARED VOCABULARY — one definition, two page families.

   WHY THIS FILE EXISTS. The index and the article print the same six objects: a
   topic pill, a date, a reading time, the dot that separates them, a button and
   a skip link. In the previous build those lived in `blog-index.css` and the
   ARTICLE consumed them — so an article page loaded a rule whose custom
   properties were declared under `[data-page='blog']` and never resolved, and
   `border: 1px solid var(--wx-rim)` silently collapsed to `medium none`. The
   repair was an alias block at the top of `blog-post.css` re-declaring nine
   `--wrx-*` names as `--art-*`, which is a workaround for a file that was in the
   wrong place.

   It is now in the right place: the palette is declared once, on `:root`, and
   both families read it. There is no alias block and nothing to keep in step.

   THE PALETTE IS ENTIRELY DERIVED. Not one value here is a new colour — every
   one is a `color-mix()` of the two the site already has, so the blog cannot
   drift from the rest of the site and a change to `--page-ink` moves all of it.

   THE ACCENT ON PAPER IS NOT `--color--lime`, AND THIS IS THE ONE RULE TO KEEP.
   `#21f1a8` measures **1.33:1** against `#edf5f3`. That is not a preference
   about contrast, it is invisibility — WCAG asks 3:1 of a graphic that carries
   meaning, and a mark at 1.33:1 carries nothing. So:

       --wx-accent      #0c7a55   4.82:1 on paper   text, rules, chart fills
       --wx-accent-lg   #149a6b   3.23:1 on paper   large marks, hover states
       --wx-mark        #21f1a8  13.82:1 on INK     never on paper, ever

   `--wx-mark` appears in exactly two places on the whole blog, and both are
   inside a filled `--wx-ink` box: the code plate and the reduced-motion figure.
   ========================================================================== */

:root {
  --wx-ink: var(--page-ink);
  --wx-dim: color-mix(in oklab, var(--page-ink) 74%, var(--page-bg));
  --wx-grey: var(--color--dark-green-tint-2);

  /* ── THE TWO QUIET GREYS, AND WHY EACH IS THE VALUE IT IS ────────────────
     Both are MEASURED against this paper rather than chosen by eye, because the
     first pass chose by eye and `golden:site-a11y` returned 890 serious
     colour-contrast nodes across twenty article pages for it.

       --wx-soft  52% ink → rgb(107,111,109) → 4.79:1   TEXT. It is the chapter
                  number in the rail, the provenance line above a heading, the
                  card ordinal and the search placeholder — all real text at
                  `--fs-micro`, all of which need 4.5:1. At 46% it measured
                  4.00:1 and failed every one of them.
       --wx-mute  40% ink → rgb(131,136,134) → 3.25:1   MEANINGFUL MARKS that
                  are not text: the ten bars of the topic mix. WCAG 1.4.11 asks
                  3:1 of a graphic that carries information, and a bar whose
                  height IS the reading time carries information. `--wx-rim`,
                  which it used to be, is 1.82:1 — a bar nobody can see is not a
                  chart.

     `--wx-rim`, `--wx-rule` and `--wx-line` stay where they are and are for
     BORDERS ONLY. A hairline separating two panels is decoration; the moment one
     of them is used for a glyph or a mark, it has to move up to one of the two
     above. */
  --wx-soft: color-mix(in oklab, var(--page-ink) 52%, var(--page-bg));
  --wx-mute: color-mix(in oklab, var(--page-ink) 40%, var(--page-bg));

  --wx-line: color-mix(in oklab, var(--page-ink) 10%, var(--page-bg));
  --wx-rule: color-mix(in oklab, var(--page-ink) 16%, var(--page-bg));
  --wx-rim: color-mix(in oklab, var(--page-ink) 22%, var(--page-bg));

  --wx-surface: color-mix(in oklab, var(--page-ink) 4%, var(--page-bg));
  --wx-plate: color-mix(in srgb, #ffffff 54%, var(--page-bg));

  --wx-accent: var(--color--lime-ink);
  --wx-accent-lg: var(--color--lime-ink-lg);
  --wx-mark: var(--color--lime);
  --wx-wash: color-mix(in oklab, var(--color--lime) 13%, var(--page-bg));
  --wx-wash-soft: color-mix(in oklab, var(--color--lime) 7%, var(--page-bg));

  --wx-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --wx-ease: var(--ease-signature);
  --wx-radius: 14px;
  --wx-radius-lg: 22px;

  /* THE READING DIRECTION AS A NUMBER. `translate` has no logical form, so an
     arrow animated with a positive X travels backwards on the Arabic document.
     Every directional transform on the blog multiplies by this. */
  --dir: 1;
}

html[dir='rtl'] {
  --dir: -1;
}

/* ── the skip links ─────────────────────────────────────────────────────── */

.wx-skip,
.art-skip {
  position: absolute;
  inset-inline-start: -9999px;
  /* Above the fixed header, the way `components/skip-link.css` puts the site's
     own one — a skip link the header paints over is a skip link nobody can
     reach with the keyboard, which is the only way it is ever used. */
  z-index: calc(var(--z-nav) + 1);
}

.wx-skip:focus-visible,
.art-skip:focus-visible {
  inset-inline-start: var(--padding--container);
  top: calc(var(--header-clear) + var(--sp-3));
  padding: var(--sp-3) var(--sp-5);
  background: var(--wx-ink);
  color: var(--page-bg);
  border-radius: 999px;
  font-size: var(--fs-sm);
}

/* ── the meta row: pill · date · reading time ───────────────────────────── */

.wx-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  font-family: var(--wx-mono);
  font-size: var(--fs-cap);
  letter-spacing: 0.02em;
  color: var(--wx-grey);
}

.wx-dot {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--wx-rim);
}

.wx-date,
.wx-read {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.wx-read-u {
  /* The suffix carries its own leading space in the bundle. */
  white-space: pre;
}

/* ── the topic pill ─────────────────────────────────────────────────────── */

/* ONE ACCENT, FOUR TOPICS, AND NO SECOND HUE. The four subjects are told apart
   by their words, not by colour: a four-hue scheme would have to clear 3:1
   against paper AND stay distinguishable under deuteranopia, and the honest
   result of trying that inside one green is four colours nobody can name. The
   pill's job is to say WHICH topic, and the label already does. */
.wx-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 0.42em 0.85em;
  border: 1px solid var(--wx-rim);
  border-radius: 999px;
  background: var(--wx-plate);
  color: var(--wx-dim);
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  font-weight: 600;
  line-height: var(--lh-flat);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  white-space: nowrap;
  text-decoration: none;
  transition:
    border-color var(--duration-fast) var(--wx-ease),
    background-color var(--duration-fast) var(--wx-ease),
    color var(--duration-fast) var(--wx-ease);
}

a.wx-pill:hover,
a.wx-pill:focus-visible {
  border-color: var(--wx-accent);
  color: var(--wx-accent);
  background: var(--wx-wash-soft);
}

.wx-pill.is-solid {
  background: var(--wx-ink);
  border-color: var(--wx-ink);
  color: var(--page-bg);
}

a.wx-pill.is-solid:hover,
a.wx-pill.is-solid:focus-visible {
  background: var(--wx-accent);
  border-color: var(--wx-accent);
  color: var(--page-bg);
}

.wx-pill.is-quiet {
  background: transparent;
  border-color: var(--wx-line);
  color: var(--wx-grey);
}

/* ── buttons ────────────────────────────────────────────────────────────── */

.wx-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  min-height: 3rem;
  padding: 0 var(--sp-6);
  border: 1px solid var(--wx-rim);
  border-radius: 999px;
  background: var(--wx-plate);
  color: var(--wx-ink);
  font-family: var(--font-label);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-decoration: none;
  transition:
    transform var(--duration-fast) var(--wx-ease),
    border-color var(--duration-fast) var(--wx-ease),
    background-color var(--duration-fast) var(--wx-ease),
    color var(--duration-fast) var(--wx-ease);
}

.wx-btn:hover,
.wx-btn:focus-visible {
  border-color: var(--wx-accent);
  color: var(--wx-accent);
  transform: translateY(-2px);
}

.wx-btn.is-solid {
  background: var(--wx-ink);
  border-color: var(--wx-ink);
  color: var(--page-bg);
}

.wx-btn.is-solid:hover,
.wx-btn.is-solid:focus-visible {
  background: var(--wx-accent);
  border-color: var(--wx-accent);
  color: var(--page-bg);
}

/* ── the section label: an eyebrow with a mark in front of it ───────────── */

.wx-section-label,
.art-section-label {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-block-end: var(--sp-6);
  color: var(--wx-grey);
}

.wx-section-dot,
.wx-eyebrow-dot,
.art-answer-mark,
.art-src-dot {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--wx-accent);
}

.art-src-dot.is-open {
  background: transparent;
  box-shadow: inset 0 0 0 2px var(--wx-accent);
}

/* ── REDUCED MOTION ──────────────────────────────────────────────────────
   Every transition in this file is a hover or focus affordance, and under
   `reduce` the affordance is the colour change alone: the transform is what a
   vestibular reader asked to be rid of, not the feedback. */
@media (prefers-reduced-motion: reduce) {
  .wx-btn:hover,
  .wx-btn:focus-visible {
    transform: none;
  }
}
}

/* ---- components · components/article-figures.css ---- */
@layer components {
/* ============================================================================
   THE ARTICLE'S FIGURE PRIMITIVES.

   Eight artefacts, one visual grammar: a hairline rule, a mono caption, and the
   thing itself. Nothing here is a card with a shadow — the article is a document
   and its figures are plates in it, so what separates them from the prose is a
   rule and space rather than an elevation.

   THE ONE DARK SURFACE ON THE ENTIRE BLOG IS THE CODE PLATE, and it is the only
   place `--wx-mark` (the bright lime) appears, because a listing is the one
   object here that is genuinely better read as light-on-dark and the only ground
   on which #21f1a8 clears contrast — 13.8:1 on ink against 1.33:1 on paper.

   EVERY FIGURE CARRIES ITS OWN NUMBERS AS TEXT. Nothing below is a picture of a
   value; each is a layout OF the value, so a screen reader, a crawler and a
   reader with images off get exactly what a sighted reader gets.
   ========================================================================== */

/* ── the shared head and caption ────────────────────────────────────────── */

/* An ADDITION to `.art-col`'s row gap, never a replacement for it — a figure
   wants more air around it than two paragraphs want between them, and this is
   the difference rather than the whole distance. */
.art-fig {
  margin-block: var(--sp-4);
}

.art-fig-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-4);
  margin-block-end: var(--sp-4);
}

.art-fig-rule {
  flex: none;
  width: 1.5rem;
  height: 1px;
  background: var(--wx-accent);
  transform: translateY(-0.3em);
}

.art-fig-cap,
.art-cap {
  font-family: var(--wx-mono);
  font-size: var(--fs-cap);
  line-height: 1.55;
  color: var(--wx-grey);
}

.art-cap-foot {
  display: block;
  margin-block-start: var(--sp-4);
  padding-block-start: var(--sp-4);
  border-block-start: 1px solid var(--wx-line);
}

.art-cap-note {
  display: block;
  margin-block-start: var(--sp-2);
  color: var(--wx-soft);
}

/* ── the callout ────────────────────────────────────────────────────────── */

/* THE MARK IS A DRAWN QUOTE, NOT A `"` CHARACTER. A typographic quote mark in
   `content` is a glyph from whichever family resolves, at whatever weight, and
   on `/ar/` it is a Zain quote in a Latin paragraph. Two rules and a radius are
   the same shape in every font on every document. */
.art-callout {
  position: relative;
  margin-block: var(--sp-3);
  padding: var(--sp-6) var(--sp-7);
  padding-inline-start: var(--sp-8);
  border-inline-start: 3px solid var(--wx-accent);
  border-radius: 0 var(--wx-radius) var(--wx-radius) 0;
  background: var(--wx-wash-soft);
}

html[dir='rtl'] .art-callout {
  border-radius: var(--wx-radius) 0 0 var(--wx-radius);
}

.art-callout-mark {
  position: absolute;
  inset-block-start: var(--sp-6);
  inset-inline-start: var(--sp-5);
  width: 12px;
  height: 10px;
  border-inline-start: 3px solid var(--wx-accent);
  border-inline-end: 3px solid var(--wx-accent);
  border-block-start: 3px solid var(--wx-accent);
  border-start-start-radius: 3px;
  border-start-end-radius: 3px;
  opacity: 0.4;
}

.art-callout p {
  margin: 0;
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--wx-ink);
}

/* ── the code listing ───────────────────────────────────────────────────── */

.art-code {
  margin: 0;
  padding: var(--sp-6) var(--sp-7);
  overflow-x: auto;
  border-radius: var(--wx-radius);
  background: var(--wx-ink);
  color: color-mix(in oklab, var(--color--white) 88%, var(--color--dark-green));
  font-family: var(--wx-mono);
  font-size: var(--fs-cap);
  line-height: 1.75;
  tab-size: 2;
  /* The listing is always Latin source, on every document, so it is pinned LTR:
     an Arabic page must not reorder a line of JavaScript. */
  direction: ltr;
  text-align: start;
}

.art-code:focus-visible {
  outline: 2px solid var(--wx-mark);
  outline-offset: 3px;
}

.art-code code {
  font: inherit;
  white-space: pre;
}

/* ONE SPAN PER LINE, CREATED BY THE RUNTIME on entry, so the listing can arrive
   a line at a time. Split on `\n` and nothing else — the site's own
   `split-registry.js` exists because splitting a joined script into characters
   spells Arabic wrong, and a line is coarser than a word. */
.art-code-l {
  display: block;
  opacity: 1;
}

/* THE START STATE IS GATED ON `[data-art-live]`, NOT ON `[data-art-split]`, and
   the difference is a blank code block. `data-art-split` is written by the
   splitter itself and would arm the hidden state whether or not anything was
   ever going to reveal it — including under `reduce`, where the entrance
   installer returns null and `.is-in` never arrives. `[data-art-live]` is on
   `<html>` and is written ONLY once the reveal observer exists, so the hidden
   state cannot outlive the thing that undoes it. Both attributes are required:
   one says a reveal is running, the other says these spans exist to reveal. */
[data-art-live] .art-code[data-art-split] .art-code-l {
  opacity: 0;
  transform: translateY(4px);
}

[data-art-live] .art-code-fig.is-in .art-code[data-art-split] .art-code-l {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--duration-base) var(--wx-ease) calc(var(--i, 0) * 26ms),
    transform var(--duration-base) var(--wx-ease) calc(var(--i, 0) * 26ms);
}

/* ── the table ──────────────────────────────────────────────────────────── */

.art-table-scroll {
  overflow-x: auto;
  border: 1px solid var(--wx-line);
  border-radius: var(--wx-radius);
  background: var(--wx-plate);
}

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

.art-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
  line-height: 1.5;
}

.art-table th,
.art-table td {
  padding: var(--sp-4) var(--sp-5);
  text-align: start;
  vertical-align: top;
}

.art-table thead th {
  position: sticky;
  inset-block-start: 0;
  background: var(--wx-surface);
  border-block-end: 1px solid var(--wx-rim);
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--wx-grey);
  white-space: nowrap;
}

.art-table tbody tr + tr td {
  border-block-start: 1px solid var(--wx-line);
}

.art-table tbody tr {
  transition: background-color var(--duration-fast) var(--wx-ease);
}

.art-table tbody tr:hover {
  background: var(--wx-wash-soft);
}

.art-table td:first-child {
  color: var(--wx-ink);
  font-weight: 500;
}

/* NUMBERS ALIGN AND PROSE DOES NOT. `tabular-nums` on every cell would make a
   sentence look letter-spaced; the last column is where these tables put their
   result, so that is where the digits are made to line up. */
.art-table td:last-child,
.art-table th:last-child {
  font-variant-numeric: tabular-nums;
}

/* ── the metric tiles ───────────────────────────────────────────────────── */

/* THE SEPARATORS ARE SHADOWS ON THE CELLS, NOT THE CONTAINER SHOWING THROUGH A
   GAP. The gap trick — a 1px gap over a `--wx-line` container — is one line
   shorter and wrong for an `auto-fit` grid: when four tiles wrap to three
   columns, the two EMPTY cells in the second row are not cells at all, so the
   container's own colour paints straight through and the figure ends in a grey
   slab. Measured at 390, where every four-tile group did exactly that.
   `box-shadow` costs no layout, meets its neighbour's inside the 1px gap so the
   rule still reads as a hairline, and an empty track simply stays plate. */
.art-metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
  gap: 1px;
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--wx-line);
  border-radius: var(--wx-radius);
  background: var(--wx-plate);
  overflow: hidden;
}

.art-metric {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-6);
  background: var(--wx-plate);
  box-shadow: 0 0 0 1px var(--wx-line);
}

/* PROPORTIONAL FIGURES, NOT TABULAR. A standalone display number set in
   `tabular-nums` gives every digit the width of a zero, so `121` reads loose at
   this size. Tabular is for columns that must align; this is not one. */
.art-metric-v {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--track-tight);
  color: var(--wx-ink);
}

.art-metric-l {
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--wx-accent);
}

.art-metric-n {
  font-size: var(--fs-cap);
  line-height: 1.5;
  color: var(--wx-grey);
}

/* ── the step timeline ──────────────────────────────────────────────────── */

.art-steps {
  position: relative;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The connector runs between the FIRST and LAST disc, not the full height, so
   it never dangles past the last step. */
.art-steps::before {
  content: '';
  position: absolute;
  inset-block: 1.1rem;
  inset-inline-start: 1.1rem;
  width: 1px;
  background: var(--wx-rule);
}

.art-step {
  position: relative;
  display: grid;
  grid-template-columns: 2.2rem minmax(0, 1fr);
  gap: var(--sp-5);
  padding-block: var(--sp-5);
}

.art-step-n {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.2rem;
  height: 2.2rem;
  border: 1px solid var(--wx-rim);
  border-radius: 50%;
  background: var(--wx-plate);
  font-family: var(--wx-mono);
  font-size: var(--fs-cap);
  font-variant-numeric: tabular-nums;
  color: var(--wx-accent);
}

.art-step-b {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.art-step-t {
  font-size: var(--fs-body);
  font-weight: 600;
  line-height: var(--lh-snug);
  color: var(--wx-ink);
}

.art-step-p {
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--wx-dim);
}

/* ── the term grid ──────────────────────────────────────────────────────── */

/* Same device, same reason as `.art-metrics` above. */
.art-terms {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: 1px;
  margin: 0;
  border: 1px solid var(--wx-line);
  border-radius: var(--wx-radius);
  background: var(--wx-plate);
  overflow: hidden;
}

.art-term {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-6);
  background: var(--wx-plate);
  box-shadow: 0 0 0 1px var(--wx-line);
}

.art-term dt {
  font-family: var(--wx-mono);
  font-size: var(--fs-cap);
  color: var(--wx-accent);
}

.art-term dd {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--wx-dim);
}

/* ── the one live demo ──────────────────────────────────────────────────── */

.art-demo {
  margin: 0;
  padding: var(--sp-7);
  border: 1px solid var(--wx-line);
  border-radius: var(--wx-radius-lg);
  background: var(--wx-plate);
}

.art-demo-stage {
  margin: var(--sp-5) 0;
  padding: var(--sp-7) var(--sp-5);
  border-radius: var(--wx-radius);
  background: var(--page-bg);
  font-size: var(--fs-display);
  line-height: 1.6;
  text-align: center;
  color: var(--wx-ink);
}

.art-demo-u {
  display: inline-block;
  padding: 0.05em 0.12em;
  border-radius: 4px;
  background: var(--wx-wash);
  box-shadow: inset 0 0 0 1px var(--wx-rim);
}

.art-demo-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-4);
}

.art-demo-btn {
  padding: 0.55rem var(--sp-5);
  border: 1px solid var(--wx-rim);
  border-radius: 999px;
  background: var(--page-bg);
  color: var(--wx-dim);
  font: inherit;
  font-size: var(--fs-sm);
  cursor: pointer;
  transition:
    background-color var(--duration-fast) var(--wx-ease),
    border-color var(--duration-fast) var(--wx-ease),
    color var(--duration-fast) var(--wx-ease);
}

.art-demo-btn:hover,
.art-demo-btn:focus-visible {
  border-color: var(--wx-accent);
  color: var(--wx-accent);
}

.art-demo-btn[data-on] {
  background: var(--wx-ink);
  border-color: var(--wx-ink);
  color: var(--page-bg);
}

.art-demo-count {
  margin: 0;
  margin-inline-start: auto;
  font-family: var(--wx-mono);
  font-size: var(--fs-cap);
  color: var(--wx-grey);
}

.art-demo-count b {
  font-size: var(--fs-h4);
  color: var(--wx-ink);
}

.art-demo-note {
  display: block;
  margin-block-start: var(--sp-5);
}

/* ── REDUCED MOTION ─────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  [data-art-live] .art-code[data-art-split] .art-code-l,
  [data-art-live] .art-code-fig.is-in .art-code[data-art-split] .art-code-l {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .art-table tbody tr,
  .art-demo-btn {
    transition: none;
  }
}
}

/* ---- sections · sections/home-sr-headings.css ---- */
@layer sections {
/* Zero-height, out of flow: the heading block must not contribute to the hero's
   geometry, and every later section's document offset depends on that. */
.home-title-w {
  position: absolute;
  height: 0;
}
}

/* ---- sections · sections/home-hero.css ---- */
@layer sections {
/* ============================================================================
   THE HERO — a 200vh track with a one-viewport pin, and the CSS wireframe.

   THE PIN IS NATIVE STICKY. `.sticky-track.home-hero` is 200vh tall and
   `.sticky-item.home-hero` sticks to `top: 0` inside it, so the hero holds
   still for exactly one viewport of scroll and then leaves. The Golden Master
   creates no pin-spacer and no JavaScript pin for this; the track's height IS
   the scroll length, and every document offset below the hero is measured from
   it. Change `200vh` and the whole page moves.

   TWO HEIGHT UNITS, ON PURPOSE, AND THE SPLIT IS RUNWAY vs BOX.

   THE TRACK STAYS `200vh`. It is a scroll RUNWAY — a length of document, not a
   thing anyone looks at — and `vh` is the large viewport, which is a constant
   that does not move when Safari's address bar does. A runway in `dvh` would
   change the height of the document mid-scroll and re-price every offset below
   the hero; a runway in `svh` would shorten the pin on a phone for no reason.

   THE PINNED BOX IS `100svh`. `.sticky-item.home-hero` and `.c.home-hero` are
   what a visitor actually sees held still, so they must FIT the visible area
   with the bar extended, which is what `svh` means. They no longer have to
   agree with the track: sticky pins for `track − item`, so a shorter item is a
   slightly longer pin and nothing is cut off. On a desktop `svh`, `lvh` and
   `vh` are the same number and this changes nothing at all.

   `.gl-canvas` keeps `calc(var(--vh) * 100)` because that box is a MEASUREMENT
   the WebGL scene reads, and `--vh` is the token `tokens/color.css` now resolves
   to `1svh` — so the portrait's frame and the pinned box are the same height by
   construction rather than by two rules agreeing.

   The corner wireframe (`wfx-`) is the only self-running motion in this file and
   it is entirely CSS: 20 strokes, one keyframe, no JavaScript, no per-frame
   work. It is desktop-only.
   ========================================================================== */

.sticky-track.home-hero {
  position: relative;
  height: 200vh;
}

/* `position: sticky; top: 0` comes from `layout/container.css`, which owns the
   class both tracks share. The hero adds only the height that makes its pin last
   one full viewport. */
.sticky-item.home-hero {
  height: 100vh;
  height: 100svh;
}

.s.home-hero {
  z-index: var(--z-hero);
}

/* `max-width: none` releases the shared container measure — the hero is
   full-bleed, and the GL box below is 100vw. The Golden Master needs
   `!important` here only because it re-declares the same value from a later
   inline block; the cascade layers make that unnecessary. */
.c.home-hero {
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
  max-width: none;
  min-height: 100vh;
  min-height: 100svh;
}

/* THE MEASURING BOX, not a canvas. Stage 16 reads this element's rect to size
   the render plane and paints into `.gl-wrap`; the box itself never contains
   anything and never paints. It is empty in the Golden Master too. */
.gl-canvas {
  position: absolute;
  inset: 0;
  z-index: var(--z-hero);
  width: 100vw;
  height: calc(var(--vh) * 100);
}

/* ── the corner wireframe ──────────────────────────────────────────────────
   TWO CLASSES, ONE ELEMENT. The Golden Master declares `width: 7.4375rem` and
   `height: 15.25rem` on `.home-hero-next-race-w` and `width: 8.25rem;
   height: auto` on `.wfx-host` — same element, same specificity, the later file
   wins both. Neither of the first pair has ever applied, so neither is written
   here; the board sizes itself and the box is bottom-anchored.

   The clip-path is the seam a later stage animates: the corner is wiped away
   with `ellipse(100% 0% ...)` on the first pixel of scroll. At rest the ellipse
   is wider and taller than the box, so it clips nothing.

   The bottom offset clears the notch on a device that has one: `--sa-b` is the
   named safe-area inset, declared once in `tokens/motion.css` with its own
   fallback, so no use site has to remember to write one. */
.home-hero-next-race-w {
  position: absolute;
  left: var(--gap);
  bottom: calc(var(--gap) + var(--sa-b));
  z-index: var(--z-hero);
  clip-path: ellipse(100% 120% at 50% 0);
  /* THE LATER STAGE IS HERE (P6-05). `--animation-default` is the Golden
     Master's own `0.75s cubic-bezier(0.65, 0.05, 0, 1)`, carried across in
     `tokens/motion.css`, and it is the value the reference animates this with. */
  transition: clip-path var(--animation-default);
}

/* WIPED AWAY ON THE FIRST PIXEL OF SCROLL.
   The ellipse collapses to zero height from the same origin, so the board and
   its caption are erased upward rather than faded — which is why the resting
   ellipse is taller than the box: at 120% it clips nothing, at 0% it clips
   everything. `runtime/sections/hero-corner.js` writes the class; measured on
   the reference, it is present at scroll 30 and absent at 0. */
.home-hero-next-race-w.hidden {
  clip-path: ellipse(100% 0% at 50% 0);
}

.wfx-host {
  display: flex;
  flex-flow: column;
  gap: 0.625rem;
  width: 8.25rem;
  height: auto;
  color: var(--color--dark-green-tint-2);
}

.wfx-board {
  display: block;
  width: 100%;
  height: auto;
}

/* the four corner ticks: registration marks, never animated */
.wfx-crop path {
  fill: none;
  stroke: currentcolor;
  stroke-width: 1;
  opacity: 0.4;
}

/* Every stroke carries `pathLength="100"`, so one dash pair drives paths of any
   real length. The delay is the group's slot in the 18 s cycle plus the
   stroke's own stagger inside it — which is why the `:nth-child` ladder below
   makes stroke ORDER in the markup load-bearing. */
.wfx-e {
  fill: none;
  stroke: var(--color--lime-off);
  stroke-width: 1.1;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 100 100;
  stroke-dashoffset: 100;
  animation: wfx-draw 18s linear infinite;
  animation-delay: calc(var(--wfx-t, 0s) + var(--wfx-i, 0) * 0.1s);
}

/* copy lines are drawn with the flat of the pen, not its tip */
.wfx-bar {
  stroke-width: 2.6;
}

.wfx-g1 {
  --wfx-t: 0s;
}

.wfx-g2 {
  --wfx-t: 4.5s;
}

.wfx-g3 {
  --wfx-t: 9s;
}

.wfx-g4 {
  --wfx-t: 13.5s;
}

.wfx-e:nth-child(1) {
  --wfx-i: 0;
}

.wfx-e:nth-child(2) {
  --wfx-i: 1;
}

.wfx-e:nth-child(3) {
  --wfx-i: 2;
}

.wfx-e:nth-child(4) {
  --wfx-i: 3;
}

.wfx-e:nth-child(5) {
  --wfx-i: 4;
}

.wfx-e:nth-child(6) {
  --wfx-i: 5;
}

/* Draw (0 -> 4.7%), hold (to 17.2%), erase off the far end (to 21.1%), then
   wait out the other three groups' slots. One 18 s pass per group. */
@keyframes wfx-draw {
  0% {
    stroke-dashoffset: 100;
    animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1);
  }

  4.7% {
    stroke-dashoffset: 0;
  }

  17.2% {
    stroke-dashoffset: 0;
    animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1);
  }

  21.1% {
    stroke-dashoffset: -100;
  }

  100% {
    stroke-dashoffset: -100;
  }
}

/* The label track (0.1em) is too open for a 12 px caption under a 132 px board;
   everything else about the eyebrow is inherited. */
.wfx-cap .text-eyebrow {
  letter-spacing: 0.04em;
}

/* ── the tablet branch ─────────────────────────────────────────────────────
   Hidden above 991, where the wireframe and the portrait carry the corner.
   Below it, this is the hero's only text. */
.home-hero-tablet-hero-text {
  display: none;
}

.eyebrow-w.hero-next-race {
  justify-content: center;
  align-items: flex-start;
  max-width: 80%;
  text-align: center;
}

/* ── the four scroll probes ────────────────────────────────────────────────
   Zero-width, invisible, and geometry the rest of the page reads: two
   nav-theme markers over the first and last 10/90 vh of the pin, the hero
   timeline's trigger, and the rect `--hsx-p` is scrubbed over. They are
   siblings of the pin, so they measure against the track's full 1,800 px. */
.hero-nav-theme {
  position: absolute;
  top: 0;
  left: 0;
}

.hero-nav-theme.is-1 {
  height: 10vh;
}

.hero-nav-theme.is-2 {
  top: 10vh;
  height: 90vh;
}

.hero-eyebrow-tracker {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 1px;
  height: 30vh;
  pointer-events: none;
}

.hero-rive-tracker {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 10px;
  height: 100vh;
  pointer-events: none;
}

/* ── 991 and below ─────────────────────────────────────────────────────────
   The hero column moves to the top of the viewport so the tablet text sits
   under the portrait's face rather than across it. `z-index: -5` puts that text
   BELOW the GL layer at -1: the portrait is drawn over it, which is the
   composition, not a mistake. */
@media screen and (max-width: 991px) {
  .c.home-hero {
    justify-content: center;
    align-items: flex-start;
  }

  /* Desktop only. The caption would resolve under 4 px on the rem ladder at
     these widths, and the tablet branch already carries its own mark and
     tagline. The Golden Master repeats this at 479 on the wrapper class; that
     repeat is redundant with this rule and is not reproduced. */
  .wfx-host {
    display: none;
  }

  /* Anchored to the container, not shrink-wrapped: the mark this column used to
     size itself from was deleted, after which it took its width from the
     eyebrow and `max-width: 80%` resolved against a box the eyebrow was itself
     setting — a self-tightening constraint that broke the tagline over three
     rows on a 390 px phone. */
  .home-hero-tablet-hero-text {
    display: flex;
    flex-flow: column;
    justify-content: flex-start;
    align-items: center;
    gap: var(--gap);
    position: relative;
    z-index: var(--z-hero-tablet-text);
    width: 100%;
    margin-top: 10rem;
    color: var(--color--dark-green);
  }
}

/* `svh` — the small viewport height — is the one place the hero opts out of
   `--vh`: at phone widths the measuring box must match the portrait's frame
   with the URL bar EXTENDED, or the face is cropped every time it retracts. */
@media screen and (max-width: 479px) {
  .gl-canvas {
    height: 100svh;
  }
}

/* The wireframe is the section's only self-running animation, and the answer is
   not "freeze it mid-stroke". The reset layer's kill switch would collapse the
   18 s cycle to 0.01 ms and land every stroke on the ERASED keyframe, leaving
   an empty board; `animation-name: none` takes the animation out of play
   entirely — a name the important duration cannot revive — and one group is
   shown fully drawn, which is what the artboard is for. */
@media (prefers-reduced-motion: reduce) {
  /* The corner still LEAVES — it is chrome for the hero and stays behind on the
     rest of the page either way — it just stops sliding there. Shortened rather
     than killed, which is how every other reveal on this page answers `reduce`. */
  .home-hero-next-race-w {
    transition-duration: 0.01s;
  }

  .wfx-e {
    animation-name: none;
    stroke-dashoffset: 0;
  }

  .wfx-g2,
  .wfx-g3,
  .wfx-g4 {
    display: none;
  }
}

/* ── the touch swipe lock ──────────────────────────────────────────────────
   A fixed control in the bottom-right corner, below the desktop boundary only.
   Every value is the Golden Master's:

     .home-hero-touch-controls-w { right: var(--gap); bottom: var(--gap);
                                   z-index: 20; display: flex; position: fixed }
     .home-hero-touch-desc-w     { width: 10rem; right: 6rem (5rem below 768);
                                   flex-flow: column; align-items: flex-end }
     .touch-desc-inner           { position: absolute; overflow: clip }
     .btn-w.is-touch-swipe       { border-radius: .75rem; width: min(4rem, 60px) }

   `display: none` is the DEFAULT and the media query below is what turns it on,
   which is the same single decision `hero-swipe-lock.js` gates its mount on. On
   the Golden Master those two are a media query and a JavaScript comparison
   against a literal, one pixel apart — which is how the control ends up visible
   with no click handler in the scrollbar band between them (defect F39). */
.home-hero-touch-controls-w {
  display: none;
  position: fixed;
  right: var(--gap);
  bottom: var(--gap);
  z-index: var(--z-sticky-over);

  align-items: center;
  justify-content: flex-start;
  pointer-events: auto;
}

/* THE WINDOW, AND IT IS THIS ELEMENT — WHICH IS THE WHOLE OF DEFECT P11-01.

   The two captions used to be `position: absolute` children of a flex box whose
   only children were those two, so the box had NO IN-FLOW CONTENT and collapsed
   to zero height. `overflow: clip` was on each CAPTION rather than on the box,
   and an element cannot clip itself: translating a clipped box moves its clip
   rectangle with it. So `translateY(100%)` slid the "locked" caption one line
   DOWN THE PAGE instead of one line OUT OF VIEW, and both captions were legible
   at once — measured at 320, 390, 430 and 768 in WebKit and Chromium alike,
   `TAP TO LOCK` at y 623.0-633.5 and `BACK TO SCROLL` at y 633.7-644.2.

   A GRID OF ONE CELL fixes it and needs no absolute positioning at all. Both
   captions are placed in cell 1/1, so they overlap by construction; grid's
   default `stretch` makes them the SAME HEIGHT, which is what makes `100%` of a
   caption exactly one window; and the clip is on the box that does not move.
   The invariant the runtime relies on is now structural: the box is one caption
   tall, and a caption translated by its own height is outside it.

   `clip` rather than `hidden`, as everywhere in this project: `hidden` would
   make this a scroll container, and a focus move inside it could shift the
   label out of view with no scrollbar to explain why. */
.home-hero-touch-desc-w {
  position: absolute;
  right: 6rem;

  display: grid;
  grid-template: 1fr / 1fr;
  flex: none;
  align-items: stretch;

  width: 10rem;
  color: var(--color--white);
  overflow: clip;

  /* The labels are read by sighted visitors only: the switch itself carries the
     accessible name, and announcing both a name and a caption that says the
     same thing twice is noise. */
  pointer-events: none;
}

.touch-desc-inner {
  grid-area: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  pointer-events: auto;
}

/* The resting pose: the "locked" caption waits one line below. The runtime
   slides them; this is where they start, so with no JavaScript the control
   reads correctly rather than showing both. */
.touch-desc-inner[data-home-swipe-desc='locked'] {
  transform: translateY(100%);
}

.btn-w.is-touch-swipe {
  width: min(4rem, 60px);
  height: auto;
  padding-right: 0;
  padding-left: 0;
  border-radius: 0.75rem;
  aspect-ratio: 1;
}

.btn-inner.is-touch-swipe {
  flex: none;
  width: 65%;
}

.btn-icon-w.is-touch-swipe {
  width: 90%;
}

.swipe-glyph {
  display: block;
  width: 100%;
  height: auto;
}

/* WHICH GLYPH IS SHOWING IS A FUNCTION OF THE STATE ATTRIBUTE, not of two
   inline `display` values written by JavaScript. The runtime owns one attribute;
   the stylesheet owns the appearance that follows from it. */
.swipe-glyph.is-locked,
[data-scroll-disabled='true'] .swipe-glyph.is-unlocked {
  display: none;
}

[data-scroll-disabled='true'] .swipe-glyph.is-locked {
  display: block;
  width: 70%;
  margin: 0 auto;
}

@media (max-width: 991px) {
  .home-hero-touch-controls-w {
    display: flex;
  }
}

@media (max-width: 767px) {
  .home-hero-touch-desc-w {
    right: 5rem;
  }
}
}

/* ---- sections · sections/home-signature.css ---- */
@layer sections {
/* ============================================================================
   THE SIGNATURE BLOCK — the hero's absolute overlay, and the 22-run pen.

   `section.s.home-marquee` is `position: absolute; inset: 0` INSIDE the hero's
   sticky item. It is not a marquee and nothing here travels sideways; the class
   is the Golden Master's and every checkpoint anchors on it.

   THE PEN. `mask#hsx-pen` is a chain of 22 stroke runs in the order the name is
   actually written, each as wide as the letterform it uncovers. Every run has
   `pathLength="100"`, so one dash pair drives all 22, and each run gets its own
   window on ONE inherited custom property: `--hsx-s` is where in that property's
   0 -> 1 travel the run starts, `--hsx-d` how much of it the run takes. The
   `clamp()` in `.hsx-i` turns the global progress into that run's local 0 -> 1.

   `--hsx-p: 1` IS THE SETTLED VALUE — fully written. A later stage scrubs it
   from 0 over `.hero-rive-tracker`'s rect; with nothing running, or under
   reduced motion, the signature is simply finished. The Golden Master gets there
   the other way round, blanking the page with `.hfx-js .hsx-host { --hsx-p: 0 }`
   and relying on a gate class to un-blank it — which means a script that fails
   to load leaves a blank signature. This file opts IN to motion instead, so the
   correct state is the one that needs nothing.
   ========================================================================== */

/* `user-select: none` because the whole block is artwork over a portrait: a
   drag across the hero should not select an invisible rectangle. */
.s.home-marquee {
  position: absolute;
  inset: 0;
  width: 100%;
  user-select: none;
}

/* Full-bleed: this container centres the dock against the viewport, not against
   the shared measure. */
.c.is-marquee {
  display: flex;
  justify-content: center;
  align-items: center;
  max-width: none;
  min-height: calc(var(--vh) * 100);
  padding-inline: 0;
}

/* `marqee` — the typo is in the Golden Master's markup AND its stylesheet
   (defect L5), so both halves stay wrong together or the 15rem is silently
   lost. The height is what `.marquee-adv-top-w` hangs its -15rem off. */
.marqee-adv-layout {
  position: relative;
  width: 100%;
  height: 15rem;
}

.marquee-adv-top-w {
  position: absolute;
  inset: -15rem 0 auto;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  margin-inline: auto;
}

.marquee-adv-top-layout {
  display: flex;
  flex-flow: column;
  justify-content: flex-start;
  align-items: center;
  gap: var(--gap);
  text-align: center;
}

.marquee-signature-w {
  width: 4.95rem;
  height: 2.28438rem;
}

/* The Golden Master hides this mark in its base rule and un-hides it from a
   Webflow component-variant class. With the variant class gone, the settled
   state is written directly. */
.image.is-ln4-logo {
  display: inline-block;
  width: 2.125rem;
  height: 2.125rem;
}

/* ── the dock ──────────────────────────────────────────────────────────────
   `.marquee-gl-target` is the rect the hero's render plane docks INTO (stage
   16), read as `offsetWidth`/`offsetHeight` on every refresh. Its size is a
   contract, not a look: it drops 17.5% across the 991/992 rem cliff, and the
   plane resizes to whatever it says at that moment.

   `pointer-events` flips twice on purpose — off on the full-width layer so it
   cannot swallow clicks across the hero, back on inside the artwork, which is
   where the GL scene wants the pointer. */
.marquee-gl-target-w {
  position: absolute;
  inset: 0;
  z-index: var(--z-hero);
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100vw;
  pointer-events: none;
}

.marquee-gl-target {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 35.375rem;
  height: 22.875rem;
}

.marquee-gl-rive-target {
  position: relative;
  z-index: var(--z-section);
  flex: none;
  width: 70rem;
  max-width: 800px;
  aspect-ratio: 70 / 50;
  pointer-events: auto;
}

.marquee-gl-rive-w {
  width: 100%;
  height: 100%;
}

.hsx-host {
  --hsx-p: 1;
  display: flex;
  justify-content: center;
  align-items: center;
}

.hsx-sig {
  display: block;
  width: 100%;
  height: auto;
  color: var(--color--lime);
}

/* `#fff` is not a colour decision and must not become a token: inside a mask,
   white is FULL LUMINANCE — "show what is underneath". Any palette value here
   would make the signature translucent.

   `100 100.5` rather than `100 100`: the half-unit gap keeps the tail of a run
   from re-entering at the head on a sub-pixel rounding error. */
.hsx-i {
  fill: none;
  stroke: #fff; /* mask luminance, not a palette colour */
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 100 100.5;
  stroke-dashoffset: calc(100 - 100 * clamp(0, (var(--hsx-p, 1) - var(--hsx-s)) / var(--hsx-d), 1));
}

/* The 22 windows, in writing order. `--hsx-s` + `--hsx-d` tile 0.03 -> 0.99 with
   deliberate overlaps where one stroke leads into the next, and the widths are
   the pen's width at that point in the name — they are artwork measurements,
   not a scale. The count is encoded here, in the markup and in the drawing;
   nothing checks that the three agree, so they are changed together or not at
   all. */
.hsx-i1 {
  --hsx-s: 0.03;
  --hsx-d: 0.0894;
  stroke-width: 5.25;
}
.hsx-i2 {
  --hsx-s: 0.1194;
  --hsx-d: 0.0531;
  stroke-width: 6;
}
.hsx-i3 {
  --hsx-s: 0.1725;
  --hsx-d: 0.1081;
  stroke-width: 4.5;
}
.hsx-i4 {
  --hsx-s: 0.2807;
  --hsx-d: 0.0613;
  stroke-width: 9;
}
.hsx-i5 {
  --hsx-s: 0.342;
  --hsx-d: 0.0343;
  stroke-width: 6;
}
.hsx-i6 {
  --hsx-s: 0.3763;
  --hsx-d: 0.0454;
  stroke-width: 5.25;
}
.hsx-i7 {
  --hsx-s: 0.4264;
  --hsx-d: 0.0591;
  stroke-width: 5.25;
}
.hsx-i8 {
  --hsx-s: 0.4902;
  --hsx-d: 0.0415;
  stroke-width: 6.75;
}
.hsx-i9 {
  --hsx-s: 0.5317;
  --hsx-d: 0.0291;
  stroke-width: 5.25;
}
.hsx-i10 {
  --hsx-s: 0.5608;
  --hsx-d: 0.0269;
  stroke-width: 7.5;
}
.hsx-i11 {
  --hsx-s: 0.5924;
  --hsx-d: 0.0607;
  stroke-width: 5.25;
}
.hsx-i12 {
  --hsx-s: 0.6531;
  --hsx-d: 0.0137;
  stroke-width: 3;
}
.hsx-i13 {
  --hsx-s: 0.6668;
  --hsx-d: 0.0759;
  stroke-width: 6.75;
}
.hsx-i14 {
  --hsx-s: 0.7427;
  --hsx-d: 0.02;
  stroke-width: 4.5;
}
.hsx-i15 {
  --hsx-s: 0.7627;
  --hsx-d: 0.0196;
  stroke-width: 3;
}
.hsx-i16 {
  --hsx-s: 0.7823;
  --hsx-d: 0.0068;
  stroke-width: 9;
}
.hsx-i17 {
  --hsx-s: 0.7937;
  --hsx-d: 0.0612;
  stroke-width: 5.25;
}
.hsx-i18 {
  --hsx-s: 0.8549;
  --hsx-d: 0.0138;
  stroke-width: 3;
}
.hsx-i19 {
  --hsx-s: 0.8687;
  --hsx-d: 0.0553;
  stroke-width: 6.75;
}
.hsx-i20 {
  --hsx-s: 0.924;
  --hsx-d: 0.04;
  stroke-width: 6;
}
.hsx-i21 {
  --hsx-s: 0.964;
  --hsx-d: 0.018;
  stroke-width: 3;
}
.hsx-i22 {
  --hsx-s: 0.982;
  --hsx-d: 0.008;
  stroke-width: 8.25;
}

/* Below the pin breakpoint the artwork widens against the viewport rather than
   the rem ladder, which is discontinuous at exactly these widths. */
@media screen and (max-width: 991px) {
  .marquee-gl-rive-target {
    width: 50vw;
  }
}

@media screen and (max-width: 479px) {
  /* `svh` and a released min-height: on a phone the block must fit the SMALL
     viewport, or the dock is pushed under the URL bar every time it extends. */
  .c.is-marquee {
    height: 100svh;
    min-height: auto;
  }

  .marquee-gl-target {
    width: 25rem;
    height: 29rem;
  }

  .marquee-gl-rive-target {
    width: 80vw;
  }
}
}

/* ---- sections · sections/home-journey.css ---- */
@layer sections {
/* ============================================================================
   THE JOURNEY — a horizontal track of four glass chapter cards on a rail.

   TWO LAYOUTS, ONE MARKUP.

   >= 992   `.horizontal-track` is a flex row 4,052 px wide (at 1440x900) inside
            a sticky pin, and a scroll controller translates it left by exactly
            `trackScrollWidth - innerWidth`. THAT CONTROLLER IS STAGE 8 AND IS
            NOT HERE, and neither is the section height it writes. The section
            therefore stands one viewport tall and the track's tail sits off the
            right edge, clipped by the body's `overflow-x: clip`. This is a
            KNOWN, RECORDED gap, not an approximation: the real height is a
            function of track width and viewport width, so writing 2,612 px here
            would hard-code one viewport's answer into a rule that every
            document offset below this section depends on.

   <= 991   No pin, no controller, no JavaScript: `.horizontal-track` becomes a
            plain vertical column of full-width cards. That branch is complete
            today and is the one most visitors see.

   THE CARD USED TO BE SQUARE-ISH BY CONSTRUCTION — 1480x1080, the exact ratio of
   the four photographs, so `object-fit: cover` cropped nothing at any width.
   Above 991 it is now 30% wider than that (P8-04, and the reasoning is on
   `--jr-frame-widen` below): a 1.78:1 frame with a centre crop, because the
   panel's vertical budget is spent to within half a pixel and the width had
   nowhere else to come from. BELOW 992 the photograph's own ratio still holds —
   that branch has no rail to clear, so the height follows the width.

   The copy sits on a four-layer progressive blur: four backdrop copies, each
   blurred harder and masked a little higher than the last, so the frost is dense
   under the text and dissolves as it climbs rather than ending on a line.
   ========================================================================== */

/* The pin's outer container runs full-bleed; the track measures itself against
   the viewport, not the shared container measure. */
.c.is-horiz-scroll {
  position: sticky;
  top: 0;
  max-width: none;
  padding-inline: 0;
}

.horizontal-pin-spacer {
  position: relative;
}

.horizontal-pin-sticky {
  position: sticky;
  top: 0;
  display: flex;
}

/* `padding-left: 75vw` is the lead-in: the first card enters from three
   quarters of the way across rather than sitting at the edge when the pin
   engages. It is also part of the track's scroll width, which is what stage 8
   derives the pin distance from — so it is a timing number as much as a
   spacing one. */
.horizontal-track {
  display: flex;
  flex: none;
  height: calc(var(--vh) * 100);
  padding-block: calc(var(--vh) * 7) calc(var(--gap) * 2);
  padding-inline: 75vw var(--gap);
}

/* The nav-theme floor for the section BELOW: zero height, hanging 10vh past the
   bottom edge, so the header changes ink as the next chapter's ground arrives. */
.sticky-track-theme-change {
  position: absolute;
  bottom: calc(var(--vh) * -10);
  /* Logical for consistency rather than for effect: the marker is zero-width and
     full-bleed, so which inline edge it starts at is unobservable. It is changed
     with the rest of this file so a reader does not have to work out which of
     the physical values here are the load-bearing ones. */
  inset-inline-start: 0;
  height: calc(var(--vh) * 10);
}

/* ── one chapter ───────────────────────────────────────────────────────────
   THE CARD'S OWN PALETTE AND STACK, declared once at the top of the subtree so
   every rule below reads a name. The greys and alphas here are card-local glass
   values that appear nowhere else on the site and are not in the shared palette;
   the one colour that IS a brand decision — the rail and the hover rim — is
   mixed from `--color--lime` so it tracks the accent.

   `--jr-z-*` is stacking WITHIN one card. `.jr-card-in` opens its own stacking
   context, so this ladder is local sibling order and cannot reach or be reached
   by the page ladder in `tokens/layer.css`. */
.jr-panel {
  --jr-card-h: 42vh;

  /* THE FRAME IS 30% WIDER THAN THE PHOTOGRAPH'S OWN RATIO (P8-04), AND THE
     HEIGHT COULD NOT MOVE WITH IT.

     The cards were 518px at 1440 and the copy inside them ran to four lines of
     a very short measure. The ask was 30% more width and more room for the
     text; the arithmetic says the height cannot follow. `.jr-card` is half a
     panel, the panel is the track's content height, and the track is one
     viewport less its own padding — 803.67px at 1440x900. Half of that, less
     the 2.6vh that holds the card clear of the rail, is 401.4px, and 42vh is
     401.4px. The vertical budget is saturated to within half a pixel, and a
     ratio-preserving +30% would need 54.6vh — 90px more than the composition
     has, at which point every card crosses the rail that runs through the
     middle of the panel and the alternating above/below reading collapses.

     So the frame goes to 1.78:1 and `object-fit: cover` centre-crops about a
     ninth off the top and bottom of each photograph. That is a real, chosen
     trade: the four sources are wide subjects — a desk, two monitors, a laptop,
     a hand holding a device — and none of them carries information in the top or
     bottom ninth. The comment this replaces claimed `cover` "never crops one",
     which was true and is now not; it is stated here rather than left for the
     next reader to discover from the pixels.

     ONE MULTIPLIER, NOT A NEW LITERAL, so the photograph's own ratio is still
     written down and the deviation from it is legible as a deviation. */
  --jr-frame-widen: 1.3;
  /* 1480 x 1080 — the exact ratio of the four source photographs. */
  --jr-card-w: calc(var(--jr-card-h) * (1480 / 1080) * var(--jr-frame-widen));

  /* 5rem, down from 7rem. The panel is the card plus twice this, and the card
     just gained 155px — at the old padding the rail would have grown 622px of
     pin at 1440 for four cards. Trimming the gutter puts most of that back and
     tightens a rhythm that was loose to begin with: the cards are the content,
     the gap between them is not. */
  --jr-pad: 5rem;

  --jr-ink: #f2f4ef;
  --jr-line: color-mix(in srgb, var(--color--lime) 30%, transparent);
  --jr-shell: #1b1d1d;
  --jr-rim: #ffffff26;
  --jr-gloss: #ffffff14;
  --jr-chip-ink: #10120f;
  --jr-scrim-mid: #0d0f0f7a;
  --jr-scrim-foot: #0a0c0cd1;
  --jr-sheen-band: #ffffff2e;
  --jr-pill-bg: #ffffffe6;

  /* The homepage's only monospace, and only inside this card: the BADGE and the
     COUNTER are instrument readings, not prose. There is no mono face in
     `tokens/`, so the stack is named here rather than repeated twice.

     THE BODY USED TO BE THE THIRD READER, AND IT WAS THE ONE PLACE ON THE
     HOMEPAGE WHERE PROSE WAS SET IN A FONT THE SITE DOES NOT OWN (P8-03). It is
     a sentence a visitor reads, not a number they glance at, and no system font
     stack is part of this site's identity — `ui-monospace` resolves to SF Mono
     on a Mac, Consolas on Windows and something else again on Android, so the
     one paragraph inside each chapter card had a different voice on every
     machine. The Arabic sheet had already decided this: `locale/arabic.css`
     routes `body *` through the Arabic face — Zain since P10 — and its mono
     carve-out lists only `code`, `pre`, `.cdx-code`, `.cdx-file` and `.scx-x`;
     `.jr-body` was deliberately not among them, so `/ar/` has always rendered
     this paragraph in the site's own face. EN and DE now agree with it. */
  --jr-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --jr-z-glass: 1;
  --jr-z-scrim: 2;
  --jr-z-sheen: 3;
  --jr-z-info: 4;

  position: relative;
  display: flex;
  flex: none;
  width: calc(var(--jr-card-w) + var(--jr-pad) * 2);
  height: 100%;
  padding-inline: var(--jr-pad);
}

/* the rail itself, threaded through the middle of every panel */
.jr-panel::before {
  content: '';
  position: absolute;
  inset: 50% 0 auto;
  height: 1px;
  background-color: var(--jr-line);
  pointer-events: none;
}

/* There are no opening and closing panels for the rail to start and finish on,
   so it fades up inside the first card and away inside the last instead of
   being cut off in mid-air.

   A GRADIENT ANGLE IS PHYSICAL AND HAS TO BE MIRRORED BY HAND. `90deg` runs
   left to right, so on `/ar/` — where the rail is entered from the RIGHT — the
   first card faded DOWN as the reader met it and the last faded up as they
   left. One custom property carries the angle so the two rules below stay one
   decision, and `270deg` is the same gradient read the other way. */
.jr-panel {
  --jr-rail-angle: 90deg;
}

html[dir='rtl'] .jr-panel {
  --jr-rail-angle: 270deg;
}

.jr-panel:first-child::before {
  background-color: transparent;
  background-image: linear-gradient(var(--jr-rail-angle), transparent, var(--jr-line) 46%);
}

.jr-panel:last-child::before {
  background-color: transparent;
  background-image: linear-gradient(var(--jr-rail-angle), var(--jr-line) 36%, transparent);
}

/* Physical centring on purpose: `left: 50%` pulled back by half the dot's own
   width is symmetric, so RTL needs no second rule. Same reasoning as the deck's
   bloom in `sections/home-certificate.css`. */
.jr-node {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: var(--z-raise);
  width: 0.4375rem;
  height: 0.4375rem;
  margin: -0.21875rem 0 0 -0.21875rem;
  border-radius: 50%;
  background-color: var(--color--lime);
  box-shadow: 0 0 0 0.3rem color-mix(in srgb, var(--color--lime) 12%, transparent);
}

/* the tether from the rail up or down to the card it belongs to */
.jr-node::after {
  content: '';
  position: absolute;
  left: 50%;
  width: 1px;
  height: 2vh;
  background-image: linear-gradient(var(--jr-line), transparent);
  transform: translateX(-50%);
}

.is-up .jr-node::after {
  bottom: 100%;
  background-image: linear-gradient(transparent, var(--jr-line));
}

.is-down .jr-node::after {
  top: 100%;
}

/* ── the card shell ────────────────────────────────────────────────────────
   Half the panel's height, hanging off the rail on whichever side the chapter
   alternates to. */
.jr-card {
  position: relative;
  display: flex;
  flex-flow: column;
  width: 100%;
  height: 50%;
}

.is-up .jr-card {
  justify-content: flex-end;
  padding-bottom: 2.6vh;
}

.is-down .jr-card {
  justify-content: flex-start;
  margin-top: auto;
  padding-top: 2.6vh;
}

/* The bezel: a hairline rim and a top-down gloss over the media, with 0.3125rem
   of shell showing all round it. `z-index` opens the stacking context the four
   overlays below live in. */
.jr-card-in {
  position: relative;
  z-index: var(--z-base);
  display: block;
  flex: none;
  width: 100%;
  height: var(--jr-card-h);
  padding: 0.3125rem;
  border: 1px solid var(--jr-rim);
  border-radius: 1.5rem;
  background-color: var(--jr-shell);
  background-image: linear-gradient(var(--jr-gloss), transparent 38%);
  overflow: clip;
  transition: border-color 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.jr-card-in:hover {
  border-color: color-mix(in srgb, var(--color--lime) 60%, transparent);
}

.jr-frame {
  position: relative;
  display: flex;
  justify-content: flex-end;
  align-items: center;
  width: 100%;
  height: 100%;
  border-radius: 1.1875rem;
  overflow: clip;
  transition: filter 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

/* The frame and the source share a ratio, so `cover` has nothing to crop. Scoped
   to the frame rather than declared on the bare `.image`: that class is shared
   with sections this file does not own. */
.jr-frame .image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.jr-card-in:hover .jr-frame {
  filter: saturate(1.12) brightness(1.05);
}

/* ── the progressive blur ──────────────────────────────────────────────────
   Four backdrop copies rather than one: a single `blur()` ends on a hard line
   wherever its mask stops. Each layer here is blurred harder and masked to
   finish LOWER than the one before, so the four fade-outs overlap into a
   continuous ramp from dense frost at the foot to clear glass at the top. */
.jr-glass,
.jr-scrim,
.jr-sheen {
  position: absolute;
  pointer-events: none;
}

.jr-glass {
  inset: auto 0.3125rem 0.3125rem;
  z-index: var(--jr-z-glass);
  height: 62%;
  border-radius: 0 0 1.1875rem 1.1875rem;
  overflow: clip;
  transition: height 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.jr-card-in:hover .jr-glass {
  height: 68%;
}

.jr-glass span {
  position: absolute;
  inset: 0;
}

.jr-glass span:first-child {
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  -webkit-mask-image: linear-gradient(to top, #000 0 26%, transparent 62%);
  mask-image: linear-gradient(to top, #000 0 26%, transparent 62%);
}

.jr-glass span:nth-child(2) {
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  -webkit-mask-image: linear-gradient(to top, #000 0 16%, transparent 48%);
  mask-image: linear-gradient(to top, #000 0 16%, transparent 48%);
}

.jr-glass span:nth-child(3) {
  -webkit-backdrop-filter: blur(13px);
  backdrop-filter: blur(13px);
  -webkit-mask-image: linear-gradient(to top, #000 0 8%, transparent 34%);
  mask-image: linear-gradient(to top, #000 0 8%, transparent 34%);
}

.jr-glass span:nth-child(4) {
  -webkit-backdrop-filter: blur(26px);
  backdrop-filter: blur(26px);
  -webkit-mask-image: linear-gradient(to top, #000 0 2%, transparent 20%);
  mask-image: linear-gradient(to top, #000 0 2%, transparent 20%);
}

/* Tint under the frost. Blur alone does not guarantee contrast — it averages
   the photograph, and a bright one averages bright. This is what holds the
   copy on all four images. */
.jr-scrim {
  inset: auto 0.3125rem 0.3125rem;
  z-index: var(--jr-z-scrim);
  height: 62%;
  border-radius: 0 0 1.1875rem 1.1875rem;
  background-image: linear-gradient(transparent 38%, var(--jr-scrim-mid) 64%, var(--jr-scrim-foot));
}

/* A light sweep that crosses the card on hover. It starts and ends off the
   card, so nothing of it is visible at rest.

   IT CROSSES WITH THE READING DIRECTION, NOT AGAINST IT. A `translateX` is
   physical and a gradient angle is too, so on `/ar/` the sweep entered from the
   side the reader's eye was leaving — which does not read as light moving over
   a surface, it reads as a scanline. Two custom properties rather than a
   duplicated block: the distance, the easing and the timing stay declared once. */
.jr-sheen {
  --jr-sheen-dir: 1;
  --jr-sheen-angle: 105deg;

  inset: 0.3125rem;
  z-index: var(--jr-z-sheen);
  border-radius: 1.1875rem;
  background-image: linear-gradient(
    var(--jr-sheen-angle),
    transparent 42%,
    var(--jr-sheen-band) 50%,
    transparent 58%
  );
  opacity: 0;
  transform: translateX(calc(-120% * var(--jr-sheen-dir)));
  transition:
    transform 0.85s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    opacity 0.4s linear;
}

html[dir='rtl'] .jr-sheen {
  --jr-sheen-dir: -1;
  --jr-sheen-angle: 75deg;
}

.jr-card-in:hover .jr-sheen {
  opacity: 1;
  transform: translateX(calc(120% * var(--jr-sheen-dir)));
}

/* ── the copy block ───────────────────────────────────────────────────────── */
.jr-info {
  position: absolute;
  inset: auto 0 0;
  z-index: var(--jr-z-info);
  display: flex;
  flex-flow: column;
  gap: 0.4rem;
  padding: 0 1.15rem 1.15rem;
}

.jr-info-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 0.5rem;
}

.jr-title {
  font-family: var(--font-body);
  font-size: var(--fs-h4);
  line-height: 1.08;
  letter-spacing: 0.008em;
  color: var(--jr-ink);
}

/* A CARD TITLE IS A HEADING THAT IS NOT AN `<h*>`, AND THE LOCALE SHEET NEVER
   SAW IT. `.jr-title` is a `<div>`, so `locale/arabic.css`'s
   `:is(h1, h2, h3, .text-*)` has never matched it and it has kept `1.08` on
   `/ar/` since the section shipped. Three of the four card titles carry a
   hamza-bearing alef — `بدأ`, `إلى` — which reaches 1.024em above the baseline,
   and every one of these titles wraps. Measured with fontTools over the strings
   that ship, at every word boundary, in the prose family's `size-adjust: 112%`:

       ثم دخل الذكاء الاصطناعي | إلى عدّة العمل    1.3762  <- the floor
       بدأ كل شيء | مع الويب                        1.2880

   1.45 is the same number `locale/arabic.css` derives for a display line that
   can wrap, and it clears this floor by 7.4% of an em. Found by the RTL audit,
   not by a report. */
html[lang='ar'] .jr-title {
  line-height: 1.45;
}

.jr-badge {
  display: flex;
  justify-content: center;
  align-items: center;
  flex: none;
  width: 1.375rem;
  height: 1.375rem;
  border-radius: 50%;
  background-color: var(--color--lime);
  font-family: var(--jr-mono);
  font-size: var(--fs-micro);
  color: var(--jr-chip-ink);
}

/* WEIGHT 400, NOT 300, AND THAT IS PART OF THE SAME FIX. `quicksand/light.woff2`
   is declared in `base/fonts.css` and selected by nothing the site renders —
   which is why `src/content/locales.json` preloads three faces and
   `tests/unit/emitted-document.test.mjs` asserts exactly three. Asking for 300
   here would newly select an unpreloaded face on the homepage and buy a FOUT for
   a weight nobody else uses. Regular at `--fs-sm` already sits lighter than the
   mono it replaces, and the recession is carried by `opacity` regardless. */
.jr-body {
  max-width: 100%;
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  font-weight: 400;
  line-height: 1.5;
  color: var(--jr-ink);
  opacity: 0.74;
}

.jr-foot {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin-top: 0.25rem;
}

.jr-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.28rem;
  letter-spacing: 0.06em;
  color: var(--jr-ink);
  opacity: 0.78;
}

.jr-ico {
  flex: none;
  width: 0.75rem;
  height: 0.75rem;
}

/* The counter is the one element that inverts — dark on white — so the reader
   always knows which of four they are looking at without hunting for it. */
.jr-pill {
  /* A COUNTER IS A NUMBER, AND A FLEX CONTAINER ORDERS ITS ITEMS BY DIRECTION.
     `01` `/` `04` are three flex items, so on `/ar/` they were laid out in
     reverse and every card claimed to be the fourth of one. `direction` rather
     than `row-reverse`, so the same rule also holds the bidi run together;
     `isolate` keeps it from reordering the Arabic beside it. The certificate's
     `.scx-x` makes the same correction for the same reason. */
  direction: ltr;
  unicode-bidi: isolate;
  display: inline-flex;
  align-items: center;
  gap: 0.18rem;
  margin-inline-start: auto;
  padding: 0.3rem 0.6rem;
  border-radius: 999px;
  background-color: var(--jr-pill-bg);
  font-family: var(--jr-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.04em;
  color: var(--jr-chip-ink);
  transition: background-color 0.45s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.jr-pill-sep {
  opacity: 0.45;
}

.jr-card-in:hover .jr-pill {
  background-color: var(--color--lime);
}

/* ── 992 and above: the pinned track ───────────────────────────────────────
   The last panel carries a lead-out so the fourth card is not still leaving the
   viewport when the pin releases. It is also 16vw of track width, and track
   width is what stage 8 derives the pin distance from.

   `no-preference` is part of the condition, not an afterthought — see the
   vertical branch below for why. */
@media screen and (min-width: 992px) and (prefers-reduced-motion: no-preference) {
  .jr-panel:last-child {
    width: calc(var(--jr-card-w) + var(--jr-pad) + 16vw);
    /* Logical, and load-bearing rather than cosmetic: this padding is part of
       `track.offsetWidth`, which is what the controller derives the pin distance
       and the section's own height from. On the physical right in an RTL
       document it put the lead-out where the reader STARTS. */
    padding-inline-end: 16vw;
  }
}

/* ── the vertical column: below 991, AND under `reduce` at any width ───────
   No pin, no track, no controller. The card takes the column's width and gets
   its height from the ratio, so the framing survives every screen rather than
   only the pinned one.

   THE `reduce` HALF OF THAT CONDITION IS NOT COSMETIC — it is the difference
   between a reduced-motion visitor reading four chapters and reading one.

   The rail is a horizontal system: four cards on a 4,052px track inside a
   1,440px viewport, and the only thing that brings the other three into view is
   the scrubbed transform. Switch that off for `reduce` — which is right, a
   scroll-driven sideways slide is exactly the motion the preference is about —
   and three quarters of the section is simply gone, clipped by the body's
   `overflow-x`. Measured before this rule existed: the document was 14,884px
   instead of 18,709, and the missing 3,825 were the chapters nobody could
   reach.

   So `reduce` gets the SAME presentation the phone gets: the same four cards,
   stacked, reading top to bottom, with no motion of any kind. It is CSS, so it
   holds whether or not the runtime is there (motion-architecture §6.1 tier T1),
   and `src/runtime/sections/home-journey.js` simply does not mount the
   horizontal system when the preference is set — the two halves agree because
   they read the same two conditions. */
@media screen and (max-width: 991px), screen and (prefers-reduced-motion: reduce) {
  /* `align-items: center` (P8-04). The column is `stretch` by default, so a
     panel narrower than the track sat hard against the cross-start edge — left
     on the Latin documents, right on the Arabic one. Four cards pinned to one
     side of a wide tablet column is not a composition anyone chose; it is what a
     capped width does when nothing says otherwise. */
  .horizontal-track {
    flex-flow: column;
    align-items: center;
    gap: 5rem;
    width: 100%;
    height: auto;
    padding-inline: var(--gap);
  }

  /* PIXELS, not rem, and that is the fix. `1rem` is viewport-derived here —
     8.89 px at 768, 10.83 px at 390 — so a 26rem cap collapsed to 231 px inside
     a 768 px column and the cards read as a narrow strip. Above ~430 CSS px it
     caps as intended; below it the column is already narrower, so a phone gets
     the full width without a second breakpoint.

     541 = 416 x 1.3 (P8-04) — the same +30% the rail branch takes above 991, so
     the two branches state one decision rather than two. The card keeps the
     photograph's own 1480/1080 ratio here: this branch has no rail through the
     panel's middle, so the height is free to follow the width and nothing is
     cropped. Below ~430px of column the cap is inert, so a phone sees no change
     — the widening is visible from small tablets up. */
  .jr-panel {
    --jr-card-w: 100%;
    width: 100%;
    max-width: 541px;
    height: auto;
    padding-inline: 0;
  }

  .jr-panel::before,
  .jr-node {
    display: none;
  }

  /* Two of the four blur layers go. Each is a backdrop copy plus a two-pass
     gaussian over the card's full width, and below this breakpoint the cards are
     FULL WIDTH and stacked, so one or two are on screen at once while the page
     scrolls — on a phone at DPR 3 that is the most expensive thing per frame on
     the page. The 2 px and 6 px passes are the two that are invisible over a
     photograph at this size; 13 px and 26 px carry the effect and both stay, so
     the frosted foot still reads exactly as designed. */
  .jr-glass span:first-child,
  .jr-glass span:nth-child(2) {
    display: none;
  }

  .jr-card {
    height: auto;
  }

  .is-up .jr-card,
  .is-down .jr-card {
    margin-top: 0;
    padding-block: 0;
  }

  .jr-card-in {
    aspect-ratio: 1480 / 1080;
    height: auto;
  }
}

@media screen and (max-width: 479px) {
  .horizontal-track {
    padding-block: 0;
  }
}
}

/* ---- sections · sections/home-otot.css ---- */
@layer sections {
/* ============================================================================
   S05 — CODE SIDE / DESIGN SIDE, and the studio shot that covers it.

   THE TRACK HAS NO HEIGHT AND MUST NOT BE GIVEN ONE. `.sticky-track.is-home-otot`
   is `position: relative` and nothing else; its 1,800 px on the Golden Master is
   two 100vh children measured at a 900 px viewport, not a number anyone typed.
   `.sticky-item` pins the split screen at `top: 0` for exactly as long as the
   studio shot below it takes to scroll past. Give the track a height and the pin
   silently decouples from the content it is pinning.

   THE PIN ITSELF IS NOT DECLARED HERE. `.sticky-item` is `position: sticky;
   top: 0` in `layout/container.css`, because two tracks share it and a shared
   class two section sheets each hold an opinion about is how they drift.

   NOTHING IN THIS FILE WAITS FOR JAVASCRIPT. `H_()` slides the two backdrop
   portraits in from opposite edges and nudges the text columns (stage 9); every
   value below is where that finishes, so the section is complete standing still.
   ========================================================================== */

.sticky-track.is-home-otot {
  position: relative;
}

.s.is-otot-home {
  color: var(--color--dark-green);
}

/* The one container on the page that gives up the measure: the backdrop pair is
   full-bleed and the split screen is centred inside it, not inside a column. */
.c.is-otot-home {
  position: relative;
  max-width: none;
}

/* ── the split screen ────────────────────────────────────────────────────
   `.sticky-item` is `position: sticky`, which creates a stacking context, so
   these two only ever contest each other and the Golden Master's literal 10 vs
   0 says nothing more than "text over backdrop". */
.otot-home-inner {
  position: relative;
  z-index: var(--z-raise);
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: calc(var(--vh) * 100);
}

/* ── the diptych does not mirror, on any document ──────────────────────────
   Owner decision, and the composition is the argument for it. These are two
   halves of one claim about one practice: the left column argues CODE and reads
   INTO the centre gap, the right argues DESIGN and reads OUT of it, and the two
   backdrop portraits are cropped for those two specific edges.

   The portraits are PHYSICAL — `inset: auto auto 0 0` and `auto 0 0 auto` — and
   were never mirrored, so under the implicit RTL flip each argument sat in front
   of the wrong face and the alt text said so. Pinning the LAYOUT is what puts
   them back, which is also why every physical offset below stays physical and
   needs no RTL counterpart of its own.

   `direction`, for the reason `components/site-nav.css` gives at length: one
   declaration owns every implicit flip, where a `row-reverse` owns one and
   leaves the next container somebody adds here mirrored. */
html[dir='rtl'] .otot-home-layout {
  direction: ltr;
}

/* The Arabic inside the columns still reads right to left, so `text-align:
   right` on `.is-1` and `left` on `.is-2` keep meaning what they say: the CODE
   column's words END at the centre gap and the DESIGN column's BEGIN there. */
html[dir='rtl'] .otot-home-layout :is(h2, p, .visually-hidden) {
  direction: rtl;
  unicode-bidi: isolate;
}

.otot-home-layout {
  display: flex;
  gap: calc(var(--gap) * 3);
}

.otot-home-text-col {
  display: flex;
  flex-flow: column;
  gap: var(--gap);
  justify-content: flex-start;
  align-items: stretch;
}

/* The two columns face each other across the centre gap: CODE reads right to
   left into it, DESIGN reads left to right out of it. */
.otot-home-text-col.is-1 {
  align-items: flex-end;
  text-align: right;
}

.otot-home-text-col.is-2 {
  align-items: flex-start;
  text-align: left;
}

.otot-home-text-w {
  position: relative;
}

.otot-home-p-w {
  display: flex;
  flex-flow: column;
  width: 21rem;
  margin-top: var(--gap);
}

.otot-home-p-w.is-2 {
  justify-content: flex-start;
  align-items: flex-start;
}

/* ── the backdrop portraits ──────────────────────────────────────────────
   Sized from the VIEWPORT HEIGHT, not the width: both are floor-anchored and
   their `aspect-ratio` then decides how much width they take, which is why
   `width: auto` overrides the shared vh width on both variants. The two ratios
   differ because the left plate is cropped narrower than its 898 px source. */
.otot-home-bg {
  position: absolute;
  inset: 0;
  z-index: var(--z-base);
}

/* Physical on purpose, and safe on every document because the diptych above
   does not mirror: each portrait is cropped for one specific edge and backs one
   specific column. `.is-1` is the left plate behind the CODE argument and `.is-2`
   the right plate behind DESIGN, in all three locales. */
.otot-home-img-w {
  position: absolute;
  inset: auto auto 0 0;
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
  width: calc(var(--vh) * 34.75);
  max-width: 27.625rem;
  height: calc(var(--vh) * 82.67);
}

.otot-home-img-w.is-1 {
  justify-content: flex-end;
  align-items: flex-start;
  aspect-ratio: 834 / 1984;
  width: auto;
  max-width: none;
  height: calc(var(--vh) * 90.67);
}

.otot-home-img-w.is-2 {
  inset: auto 0 0 auto;
  aspect-ratio: 898 / 1984;
  width: auto;
  max-width: none;
  height: calc(var(--vh) * 90.67);
}

.otot-home-img {
  object-fit: cover;
  width: 100%;
  max-width: none;
  height: 100%;
}

/* ── the full-bleed studio shot ──────────────────────────────────────────
   `--z-hero` is 10: the same slot, and the same relationship, the hero takes
   over the sticky track it lives in. This section is a sibling of `.sticky-item`
   and has to paint over it as it arrives. The token name says hero and the
   meaning is structural; a rename is requested in the handoff.

   TOP-FIRST FRAMING, WITH THE PARALLAX KEPT. The image is a fifth taller than
   its clipped wrapper and pinned to that wrapper's TOP edge, and it scales from
   the same edge — so the spare fifth is always taken off the BOTTOM, and the
   crop never eats into the top of the photograph. `home-fx` pans that spare
   fifth (stage 9); with no JavaScript, and under reduced motion, the image parks
   at the top, which is the correct frame anyway.

   The Golden Master also carries `.image.is-otot-home-end { height: 80vh }`
   inside its 479 block. It is dead: the rule below is later in the sheet at
   equal specificity, so `120%` wins at every width. It is not reproduced. */
.s.is-otot-end {
  position: relative;
  z-index: var(--z-hero);
}

.otot-home-end-img-w {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  min-height: calc(var(--vh) * 100);
  overflow: clip;
}

.image.is-otot-home-end {
  position: absolute;
  inset: 0 0 auto;
  object-fit: cover;
  object-position: 50% 0;
  width: 100%;
  height: 120%;
  transform-origin: 50% 0;
}

/* ── tablet and below ────────────────────────────────────────────────────
   The split screen stops being vertically centred and hangs from a fixed
   8rem top instead, because the portraits lose a third of their height here
   and the text would otherwise float in the middle of an empty band. */
@media screen and (max-width: 991px) {
  .otot-home-inner {
    justify-content: center;
    align-items: flex-start;
    padding-top: 8rem;
  }

  .otot-home-img-w,
  .otot-home-img-w.is-1 {
    height: calc(var(--vh) * 62.67);
  }

  /* -1px, not 0: the right plate is `justify-content: flex-end` here, and a
     hairline of container padding shows through at some device pixel ratios. */
  .otot-home-img-w.is-2 {
    right: -1px;
    justify-content: flex-end;
    align-items: flex-start;
    height: calc(var(--vh) * 62.67);
  }
}

/* ── phone ───────────────────────────────────────────────────────────────
   Two columns side by side rather than a flex row, and both portraits are
   pushed 7rem off their own edge so the faces sit behind the text instead of
   competing with it in a 360 px frame. */
@media screen and (max-width: 479px) {
  .otot-home-layout {
    display: grid;
    grid-template-rows: auto;
    grid-template-columns: 1fr 1fr;
    gap: calc(var(--gap) * 2);
  }

  .otot-home-p-w {
    width: 100%;
  }

  .otot-home-img-w.is-1 {
    left: -7rem;
    width: auto;
  }

  .otot-home-img-w.is-2 {
    right: -7rem;
  }

  .otot-home-end-img-w {
    min-height: calc(var(--vh) * 60);
  }
}
}

/* ---- sections · sections/home-stack-grid.css ---- */
@layer sections {
/* ============================================================================
   S06a — SIXTEEN STACK TILES.

   WHAT A TILE IS, IN ONE PASS. A masked square (`.helmet-grid-item`, notched by
   an SVG mask), two frame pairs stacked inside it — one desktop, one phone, each
   a tint BASE outline and a lime OVERLAY that fades in on hover — a resting mark
   (`.cdx-abbr` + `.cdx-full`), and a code panel that the notch wipes open over
   the mark. The label under the tile is absolutely positioned and belongs to the
   `<li>`, not to the masked box, which is why it is not clipped by the mask.

   THE MASK IS WHAT MAKES THE SQUARE A STACKING CONTEXT. `mask-image` on
   `.helmet-grid-item` creates one, so every z-index inside a tile is contested
   only against its own siblings. That is why the Golden Master's literals (10,
   10, 0, 5) carry no information beyond an ordering, and why the tokens below
   are `--z-raise` and `--z-base` rather than a transcription.

   THE PANELS ARE SETTLED. `cdx` types one snippet at a time on hover (stage 10);
   the settled state is `.cdx-snip:first-child` standing fully written at its
   natural `.cdx-line` width. The Golden Master's `.hfx-js` start states —
   `display: none` on every snippet, `width: 0` on every line — are NOT
   reproduced: they exist to hide the settled state from a browser that is about
   to animate it, and this phase animates nothing.

   ONE TIMING, FOUR PROPERTIES. The notch wipe, the mark's scale and both overlay
   fades all run on `--animation-default` — the Golden Master's site-wide 0.75s on
   `cubic-bezier(0.65, 0.05, 0, 1)`, already carried into `tokens/motion.css` —
   so the hover arrives as one gesture rather than as four that nearly agree.
   ========================================================================== */

.s.home-helmets {
  position: relative;
  z-index: var(--z-section-high);
}

/* THE DARK CHAPTER OPENS ON THE SITE'S OWN RHYTHM (P8-08).

   This container held `calc(var(--gap) * 2)` over `var(--gap)` — 33px above the
   "Languages & Technologies" head and 17px under the last row of tiles, at
   1440. Both are container gaps, not section rhythm, and the first one is the
   one you see: the head landed 33px under the hard edge where the full-bleed
   studio photograph ends, so the loudest heading in the dark chapter read as
   pinned to the top of it while the block below breathed. The two numbers were
   also not a ratio anyone chose — they were the same token, doubled once.

   `--section-y` is the ONE section rhythm the whole site is built on
   (`tokens/type.css`), and it is what every other page-level section uses. Both
   ends take it, so the chapter opens and closes on the same measure and the
   spacing follows the type scale instead of the layout gap. 92px at 1440,
   52px at the phone floor. */
.c.home-helmets {
  position: relative;
  padding-top: var(--section-y);

  /* THE CHAPTER CLOSES ON AN ARC NOW, NOT ON A RULED EDGE (P10).

     P8-08 gave both ends the site's one rhythm because both ends were flat
     cuts. They are not any more: the brand-mark callout used to sit between the
     last tile row and the next section, supplying about 590px of black air at
     1440, and it was removed at the owner's instruction. What closes the chapter
     now is `.scx-visor` — the certificate's arc, hosted by the section BELOW and
     painting upward into this one, 8rem deep at the centre and 5.6rem at the
     gutters (`sections/home-certificate.css`). A dome eats the clearance a
     ruled edge did not.

     So the closing measure is the rhythm PLUS the arc's own depth. Rather than
     copy 8rem / 5rem / 3.5rem out of another section's stylesheet — a number two
     files apart is how the legacy drifted — the same quantity is expressed in
     the token this chapter already uses. Across all four bands `2 x
     --section-y` IS `--section-y + arc`:

         1440   184px  vs  92 + 107 = 199
          992   151px  vs  75 +  74 = 149
          991   151px  vs  75 +  57 = 132
          390   106px  vs  53 +  39 =  92

     One token, no cross-file dependency, and it tracks the arc's own
     breakpoints because both are driven by the root ladder. */
  padding-bottom: calc(var(--section-y) * 2);
}

/* Four across, two across from tablet down. The list markers go with the
   `<div role="list">` they replaced; the grid never had room for them. */
.helmet-grid {
  display: grid;
  grid-template-rows: auto;
  grid-template-columns: 1fr 1fr 1fr 1fr;
  gap: var(--gap);
  margin: 0;
  padding: 0;
  list-style: none;
}

.helmet-grid-item-w {
  position: relative;
}

/* THE NOTCHED SQUARE. The mask is an SVG fill, not a clip-path, because the
   notch is a rounded rectangle with a bevelled corner and the phone version is a
   different shape rather than the same shape scaled — hence the second file
   below 480. `-webkit-` first: unprefixed `mask-image` only reached Chrome 120,
   and this is the one place on the page where losing the mask loses the
   silhouette rather than a nicety. */
/* IT IS A REAL `<button>`, AND THAT IS THE ACCESSIBILITY DECISION, NOT A DETAIL.

   The Golden Master ships a `<div>` and promotes it at runtime with `tabindex`,
   `role="button"`, a `keydown` handler for Enter and Space, a `click` handler and
   a `pointerenter`/`pointerleave` pair. That is five bindings re-implementing what
   one element already is, and it is the exact shape of legacy defect D11, where
   the same triple-binding on a control that WAS already a button made Enter toggle
   twice. Phase 3 made the same call on the nav toggle for the same reason.

   A real button gives Enter, Space, click and tap from ONE `click` listener, gets
   its focusability and its role from the parser rather than from a script, and is
   still a control when the JavaScript never arrives. What it costs is the reset
   below — and `display: flex` is doing double duty there, because it is also what
   stops Safari refusing to align a button's children. */
.helmet-grid-item {
  --mask-url: url('/assets/svg/stack-grid/helmet-mask-fill.svg');

  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: inherit;
  /* The panel it discloses is `aria-hidden` decoration, so a pointer that lands
     on the tile must reach the tile, not a child. */
  cursor: pointer;

  position: relative;
  display: flex;
  aspect-ratio: 406.89 / 411;
  width: 100%;
  -webkit-mask-image: var(--mask-url);
  mask-image: var(--mask-url);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: cover;
  mask-size: cover;
}

/* ── the frame pairs ─────────────────────────────────────────────────────
   Two pairs ship and exactly one is displayed: the phone frame is a different
   outline, not a scaled one, so it cannot be a media-query size change. */
.helmet-grid-frame-w {
  position: relative;
  z-index: var(--z-raise);
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.helmet-grid-frame-w.mobile {
  display: none;
}

.helmet-grid-frame {
  display: flex;
  aspect-ratio: 406.89 / 411;
  width: 100%;
}

.helmet-grid-frame.is-base {
  color: var(--color--dark-green-tint-2);
}

/* The lime outline rests invisible and is the loudest part of the hover. */
.helmet-grid-frame.is-overlay {
  position: absolute;
  inset: 0;
  z-index: var(--z-raise);
  opacity: 0;
  transition: opacity var(--animation-default);
}

/* THE FRAME PAINT LIVES HERE, NOT ON 64 `<path>` ELEMENTS. The markup carries
   geometry only — `viewBox` and `d`. `non-scaling-stroke` is desktop-only and
   deliberate: the desktop tile is drawn from a 407-unit box into roughly a third
   of that, so a scaled 2-unit stroke would render under a pixel. */
.helmet-grid-frame svg {
  width: 100%;
  height: 100%;
}

.helmet-grid-frame path {
  fill: none;
  stroke-width: 2;
}

.helmet-grid-frame.is-overlay path {
  stroke: var(--color--lime);
}

.helmet-grid-frame.is-base path {
  stroke: currentcolor;
}

.helmet-grid-frame-w.destop .helmet-grid-frame.is-base path {
  vector-effect: non-scaling-stroke;
}

.helmet-grid-frame-w.mobile .helmet-grid-frame.is-base path {
  stroke-width: 1;
}

/* ── the mark, and the panel that covers it ──────────────────────────────
   `.helmet-grid-item-reveal-img` keeps its Webflow name on purpose: it is the
   element the notch wipe animates and it does not care that it is now a panel
   rather than a picture. It starts scaled 5% over and clipped to a zero-height
   ellipse pinned to the top edge, so the wipe reads as the panel dropping in
   from the top rather than as a box appearing. */
.helmet-grid-item-img-w {
  position: absolute;
  inset: 0;
  z-index: var(--z-base);
  display: flex;
  justify-content: center;
  align-items: center;
}

.helmet-grid-item-img-helmet {
  position: relative;
  width: 100%;
  height: 100%;
  transition: transform var(--animation-default);
}

.helmet-grid-item-reveal-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  clip-path: ellipse(100% 0% at 50% 0);
  transform: scale(1.05);
  transition:
    clip-path var(--animation-default),
    transform var(--animation-default);
}

/* `.is-open` is the touch and keyboard equivalent of `:hover`, set by the tile's
   own controller at stage 10. Without it this section is desktop-only — `:hover`
   never matches on a touch screen and the tile is a `<div>`, so all sixteen
   panels stay sealed on a phone. The selectors are authored now so that stage 10
   is a JavaScript change and not a stylesheet change. */
[data-helmet-item]:hover .helmet-grid-item-reveal-img,
[data-helmet-item].is-open .helmet-grid-item-reveal-img {
  clip-path: ellipse(100% 120% at 50% 0%);
  transform: scale(1);
}

.helmet-grid-item-w:hover .helmet-grid-item-img-helmet,
.helmet-grid-item-w:has(.is-open) .helmet-grid-item-img-helmet {
  transform: scale(1.1);
}

.helmet-grid-item-w:hover .helmet-grid-frame.is-overlay,
.helmet-grid-item-w:has(.is-open) .helmet-grid-frame.is-overlay {
  opacity: 1;
}

/* The tile is a `<div>` and gets its focusability from stage 10's `tabindex`.
   `:focus-visible` only, so a tap does not leave a ring behind. */
[data-helmet-item]:focus-visible {
  outline: 2px solid var(--color--lime);
  outline-offset: 3px;
}

/* ── the label under the tile ────────────────────────────────────────────
   Outside the masked box, so the notch never clips it. */
.helmet-grid-item-text-w {
  position: absolute;
  inset: auto 0 0 auto;
  z-index: var(--z-raise);
  display: flex;
  justify-content: flex-end;
  align-items: center;
  width: auto;
  max-width: none;
  height: 2rem;
}

.helmet-grid-item-date-w {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  margin-inline-start: 0.8rem;
}

/* THE EXTENDER NEVER RENDERS ON THE GOLDEN MASTER. `display: none` is declared
   here and is not overridden at 991, at 479, or anywhere else in any sheet on
   any route — so the grey/lime fade pair, and the hover machinery that was
   written for it, are dead on every width. The box is reproduced because the
   markup is, and because the day someone enables it the geometry should be the
   geometry it was drawn with; the unobservable parts (a -1 stacking slot, the
   overlay's opacity fade, the colour swap on hover) are not. Flagged for the
   defect register rather than silently deleted. */
.helmet-grid-extender {
  position: absolute;
  inset: auto auto 0 -11rem;
  display: none;
  justify-content: flex-start;
  align-items: flex-start;
  width: 13.9375rem;
  height: 2.5rem;
}

.helmet-grid-extender-img {
  position: relative;
  flex: none;
  max-width: none;
  height: 100%;
  color: var(--color--dark-green-tint-2);
}

.helmet-grid-extender-img.is-overlay {
  position: absolute;
  inset: 0;
  width: 100%;
  opacity: 0;
}

/* The nav's ink floor for the dark chapter. Paints nothing; it is a probe. */
.helmet-nav-theme-target {
  position: absolute;
  inset: auto auto 0 0;
  height: calc(var(--vh) * 100);
}

/* ── the code editor inside the tile (cdx-) ──────────────────────────────
   `font-variation-settings: "wght" 700, "wdth" 96` is dropped from `.cdx-abbr`:
   the site ships static Quicksand instances, so the setting selects nothing and
   the real weight comes from `font-weight`. */
.cdx-mark {
  display: flex;
  flex-flow: column;
  gap: 0.375rem;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  padding: 1rem;
}

.cdx-abbr {
  color: var(--color--green-off-white-1);
  font-size: var(--fs-h2);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  /* The Golden Master's own values, and NOT `--animation-default`: the mark
     retreats faster than the panel arrives, which is what makes the two read as
     one movement instead of a crossfade. The curve is symmetric ease-in-out
     rather than the site default's long settle. Declared here rather than inside
     the :hover rule, where the Golden Master put it — a transition that exists
     only in the hover state eases in and SNAPS back on exit. */
  transition: color 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}

/* The mark steps back as the panel arrives, so the two never fight. */
.helmet-grid-item:hover .cdx-abbr,
.helmet-grid-item.is-open .cdx-abbr {
  color: var(--color--dark-green-tint-2);
}

/* NOT --dark-green-tint-2. That token is the site's quiet ink ON PAPER, where it
   measures 6.9:1; on this deck's near-black it is 2.7:1 and the label reads as a
   smudge. --grey-on-track is the token for quiet text on a dark ground, which is
   exactly what this is. The same swap applies to `.cdx-file` below. */
.cdx-full {
  color: var(--color--grey-on-track);
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  line-height: 1.3;
  letter-spacing: 0.06em;
  text-align: center;
  text-transform: uppercase;
}

.cdx-panel {
  display: flex;
  flex-flow: column;
  gap: 0.625rem;
  justify-content: center;
  align-items: flex-start;
  padding: 1.125rem;
  overflow: hidden;
  background-color: var(--color--black);
}

/* The filename above the snippet is a path, and a path is left-to-right for the
   same reason the snippet below it is — see `.cdx-code`. `index.html` in an RTL
   box resolves its dot against the paragraph. */
.cdx-file {
  direction: ltr;
  unicode-bidi: isolate;
  color: var(--color--grey-on-track);
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  letter-spacing: 0.06em;
}

/* CODE IS LEFT-TO-RIGHT WHATEVER THE DOCUMENT IS.
   A snippet is not prose: `<h1>Alaa Abbod</h1>` in an RTL context has its
   neutral characters — the angle brackets, the slash — resolved against the
   PARAGRAPH's direction, so the closing tag reorders and the reader is shown
   markup that would not parse. Measured on `/ar/`, where this block inherited
   `direction: rtl` from the document.
   `isolate` as well as `direction`, so the snippet cannot reorder the Arabic
   around it either. Same rule, same reason, as `.scx-x` in
   `sections/home-certificate.css`. */
.cdx-code {
  direction: ltr;
  unicode-bidi: isolate;
  display: flex;
  flex-flow: column;
  gap: 0.25rem;
  width: 100%;
}

/* One snippet is visible at a time. With nothing running, that is the first —
   standing complete, which is the settled state this phase ships. */
.cdx-snip {
  display: none;
  flex-flow: column;
  gap: 0.25rem;
}

.cdx-snip:first-child {
  display: flex;
}

/* `white-space: pre` is load-bearing: these lines are indented code and they
   must neither wrap nor collapse. The transparent right border is the caret's
   reserved 1px — stage 10 colours it rather than adding it, so no line moves
   when the caret lands. */
.cdx-line {
  display: block;
  overflow: hidden;
  width: auto;
  /* Physical, and correct on every document: the block above declares its own
     `direction: ltr`, so the caret's reserved column is the end of the line in
     all three locales. */
  border-right: 1px solid transparent;
  color: var(--color--green-off-white-1);
  font-family: var(--font-label);
  font-size: var(--fs-cap);
  line-height: 1.5;
  white-space: pre;
}

/* ── the typing state ────────────────────────────────────────────────────
   `.is-typing` is written on `[data-cdx]` by the tile's controller, and ONLY
   while a chain is actually running. Everything above this block is the settled
   state — the first snippet standing fully written at its natural width — and
   these three rules are what a live chain needs on top of it.

   The Golden Master gates the same three on `.hfx-js`, a class an inline script
   writes on `<html>`. That is a page-wide flag for a section-local fact, and it
   inverts the safe default: a visitor whose script never arrives gets the
   unfinished pose unless the class happens to be absent. Here the settled state
   is what the stylesheet says and the controller opts INTO the other one, on the
   one element it owns.

   `width: 0` matters for exactly one frame — the one between the controller
   claiming the panel and its first write — but that frame is a flash of the
   whole snippet at full width, which is the thing the animation exists to avoid.

   `.is-caret` is solid while characters are landing and blinks once they rest,
   the way a real caret behaves. The 1px transparent right border it colours is
   already reserved on `.cdx-line`, so nothing moves when the caret arrives. */
[data-cdx].is-typing .cdx-snip {
  display: none;
}

[data-cdx].is-typing .cdx-snip.is-on {
  display: flex;
}

[data-cdx].is-typing .cdx-line {
  width: 0;
}

.cdx-line.is-caret {
  border-right-color: var(--color--lime);
  animation: cdx-caret 1s step-end infinite;
}

@keyframes cdx-caret {
  0%,
  60% {
    border-right-color: var(--color--lime);
  }
  61%,
  100% {
    border-right-color: transparent;
  }
}

/* Under `reduce` the panel still opens — the wipe and the snippet ARE the
   section — but nothing types: the controller never starts a chain, so
   `.is-typing` is never written and the settled rules above already win. The
   caret's blink is silenced here because a `@keyframes` on an element the
   controller did not claim would otherwise be the one thing still moving. */
@media (prefers-reduced-motion: reduce) {
  .cdx-line.is-caret {
    animation: none;
  }
}

/* Four token colours, one per lexical role. `<i>` is the tag the Golden Master
   used; the italic is cancelled rather than the element renamed, because the
   snippets are transcribed verbatim. */
.cdx-k {
  color: var(--color--lime);
  font-style: normal;
}

.cdx-t {
  color: var(--color--lime-off);
  font-style: normal;
}

.cdx-s {
  color: var(--color--grey-on-track);
  font-style: normal;
}

.cdx-m {
  color: var(--color--dark-green-tint-2);
  font-style: normal;
}

@media screen and (max-width: 991px) {
  .helmet-grid {
    grid-template-columns: 1fr 1fr;
  }

  .helmet-grid-item-img-helmet {
    max-width: 20rem;
  }

  .helmet-grid-item-text-w {
    gap: calc(var(--gap) * 0.5);
  }

  .helmet-grid-extender {
    height: 4rem;
    left: -15rem;
  }
}

/* PHONE: THE EDITOR HAS TO FIT THE CARD, NOT THE OTHER WAY ROUND.
   Owner, 2026-08-19: "the code has to be much smaller, and the language name
   smaller too."

   This matters more here than anywhere else on the site, because `.cdx-line` is
   `white-space: pre`: a line that does not fit does not wrap and does not
   ellipse — it runs out of the panel and is cut by `overflow: hidden`, taking
   the end of the snippet with it. The longest line in the set is 23 characters,
   which at the desktop size is ~136px inside the ~155px a two-column grid gives
   a 360px phone. It fits, with nothing to spare and nothing that reads as
   deliberate.

   The three sizes below are BELOW the page type scale on purpose, and are the
   only three places on the site that are. `--fs-micro` is the floor of the
   system — the smallest any prose or label may reach — and these are neither:
   they are a facsimile of editor chrome inside a 180px card, and they read as
   texture first and text second, the way real code in a screenshot does. Written
   as their own clamps so the ramp is legible here rather than inherited from a
   token whose contract they are deliberately outside.

   The Golden Master's 991 step for `.cdx-abbr` and `.cdx-line` is not
   reproduced: it restates the two sizes the base rules already carry. */
@media screen and (max-width: 767px) {
  .cdx-mark {
    gap: 0.25rem;
    padding: 0.5rem;
  }

  /* h2 -> h4: a 21% cut, and the mark is still the largest thing on the card. */
  .cdx-abbr {
    font-size: var(--fs-h4);
    letter-spacing: -0.01em;
  }

  .cdx-full {
    font-size: clamp(8px, calc(6.9px + 0.3vw), 9.2px);
    line-height: 1.25;
    letter-spacing: 0.04em;
  }

  .cdx-panel {
    gap: 0.35rem;
    padding: 0.6rem;
  }

  .cdx-file {
    font-size: clamp(8px, calc(6.9px + 0.3vw), 9.2px);
    letter-spacing: 0.03em;
  }

  .cdx-code {
    gap: 0.12rem;
  }

  .cdx-snip {
    gap: 0.12rem;
  }

  .cdx-line {
    font-size: clamp(8.6px, calc(7.4px + 0.33vw), 10.2px);
    line-height: 1.35;
  }
}

@media screen and (max-width: 479px) {
  /* NO PHONE OVERRIDE FOR THE CONTAINER'S TOP PAD. It used to drop back to one
     `--gap` here, which is the same 17px crammed head this section was fixed for
     at desktop (P8-08) — only worse, because a phone's dark chapter opens
     directly under a full-bleed photograph with no side margin to soften it.
     `--section-y` already carries its own phone value (52px at the floor), so
     the rhythm scales without a second rule to keep in step. */

  .helmet-grid-frame-w.destop {
    display: none;
  }

  .helmet-grid-frame-w.mobile {
    display: flex;
  }

  .helmet-grid-item {
    --mask-url: url('/assets/svg/stack-grid/helmet-mask-mobile-fill.svg');
  }

  .helmet-grid-item-img-helmet {
    max-width: 12rem;
  }

  .helmet-grid-item-text-w {
    gap: calc(var(--gap) * 0.25);
    justify-content: flex-end;
    align-items: flex-end;
  }

  .helmet-grid-item-date-w {
    margin-inline-start: 0.4rem;
  }

  .helmet-grid-extender {
    bottom: 1%;
    left: -5rem;
    width: auto;
    height: 1.56rem;
  }
}
}

/* ---- sections · sections/home-certificate.css ---- */
@layer sections {
/* ============================================================================
   S07 — THE CREDENTIAL DECK (`scx-`). Six cards that deal themselves: five
   courses and the credential they add up to.

   ─────────────────────────────────────────────────────────────────────────
   WHAT THE SECTION IS NOW, AND WHAT THAT COST THE OLD FILE (P9-02, P9-03)

   A physical deck. One card faces the reader; three fainter ones fan out
   behind it at uneven angles; a fourth waits behind THOSE at zero opacity;
   one sits below the stack, blurred and dim and cut off by the stage's own
   edge. Every ~2.2s the top card is DISCARDED DOWNWARD — left drift,
   clockwise rotation, shrinking, blurring, fading — and the card that was
   leaning behind it is uncovered, straightens out of its lean and settles
   dead centre. Behind all six, a very broad, very soft bloom in the active
   card's own colour, crossfading to the next one's.

   P9-01 was the same six cards under a PINNED stage, positioned by
   `--scx-p` (0 → 1) off a `100svh + 5 * 62vh` runway. That whole apparatus is
   gone — the runway, the pin, the progress property, the six-mark rail, the
   `translateZ` depth model and the perspective it needed. A rail is a claim
   about how far through something you are, and a loop has no "through".

   ─────────────────────────────────────────────────────────────────────────
   ROLES ARE THE INTERFACE. THERE IS NO PROGRESS NUMBER.

   The controller writes ONE attribute per card, `data-scx-r`, and this file
   turns it into a pose. Six roles for six cards, rotating modulo six:

       active   the card facing the reader                z 4
       next-1   leaning counter-clockwise behind it       z 3
       next-2   fanned the other way, further back        z 2
       next-3   fanned back again, faintest               z 1
       next-4   the STAGING slot — deeper still, and at   z 0
                zero opacity. Where the wrap lands.
       prev     parked below the deck, blurred, clipped   z 5

   `prev` sits ABOVE `active` deliberately, and that is the whole answer to
   z-index popping. The falling card must stay over the card it is uncovering
   for the entire deal; once parked it overlaps nothing, so the ordering it
   keeps costs nothing. Every other role change moves a card one step up a
   ladder it was already on. No pair ever swaps.

   THE POSES ARE DECLARED ONCE, AS VARIABLES, and are read in two places: the
   role rules below and the one keyframe pair. A pose that existed twice as a
   literal would drift the first time somebody tuned the fan.

   ─────────────────────────────────────────────────────────────────────────
   THE TRANSITION IS THE DESTINATION'S, NOT THE SOURCE'S

   CSS reads `transition` off the AFTER-change style, so declaring it on each
   role rule means "however a card gets here, this is how it arrives":

       → active   600ms settle with one small overshoot, plus a fast opacity
                  ramp so the card behind is OCCLUDED rather than blended
       → prev     600ms fall on a hard ease-in — gravity taking the card
       → next-*   440ms reposition on a 40/60/80/100ms stagger, so the back
                  of the deck resettles rather than teleporting

   TWO ELEMENTS CARRY ONE MOTION, and that is not decoration. `.scx-card-w`
   translates and `.scx-card` inside it rotates and scales, because the
   reference's fall does NOT move all four terms together: at the halfway
   point the card has already turned ~5deg and lost ~7% of its size while it
   has only fallen ~55% of the way. One transform on one element cannot say
   that; two elements with two curves can, and it costs no extra DOM — the
   wrapper and the card were both already there.

   ─────────────────────────────────────────────────────────────────────────
   THERE IS `filter: blur()` HERE, AND THAT REVERSES A NOTE THIS FILE CARRIED

   P9-01 said no blur, citing `sections/home-disciplines.css`: an earlier deck
   "died on `backdrop-blur` over 80vh cards". That measurement stands and does
   not apply. It was BACKDROP blur — a read of everything painted behind an
   80vh element, re-read every frame. This is `filter: blur()` on the element's
   own raster, on a box at most 352 x 528, on six of them, five holding a
   CONSTANT radius between deals. Depth here is optical: the parked card is out
   of the focal plane and has to look it. What is not allowed is what was never
   allowed — a blur that grows every frame under a scrub.

   ─────────────────────────────────────────────────────────────────────────
   TWO POSES, ONE GATE — the same contract, two fewer conditions

   Everything above the `[data-scx-live]` block is the BASE POSE: a plain
   vertical column of six complete cards, the credential last. It is what
   ships in the markup, what a visitor with JavaScript off gets, and what
   `prefers-reduced-motion: reduce` gets. It is not a fallback in the
   apologetic sense — it is the section, and the deal is an enhancement over
   it.

   P9-01's gate also demanded `min-width: 992px` and `min-height: 620px`,
   because a pinned 100svh stage needs both. Nothing is pinned and the deck is
   sized in `clamp()`, so the same choreography runs at 390 as at 1920 and
   `reduce` is the only condition left. `[data-scx-live]` is written by
   `runtime/sections/home-certificate.js` only when the motion runtime is
   enabled, and `reset()` removes it before it clears anything else — the same
   contract as `[data-pjx-live]` on the projects rail.
   ========================================================================== */

.scx-s {
  /* The two rules the Golden Master writes as `rgba(4, 5, 4, …)` — the ink at
     12% for a box edge, at 10% for a hairline inside a card. Derived from the
     token rather than transcribed, so a palette change carries. */
  --scx-edge: color-mix(in srgb, var(--color--dark-green) 12%, transparent);
  --scx-rule: color-mix(in srgb, var(--color--dark-green) 10%, transparent);

  /* ------------------------------------------------------------------ ink

     THE COURSE CARD IS CHARCOAL, NOT THE SITE'S BLACK, and the two literals
     below are the only ones in this file. `--color--dark-green` is #040504:
     an almost pure black with a green cast, correct for a full-bleed slab and
     wrong for a small object that has to read as a MATERIAL. A card at #040504
     on #edf5f3 is a hole in the page; a card that runs #0d0f11 → #17191b has a
     surface, catches its own top edge and takes a shadow. Neutral rather than
     green on purpose — the five accents below are the only colour a card is
     allowed, and a green ground would tint every one of them. */
  --scx-card-lo: #0d0f11;
  --scx-card-hi: #17191b;
  --scx-card-ink: var(--color--white);

  /* AND THE PLATE IS A WELL IN IT. Derived rather than a third literal: the
     card's own low tone taken back towards the site's black. The plate has to
     be visibly RECESSED from the card, because on a card this dark a plate at
     the same value is not a plate — it is a rectangle that disappears, which is
     exactly what the first pass of this composition shipped. */
  --scx-plate-lo: color-mix(in srgb, var(--scx-card-lo) 68%, var(--color--dark-green));

  /* ---------------------------------------------------------- the accents

     ONE COLOUR PER COURSE, CHOSEN RATHER THAN COMPUTED. The bloom behind the
     deck carries the active card's own tone and crossfades to the next one's,
     so these five ARE the section's colour arc: cool at the fundamentals,
     warming through practice and responsibility, and landing one step from the
     credential's gold at the fifth — which is the card the reader is looking
     at when their eye drops to the gold panel underneath.

     They are named here rather than sampled from the plates for two reasons:
     the plates are still pending (see `home-certificate-cards.html`), and a
     colour derived per frame from an image is a colour nobody decided. When
     the photographs land, these stay — they are a decision, not a fallback. */
  --scx-tone-1: #6fd3bd; /* fundamentals — the site's own mint, calmed */
  --scx-tone-2: #8ab6e8; /* productivity — cool, instrumental */
  --scx-tone-3: #c9a2e6; /* prompting — the craft card */
  --scx-tone-4: #e2a98a; /* responsibility — the human tone */
  --scx-tone-5: #d8c86a; /* staying current — one step from the gold below */

  /* AND THE SIXTH IS THE CREDENTIAL'S OWN GOLD (P9-03), which is why the fifth
     course was tuned to sit one step short of it: the arc now LANDS somewhere.
     The site's gold token rather than a sixth literal — the card's seal, its
     verify pill and the bloom behind it are then provably the same colour, and
     a palette change carries to all three. */
  --scx-tone-6: var(--color--gold);

  /* --------------------------------------------------------- the geometry

     ONE WIDTH DECIDES EVERY OTHER NUMBER. Height, plate, padding, rounding
     and type all derive from `--scx-cw`, so the card is one object at every
     size instead of a set of independently-tuned breakpoint poses.

     The ratio is 1 : 1.5, and the plate is DERIVED from it rather than tuned.
     The plate is as wide as the card's content box — the card minus its own
     6.2% padding twice — and as tall as that over 3/2, which is a fixed height:
     every card's plate is then identical, whatever its title does.

     3:2 BECAUSE THE PHOTOGRAPHS ARE (P10). It was 0.44 of the card height,
     which landed within a percent of 4:3 — the ratio the plates were SPECIFIED
     at while they were still pending. The five that arrived are 1536x1024, so
     the specification moved and the plate moved with it. Written as the algebra
     rather than as a multiplier so the ratio is the number a reader sees: it is
     `--scx-ch * 0.38933` in the old form, and in this one re-tuning `--scx-pad`
     cannot silently re-crop six photographs.

     WHAT THIS FREED. The plate loses 0.076 of a card WIDTH — 24px at 1440, 27px
     at 1920, 23px at the 300px floor — and every pixel of it goes to
     `.scx-card-title`, the one `flex: 1 1 auto` band in the card. The card's own
     box does not move: it is `aspect-ratio: 1 / 1.5` and nothing inside it is
     allowed to change that. Six plates got shorter and no card got taller, which
     is the whole reason the ratio could be changed at all.

     P9-03 TOOK A QUARTER OFF THE WIDTH — `clamp(300px, 29vw, 452px)` put a
     626px card inside an 826px stage at 1440x900, a section 92% of the viewport
     tall in which the deck was the page rather than a thing on it. `22vw` lands
     317 x 475, which is 53%. Nothing else here changed to accommodate it: the
     fan, the plate, the type and the stage all derive from this one number.

     THE 300px FLOOR IS MEASURED, NOT ROUND, and lowering it is how that change
     first went wrong. Below about 296px a German or Arabic course card's
     content exceeds the 1 : 1.5 box, `aspect-ratio` yields to min-content, and
     ONE card grows taller than the other five — a deck whose cards are not the
     same size is a pile. At 258px, /de/ grew card 2 by 13px and /ar/ grew three
     of six. The floor only binds between 768 and 1364; wider is on the vw
     term. */
  --scx-cw: clamp(300px, 22vw, 352px);
  --scx-ch: calc(var(--scx-cw) * 1.5);
  --scx-pad: calc(var(--scx-cw) * 0.062);
  --scx-round: clamp(20px, calc(var(--scx-cw) * 0.07), 32px);
  --scx-plate-w: calc(var(--scx-cw) - var(--scx-pad) * 2);
  --scx-plate-h: calc(var(--scx-plate-w) / 1.5);

  /* The stage's own box, in card heights: the card, headroom above it for the
     fan, and a tail below deep enough to show the parked card's top eighth
     and cut the rest off. `--scx-lead` is the headroom; the tail is whatever
     is left of `--scx-stage-h`.

     THE HEADROOM IS DERIVED, NOT CHOSEN (P10). The tallest thing above the deck
     is `next-1`, whose painted box reaches 0.0747 of a card-height above the
     top: that is `s * (|sin r| + 1.5 * |cos r|) / 3` at scale 0.962 and -7.5deg,
     less its own 5.6% lift. Its shadow adds 4.3px — a 45px blur pulled down 18px
     reaches 4.5px past the top edge, times the scale — and its 0.5px wrapper
     blur adds 1.5 more. So the floor is `0.0747 * --scx-ch + 6px`:

         768-1363   39.6px needed   38.3px given at 0.085   1.3px SHORT
         1440       41.5px needed   40.4px given            1.1px SHORT
         >=1600     45.5px needed   44.9px given            0.6px SHORT

     The fan WAS being shaved, at every width above 767, and a pixel off the top
     of a tilted card is exactly what reads as a border — which is what the owner
     reported. 0.11 clears the floor by 11px at 1440, half the card's own
     padding: air you can see rather than a tolerance.

     THE TAIL MOVES WITH IT SO THE PARKED CARD DOES NOT. Its top edge is at
     1.09696 card-heights — 108% of its own height, less half its rotated box —
     and the slice the composition wants below it is 0.138. So
     0.11 + 1.09696 + 0.138 = 1.345, and the cut below is IDENTICAL to the one
     1.32 gave at a lead of 0.085. Not one pose value changed. */
  --scx-lead: calc(var(--scx-ch) * 0.11);
  --scx-stage-h: calc(var(--scx-ch) * 1.345);

  /* ------------------------------------------------------------ the poses

     Percentages are of the card's OWN box, so every one of these is
     size-independent. Translation is the wrapper's; rotation and scale are
     the card's; opacity and blur are the wrapper's. */

  /* active — the card facing the reader. Written out rather than assumed,
     because the recycle keyframes and the base pose both need a name for it. */
  --scx-a-x: 0%;
  --scx-a-y: 0%;
  --scx-a-r: 0deg;
  --scx-a-s: 1;
  --scx-a-o: 1;
  --scx-a-b: 0px;

  /* next-1 — leaning COUNTER-CLOCKWISE, up and to the left. This is the card
     the deal uncovers, and its lean is the other half of the choreography: the
     outgoing card turns +8deg and the incoming one unwinds from -7.5deg. Two
     opposing rotations is what makes a hand-off read as a deck rather than as
     a crossfade, so the sign here is not a taste. */
  --scx-1-x: -3.4%;
  --scx-1-y: -5.6%;
  --scx-1-r: -7.5deg;
  --scx-1-s: 0.962;
  --scx-1-o: 0.2;
  --scx-1-b: 0.5px;

  /* next-2 — fanned the OTHER way. An evenly-stacked deck reads as a printing
     error; the fan has to be uneven to read as objects somebody put down. */
  --scx-2-x: 6.8%;
  --scx-2-y: -3.4%;
  --scx-2-r: 9deg;
  --scx-2-s: 0.951;
  --scx-2-o: 0.15;
  --scx-2-b: 1px;

  /* next-3 — back to the left, wider and lower, and the faintest thing on the
     stage that is still recognisably a card. Below about 0.08 it stops being a
     silhouette and becomes a smudge. */
  --scx-3-x: -9.8%;
  --scx-3-y: 2.6%;
  --scx-3-r: -11deg;
  --scx-3-s: 0.934;
  --scx-3-o: 0.1;
  --scx-3-b: 1.6px;

  /* next-4 — THE STAGING SLOT, AND IT IS INVISIBLE. Six cards fill six roles,
     and this is the one the recycling card jumps into: deeper and further left
     than `next-3`, at zero opacity, so the jump from below the deck to behind
     it happens where nothing can see it. A card that only changed opacity on
     the next deal would materialise; one that also moves 3% and 2.5deg
     arrives. */
  --scx-4-x: -12.6%;
  --scx-4-y: 5.4%;
  --scx-4-r: -13.5deg;
  --scx-4-s: 0.918;
  --scx-4-o: 0;
  --scx-4-b: 2.4px;

  /* prev — parked below the deck. NOT A REFLECTION: this is the previous card
     itself, the right way up, still reading its own title, which is why its
     rotation continues the fall's direction (+8deg) instead of mirroring it.
     108% of its own height puts its top edge about a tenth of a card below the
     active card's bottom, and the stage cuts off everything past that. */
  --scx-p-x: -6%;
  --scx-p-y: 108%;
  --scx-p-r: 8deg;
  --scx-p-s: 0.892;
  --scx-p-o: 0.24;
  --scx-p-b: 7px;

  /* sunk — a TRANSIENT pose, reachable only inside `scx-recycle-*`. The old
     previous card keeps going down and out while the new one lands in its
     slot, so the stage never shows two cards parked in the same place. */
  --scx-sk-x: -8%;
  --scx-sk-y: 152%;
  --scx-sk-r: 10.5deg;
  --scx-sk-s: 0.86;
  --scx-sk-b: 10px;

  /* ----------------------------------------------------------- the rhythm

     Measured against the reference: the deal itself is a little over half a
     second and the card then holds for about a second and a half. Faster and
     it reads as a glitch; slower and it reads as a marketing carousel. The
     controller owns the interval (`SCX.stepMs`) and this file owns the deal;
     `tests/unit/certificate.test.mjs` pins the two together, because a hold
     shorter than the deal would start a second deal on top of the first. */
  --scx-deal: 600ms;
  --scx-rear: 440ms;

  /* THE CARD LANDS FIRST AND DIMS SECOND, AND THAT IS A FIX RATHER THAN A
     FLOURISH.

     The discarded card is still over the card it is uncovering for most of its
     fall — 47% of it at 380ms, 23% at 480ms. Fading it on the deal's own clock
     puts it at 0.75 while half of it is still on top of the new card, and the
     result is a DOUBLE EXPOSURE: two titles legible in the same place, which is
     the exact failure this section measured and wrote down the first time it
     was built. `golden:certificate` measures it in PIXELS — hide the incoming
     card and diff inside the falling one — and the first pass of this file read
     19/255 through it.

     So the opacity does not move at all until the card is nearly clear. It
     holds at 1 for the first 420ms, which is the 70% mark the reference's own
     fade starts at, and then eases to its parked 0.24 over the next 480ms —
     long after the incoming card has settled. Nothing is transparent while it
     is in the way. Measured bleed after: under 6/255, which is inside the
     card's own gradient.

     A physical card does not become see-through as it falls. It lands, and
     then it is further away. */
  --scx-recede: 480ms;
  --scx-recede-in: 420ms;

  /* THE ONE THING THAT IS NOT A DEAL. The bloom is slower than the cards and
     starts after them, so the colour arrives as the new card settles rather
     than announcing it. */
  --scx-bloom: 520ms;
  --scx-bloom-in: 90ms;

  /* ---------------------------------------------------------- the curves

     THESE ARE SOLVED, NOT PICKED. The reference's deal was read off frame by
     frame as checkpoints — where the discarded card has got to at 12%, 35%,
     65% and 100% of the transition, in translation, rotation, size, opacity
     and softness — and each curve below is the cubic-bezier through its own
     four. That is why they are not a family: one ease on all five terms is
     exactly what makes a falling card read as a sliding one.

     ARRIVING. Back-loaded, so the last third of the deal is the incoming card
     settling rather than already sat there. No overshoot in the translate or
     the turn — a card that lands twice is a toy — and one small overshoot in
     the SIZE, which is the "comes forward" the reference has: from 0.962 the
     card tops out at about 1.014 around 410ms. Meant to be felt, not seen. */
  --scx-ease-settle: cubic-bezier(0.4, 0.05, 0.3, 1);
  --scx-ease-settle-size: cubic-bezier(0.32, 0.08, 0.42, 2.2);

  /* LEAVING. The translate is a hard ease-in and that IS the gravity: the first
     70ms cover 2% of the fall and the last 200ms cover half of it. The turn is
     a much straighter curve against it, which is what puts the rotation AHEAD
     of the fall — at the halfway mark the card has turned two thirds of the way
     and dropped half of it. Opacity and blur are later still: the card is
     nearly full strength while it is still over the card it is uncovering, and
     goes soft only once it is out in the open. */
  --scx-ease-fall: cubic-bezier(0.21, 0, 0.42, 0.14);
  --scx-ease-fall-turn: cubic-bezier(0.53, 0.29, 0.4, 0.44);
  --scx-ease-fade: cubic-bezier(0.4, 0, 0.5, 1);
  --scx-ease-blur: cubic-bezier(0.15, 0, 0.63, 0);
  --scx-ease-rear: cubic-bezier(0.3, 0.6, 0.3, 1);

  /* Asked by the controller, answered by the media query below. It is how the
     module finds out whether the browser actually staged the deck without
     reproducing one condition of the gate in JavaScript (INV-11). */
  --scx-staged: 0;

  position: relative;
  z-index: var(--z-base);

  /* A light section, painted explicitly so it owns its theme rather than
     inheriting whatever the chapter above left behind. */
  background-color: var(--color--white);
  color: var(--color--dark-green);

  /* the arc's own depth — content starts below it, never under it */
  padding-top: 8rem;
  padding-bottom: var(--sp-11);
}

/* THE ARC. It belongs to the black slab that closes directly above: that slab
   must always end in a dome rather than a ruled edge. It is hosted here because
   it has to paint ON the paper it dips into. A centred ellipse, so RTL needs no
   mirror, and static on purpose — scrubbing its depth left a flat, glued
   junction for as long as the section was still approaching.

   Deliberately hit-testable: stage 6's nav-ink resolves the bar's ink through
   `elementsFromPoint`, which skips `pointer-events: none`. Left transparent to
   that walk, it falls through to the paper underneath and the brand mark stays
   dark ink — on black. It covers nothing but this section's own top padding. */
.scx-visor {
  position: absolute;
  inset: -1px 0 auto;
  z-index: var(--z-raise);
  height: 8rem;
  background-color: var(--color--black);
  clip-path: ellipse(70% 100% at 50% 0%);
}

/* The head's own air. The stock 20rem/8rem spacer pair cut 60% at the owner's
   ask, scoped so no other section moves. The arc is NOT part of this
   measurement — it has its own padding above. */
.scx-head-top {
  height: 8rem;
}

.scx-head-gap {
  height: 4.5rem;
}

/* The head stays on top so selection and hit-testing there always reach the
   words, whatever the deck below is doing with its own stacking. */
.scx-s > .c {
  position: relative;
  z-index: var(--z-raise);
}

/* The centred head's label colour is tuned for the three DARK heads that share
   `.ab-mid-brow`; #b9bbad reads 1.76:1 on this paper, #535450 reads 6.89:1. */
.scx-s .ab-mid-brow {
  color: var(--color--dark-green-tint-2);
}

/* THE SITE'S OWN GREEN, ON THE OWNER'S INSTRUCTION, AND IT IS A KNOWN FAILURE.
   `.scx-s .c-lime-off` used to resolve to `--color--lime-ink-lg` (#149a6b), the
   token that exists for exactly this case: the accent, darkened until it is
   legible on paper. It measured 3.23:1 against this section's #edf5f3, over the
   3:1 large-text bar.

   The owner asked for the accent itself — the #21f1a8 every other section uses —
   and was shown the measurement first: 1.33:1 here, against 13.57:1 on the ink
   the four identical headings above and below this one sit on. There is no
   intermediate worth having; solving the contrast equation for this ground caps
   the accent at #0aa16c, which is indistinguishable from the token that was
   already shipping. So this is not a tuning, it is a decision to ship a heading
   that fails a measurement the site otherwise passes everywhere, taken in the
   knowledge of the number.

   WHAT IT COSTS, RECORDED HERE BECAUSE IT IS PAID ELSEWHERE:
   `contracts/published-measurements.json` no longer says zero violations, the
   copy on `/disciplines` that states that figure was rewritten in all three
   locales, and `tools/golden/site-a11y.mjs` carries the one named allowance.

   The override is deleted rather than re-pointed: with nothing here,
   `base/typography.css` applies and the section uses the same token as the rest
   of the site, which is the whole of what was asked for. */

/* -- the stage ------------------------------------------------------------ */

/* THE WRAP IS THE PAGE'S GUARD ON ONE AXIS AND NOTHING ON THE OTHER, AND THAT
   SPLIT IS THE WHOLE FIX (P10).

   A card rotated 11deg out of the fan reaches past the deck on both sides, and
   at 390 that is past the viewport — so `clip` on X stays, and "no horizontal
   scrollbar" stays a property of the box rather than of the numbers in the fan.
   Clipping here rather than on the section is also what keeps the visor's 1px
   of overhang.

   `clip` ON Y WAS COLLATERAL, AND IT WAS CUTTING THE LIGHT. The bloom is 1.45
   card-heights tall inside a wrap that is 1.345, so it overhangs 25px top and
   bottom before `filter: blur(34px)` adds another 100 — and clipping an already
   blurred raster puts a RULED EDGE through light, which is the one thing that
   gives away that a bloom is an element. Measured at 1440 before this change: a
   hard line 31px above and below the stage. That is the "the background looks
   cut off at the edges" the owner reported.

   `overflow-x: clip` paired with `overflow-y: visible` is the one combination
   that clips one axis WITHOUT creating a scroll container at all — `hidden` on
   one axis forces `auto` on the other, `clip` does not. Same reasoning, and the
   same measurement, as `main.main-w` in `layout/container.css`.

   THE STAGE STILL CLIPS, ON BOTH AXES, and it is now the only clip in the
   section that does: it is the edge the parked card is deliberately cut by. Two
   clips, two jobs, and after this they are actually two rules. */
.scx-wrap {
  position: relative;
  overflow-x: clip;
  overflow-y: visible;
}

/* In the base pose the stage is doing `.c`'s job: the column sits inside the
   page gutter like any other content, landscape notch insets included.
   Physical left/right on purpose, matching `--sa-l` / `--sa-r`: the insets are
   physical, so a logical property here would double-flip them under RTL.
   `env()` is 0px on every device without a cutout, so this is inert
   everywhere else. */
.scx-stage {
  width: 100%;
  max-width: var(--fluid-container);
  margin-inline: auto;
  padding-left: calc(var(--padding--container) + var(--sa-l));
  padding-right: calc(var(--padding--container) + var(--sa-r));
}

.scx-deck {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-9);
  margin: 0;
  padding: 0;
  list-style: none;
}

.scx-card-w {
  position: relative;
  width: min(100%, var(--scx-cw));
}

/* -- one card ------------------------------------------------------------- */

.scx-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: calc(var(--scx-pad) * 0.72);
  width: 100%;

  /* ONE RATIO, EVERY CARD, EVERY LOCALE. The reader is looking at a deck, and
     a deck whose cards are not the same size is a pile. Nothing inside is
     allowed to change this box: the plate is a fixed fraction of it, the blurb
     reserves three lines whether it needs them or not, and the title takes
     whatever is left. */
  aspect-ratio: 1 / 1.5;
  padding: var(--scx-pad);

  /* The surface. A vertical lift rather than a flat fill, so the card catches
     its own top edge — the difference between an object and a rectangle. */
  background-image: linear-gradient(
    170deg,
    var(--scx-card-hi) 0%,
    var(--scx-card-lo) 62%,
    var(--scx-card-lo) 100%
  );
  border: 1px solid color-mix(in srgb, var(--scx-card-ink) 5%, transparent);
  border-radius: var(--scx-round);
  overflow: clip;

  /* Wide and soft, never a hard drop. Two stops: the ambient one that puts the
     card in the room, and a tight one directly under it that puts it on the
     paper. Static — a shadow interpolated per frame is a repaint per frame,
     and the depth in this deck comes from blur and scale. */
  box-shadow:
    0 18px 45px color-mix(in srgb, var(--color--dark-green) 16%, transparent),
    0 4px 12px color-mix(in srgb, var(--color--dark-green) 9%, transparent),
    inset 0 1px 0 color-mix(in srgb, var(--scx-card-ink) 7%, transparent);
}

/* -- the card's top row --------------------------------------------------- */

.scx-card-head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
}

/* The course's place in the sequence. `aria-hidden` because the `<ol>` says it
   already, and small because the card's subject is its title. */
.scx-card-ix {
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  line-height: var(--lh-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: color-mix(in srgb, var(--scx-card-ink) 42%, transparent);
}

/* THE MARK IN THE TOP-RIGHT IS NOT A CONTROL, and the markup says so at
   length. It is the shape the composition needs in that corner — the reference
   has one and the card is unbalanced without it — carrying no href, no
   tabindex and no pointer. Kept light and small on purpose: at 10% of the
   card's width it reads as a detail rather than as a call to action, which is
   the correct weight for something the reader cannot press. */
.scx-card-go {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--scx-cw) * 0.1);
  aspect-ratio: 1;
  border-radius: 999px;
  background-color: color-mix(in srgb, var(--scx-card-ink) 11%, transparent);
  color: color-mix(in srgb, var(--scx-card-ink) 74%, transparent);
}

/* THE PAIR IN THE TOP-RIGHT CORNER. `space-between` on the head row puts the
   counter at one end and this group at the other, whether it holds one mark or
   two — five course cards swap the chevron for the button, and the credential
   keeps its seal and gains the button beside it. */
.scx-card-marks {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: calc(var(--sp-3) * 0.75);
}

/* THE BUTTON DOES NOT EXIST UNTIL THE DECK DEALS. `display` rather than
   `disabled` or `aria-disabled`: those describe a control that is present and
   unavailable, and under `reduce`, with no JavaScript, or after a failed
   install this one is not present — the deck is a static column and there is no
   beat to stop. The pair that turns it on lives inside the one media query the
   live pose is already behind, so no JavaScript restates the gate (INV-11). */
.scx-card-go--pause {
  display: none;
  position: relative;
  /* The card tree is `pointer-events: none` so a press lands on the stage; the
     one control in it has to take the pointer back. Only the ACTIVE card's
     wrapper is hit-testable at all, so only the front button is ever reachable
     — which is the same thing the controller's `tabindex` writing says about
     the keyboard. */
  pointer-events: auto;
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  cursor: pointer;
}

/* A 44px TARGET AROUND A 31px CIRCLE. The circle is 10% of the card's width by
   design and must not grow — it reads as a detail, which is the correct weight
   for the sixth-most-important thing on the card. So the TARGET grows instead,
   as a transparent box centred on it. Declared in px and never in rem: a target
   size is a physical measurement and must not track a root font that steps
   2.31px across the 480 boundary, which is the rule `.scx-pill--verify`'s
   `min-height` already follows. Measured clear of `.scx-card`'s own
   `overflow: clip` at every width: at the 300px floor the box spans x 244-288
   inside a 300px card, and at 390 it spans 227-271 inside 280.8. */
.scx-card-go--pause::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

/* One glyph at a time, and the play triangle is FILLED where every other mark
   in this section is stroked: a stroked triangle at 14px reads as a chevron. */
.scx-ico-play {
  display: none;
  fill: currentcolor;
  stroke: none;
}

[data-scx-paused] .scx-ico-pause {
  display: none;
}

[data-scx-paused] .scx-ico-play {
  display: block;
}

.scx-card-go--pause:hover {
  background-color: color-mix(in srgb, var(--scx-card-ink) 18%, transparent);
}

/* The site's focus ring is mint on paper; this control sits on a charcoal card,
   so it names its own — the same correction `.scx-pill--verify` makes one card
   later. */
.scx-card-go--pause:focus-visible {
  outline: 2px solid var(--color--white);
  outline-offset: 3px;
}

.scx-go-mark {
  width: 46%;
  height: 46%;
  stroke: currentcolor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
}

/* -- the title ------------------------------------------------------------ */

/* The one flexible band in the card, and the reason the plate can be fixed:
   whatever the title does — one line in English, three in Arabic — it does it
   inside a box whose size is the card minus everything else. `align-content`
   keeps it optically centred in that box rather than hanging off the top. */
.scx-card-title {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  align-content: center;
  gap: 0.55em;
  margin: 0;
  text-align: center;
  font-family: var(--font-body);
  font-size: clamp(17px, calc(var(--scx-cw) * 0.062), 28px);

  /* MEDIUM, NOT BOLD. The card is a dark object with one bright element in it
     and the title is not that element — the plate is. A 700-weight title at
     this size turns the card into a headline with a picture under it. */
  font-weight: 500;
  line-height: 1.08;
  letter-spacing: 0.012em;
  text-transform: uppercase;
  text-wrap: balance;
  color: color-mix(in srgb, var(--scx-card-ink) 90%, transparent);
}

.scx-card-read {
  display: block;
  margin: 0;
}

/* The credential's own English title, under the reading, on `/de/` and `/ar/`.
   A second line rather than a parenthesis: it is the string you check against
   the document in the panel below, so it needs to be findable, not squeezed. */
.scx-card-en {
  display: block;
  margin: 0;
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  font-weight: 500;
  line-height: var(--lh-label);
  letter-spacing: 0.05em;
  text-transform: none;
  color: color-mix(in srgb, var(--scx-card-ink) 46%, transparent);
}

/* -- the plate ------------------------------------------------------------ */

/* THE VISUAL BLOCK, and the strongest thing in the card. Its ground is painted
   rather than left transparent, so the plate is a plate before its photograph
   has decoded and still a plate if the photograph never arrives.

   A FIXED FRACTION OF THE CARD, not a flex remainder: six plates that differed
   by a title's line count would be the one thing a dealing deck cannot hide.
   The box was reserved before the photographs existed, so landing them in P10
   shifted nothing. */
.scx-plate {
  position: relative;
  flex: none;
  width: 100%;
  height: var(--scx-plate-h);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 0 var(--sp-4) clamp(9px, 4.5%, 18px);
  border-radius: calc(var(--scx-round) * 0.68);
  overflow: clip;

  /* THE GROUND UNDER THE PHOTOGRAPH, and it is still painted now that there is
     one: a well in the card, tinted by the course's own accent and lit from the
     corner the card's own highlight comes from. The image goes OVER it rather
     than instead of it, so a slow decode — or a decode that never finishes —
     shows a plate rather than a hole. */
  background-color: color-mix(in srgb, var(--scx-accent, transparent) 9%, var(--scx-plate-lo));
  background-image:
    radial-gradient(
      116% 90% at 76% 2%,
      color-mix(in srgb, var(--scx-accent, transparent) 42%, transparent) 0%,
      transparent 64%
    ),
    linear-gradient(
      158deg,
      color-mix(in srgb, var(--scx-card-ink) 11%, transparent) 0%,
      transparent 44%
    );

  /* The hairline is what makes the recess read as an edge rather than as a
     smudge, at the one value that survives being scaled to 0.93 and blurred. */
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--scx-card-ink) 9%, transparent);
}

.scx-plate-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* THE NUMERAL IS GONE (P10), AND ITS JOB WENT WITH IT.
   `.scx-plate-n` was an oversized, 7%-opacity figure cropped by the plate's own
   edge, and its stated purpose was "the thing that keeps the plate from reading
   as a missing image" — a job that ends when the image lands. Over a photograph
   it is invisible on the light passages and mud on the busy ones, competing with
   the subject to be the strongest thing in the card. Removed at the owner's
   instruction. Nothing in the accessible tree changed: it was `aria-hidden`, and
   the card's own `01 / 05` counter is what says which of five this is. */

/* THE BADGE. Real metadata, not a decoration: every one of the five is part of
   the same specialization, and that is the fact the reference's pill position
   is carrying. Dark and translucent so it reads as sitting ON the plate. */
.scx-pill {
  position: relative;
  z-index: var(--z-raise);
  max-width: 100%;
  margin: 0;
  padding: 0.52em 0.95em;
  border-radius: 999px;
  background-color: color-mix(in srgb, var(--scx-card-lo) 78%, transparent);
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  line-height: var(--lh-flat);
  letter-spacing: 0.03em;
  text-align: center;
  color: color-mix(in srgb, var(--scx-card-ink) 78%, transparent);
  backdrop-filter: blur(6px);
}

/* -- the description ------------------------------------------------------ */

/* Deliberately secondary, and RESERVED AT THREE LINES whether it needs them or
   not. A two-line blurb and a three-line blurb that pushed the plate around
   would be six cards of different anatomy, which is the one thing this
   composition cannot absorb. */
.scx-card-blurb {
  flex: none;

  /* THREE LINES RESERVED, THE TEXT SITTING ON THE FLOOR OF THEM. Reserving the
     box is what keeps six cards on one anatomy; bottom-aligning inside it is
     what stops a one-line blurb from leaving two empty lines against the card's
     bottom edge. The slack moves to the gap ABOVE the description, where a
     quiet line of type sits under a strong plate and nobody can see it. */
  display: grid;
  align-content: end;
  min-height: 4.35em;
  margin: 0;
  text-align: center;
  font-size: var(--fs-cap);
  line-height: 1.45;
  text-wrap: pretty;
  color: color-mix(in srgb, var(--scx-card-ink) 58%, transparent);
}

/* Latin values inside a row that may be RTL on another locale: isolate the bidi
   run so neutral punctuation cannot hop the boundary. */
.scx-x {
  direction: ltr;
  unicode-bidi: isolate;
}

/* -- the bloom ------------------------------------------------------------ */

/* Not rendered in the base pose: a column of six cards has no active card, so
   there is no colour for the page to be breathing. */
.scx-aura {
  display: none;
}

/* ============================================================================
   THE SIXTH CARD — THE CREDENTIAL, dealt with the other five (P9-03).

   Slot for slot the same object as the five above it — same box, same ratio,
   same charcoal ground, same rounding, same shadow, same plate height. What
   follows is either WHAT SITS IN a slot or the one colour this card is allowed
   that the others are not: gold, on its rim, its seal and its one control.

   P9-02'S PANEL IS GONE AND SO IS ITS STYLESHEET — `.scx-cred`, `.scx-side`,
   `.scx-pairs`, `.scx-verify`, the contained document mount and the 2x loupe.
   None of it survives being dealt; the loupe least of all, because a lens needs
   a still target and this card is centre stage for 1.6 seconds out of every
   13.2. `home-certificate-final.html` records what replaced each part.
   ========================================================================== */

/* A warmer rim, and nothing else about the box. It is the one cue that says
   "this is the paper the other five add up to" while the card is still
   unmistakably one of them — which is the whole brief for this card. */
.scx-card--cred {
  border-color: color-mix(in srgb, var(--color--gold) 26%, transparent);
}

/* -- the head, where the mark is a stamp ---------------------------------- */

/* THE SAME CIRCLE THE OTHER FIVE CARDS CARRY, in gold and with a seal in it.
   Same slot, same size, same weight — a reader scanning the deck sees the shape
   they already know, and only then reads it as a different mark. */
.scx-card-go--seal {
  background-color: color-mix(in srgb, var(--color--gold) 20%, transparent);
  color: var(--color--gold);
}

/* Larger inside the circle than the chevron is, because a seal is a DENSER mark
   — a notched rim, a ring and a tick where the others have two strokes — and at
   31px of circle it needs the room to stay a seal rather than a smudge. */
.scx-seal {
  width: 82%;
  height: 82%;
}

/* The notch is 0.02 of the rim's own length, which is what `pathLength="1"`
   buys: the dash pattern is a FRACTION of the circle rather than a pixel count,
   so the same rule notches identically at 31px and at 54px. */
.scx-seal-rim {
  fill: none;
  stroke: currentcolor;
  stroke-width: 1.6;
  stroke-dasharray: 0.02 0.0233;
  opacity: 0.9;
}

.scx-seal-inner {
  fill: none;
  stroke: currentcolor;
  stroke-width: 0.9;
  opacity: 0.5;
}

.scx-seal-mark {
  fill: none;
  stroke: currentcolor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* -- the plate, where the photograph is the document --------------------- */

/* THE SIXTH PLATE, AND IT NO LONGER CROPS. All six plates carry a photograph as
   of P10 and all six images are 1536x1024, so `cover` on a 3:2 plate crops
   nothing in either dimension — including this one, which is the certificate
   itself re-exported by the owner at the new ratio. The 1179x896 original it
   replaced would have lost 12.3% of its height here, taking the document's
   header and its signature line.

   The ground still goes deep and warm UNDER it rather than mint, so the moment
   before the image decodes is a gold mount rather than a mint one. */
.scx-plate--doc {
  background-color: var(--color--gold-deep);
}

/* -- the pill, where the metadata is the action -------------------------- */

/* THE SECTION'S ONE CONTROL, in the slot the course cards print `part of …`
   in. Filled gold under near-black ink: 10.1:1, and the one place
   `--color--gold` may carry type is when it is the ground rather than the ink.
   `min-height` in px, not rem: a target size is a physical measurement and
   must not track a root font that steps 2.31px across the 480 boundary. The
   shadow separates a light gold pill from the white document behind it. */
.scx-pill--verify {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55em;
  min-height: 44px;
  padding: 0.5em 1.15em;
  background-color: var(--color--gold);
  color: var(--color--dark-green);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  backdrop-filter: none;
  box-shadow: 0 0.25rem 0.9rem color-mix(in srgb, var(--color--dark-green) 32%, transparent);
  transition:
    transform var(--duration-base) var(--cubic-default),
    box-shadow var(--duration-base) var(--cubic-default);
}

.scx-pill--verify .scx-go-mark {
  width: 1.15em;
  height: 1.15em;
  stroke-width: 2.1;
}

/* Lifts and gains a warm halo rather than changing hue — one gold, and the
   press state stays legible for anyone who cannot see a colour shift. */
.scx-pill--verify:hover,
.scx-pill--verify:focus-visible {
  transform: translate3d(0, -1px, 0);
  box-shadow: 0 0.4rem 1.2rem color-mix(in srgb, var(--color--gold) 60%, transparent);
}

.scx-pill--verify:active {
  transform: translate3d(0, 0, 0);
}

/* The base focus ring is mint on paper; this control sits on a charcoal card
   over a gold pill, so it names its own. */
.scx-pill--verify:focus-visible {
  outline: 2px solid var(--color--white);
  outline-offset: 3px;
}

/* -- the foot, where the description is the record ----------------------- */

/* THE SAME RESERVED BOX AS `.scx-card-blurb`, DELIBERATELY: three rows, the
   same floor, the same low contrast, the same 4.35em. A foot of a different
   height is the one thing a dealing deck cannot hide. */
.scx-facts {
  flex: none;
  display: grid;
  align-content: end;
  gap: 0.2em;
  min-height: 4.35em;
  margin: 0;
  text-align: center;
  font-size: var(--fs-cap);

  /* 1.35 RATHER THAN THE BLURB'S 1.45, AND IT IS 4px OF HEADROOM RATHER THAN A
     TASTE. On `/ar/` the holder row carries a second line (the Latin spelling
     printed on the paper), so this band is four lines where the other cards'
     description is three — and at 360 and 1364 that made the credential card
     1px taller than the other five. A record also reads better set tight. */
  line-height: 1.35;
}

.scx-fact {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 0.6em;
  min-width: 0;
}

.scx-fk {
  flex: none;
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  line-height: var(--lh-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: color-mix(in srgb, var(--scx-card-ink) 38%, transparent);
}

/* A credential id is a fixed-width string that must not wrap onto a fourth
   line and push the plate around. It ellipsises instead, and the full value is
   in the accessible tree either way. */
.scx-fv {
  min-width: 0;
  margin: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: color-mix(in srgb, var(--scx-card-ink) 72%, transparent);
}

/* The Latin spelling under the transliterated holder, on `/ar/` only. */
.scx-fv-en {
  display: block;
  font-size: var(--fs-micro);
  line-height: var(--lh-label);
  color: color-mix(in srgb, var(--scx-card-ink) 46%, transparent);
}
}

/* ---- sections · sections/home-certificate-deck.css ---- */
@layer sections {
/* ============================================================================
   S07 — THE CREDENTIAL DECK, THE HALF THAT MOVES.

   Split out of `home-certificate.css` in P10, and the seam is not arbitrary:
   everything below is behind `prefers-reduced-motion: no-preference` or is a
   `@keyframes` that only rules inside it use. The other file is the section a
   visitor gets with no JavaScript, under `reduce`, and before the controller
   installs — the head, the card, the plate, the pill — and it owns every token
   the two share, which a card inherits either way.

   ONE SECTION, TWO FILES, AND THE REASON IS A LIMIT RATHER THAN A TASTE. INV-13
   fails a source file over 60KB, on the argument that a code file which grows
   without a seam becomes a dumping ground. This section had a seam and was over
   the line, so it was cut along it. `styles/order.json` lists both, adjacently,
   in the `sections` layer — so the cascade order is unchanged and neither file
   can apply by accident.

   `tests/unit/certificate.test.mjs` reads THIS file as text: the deal duration,
   the six role selectors, the staging slot's opacity and the recycle keyframes
   are all asserted against the controller's own constants from here.
   ========================================================================== */

/* ============================================================================
   THE LIVE POSE.

   One block, one condition. See the file header for why there is no
   reduced-motion undo below this: a `reduce` visitor never reaches a rule that
   stages anything.
   ========================================================================== */
@media screen and (prefers-reduced-motion: no-preference) {
  /* The handshake. The controller reads this one property off the stage and
     learns whether the browser staged the deck, instead of re-stating the
     query's conditions in JavaScript (INV-11). */
  [data-scx-live] .scx-stage {
    --scx-staged: 1;

    height: var(--scx-stage-h);

    /* THE DECK'S OWN VIEWPORT, and the reason the parked card is cut off
       rather than fully drawn. The composition wants an edge there. */
    overflow: clip;
  }

  /* THE SWAP. One condition — the same one the whole live pose is behind — so
     the corner's button appears exactly where, and only where, there is a beat
     to stop. See `home-certificate-cards.html` for why the chevron is not
     simply deleted: it is what a visitor with no JavaScript, a visitor under
     `reduce`, and a failed install all still see. */
  [data-scx-live] .scx-card-go--mark {
    display: none;
  }

  [data-scx-live] .scx-card-go--pause {
    display: inline-flex;
  }

  /* One box, six cards in it. The deck is exactly one card, positioned in the
     stage with headroom above for the fan; everything else happens inside it
     as a transform, so nothing here relayouts sixty times a second. */
  [data-scx-live] .scx-deck {
    position: relative;
    display: block;
    width: var(--scx-cw);
    height: var(--scx-ch);
    margin: var(--scx-lead) auto 0;
    gap: 0;
  }

  [data-scx-live] .scx-card-w {
    position: absolute;
    inset: 0;
    width: auto;

    /* The default, and the pose a card holds before the controller has said
       anything. Written as the `active` variables rather than as zeroes so the
       "no role yet" state and the `active` role are provably the same pose. */
    transform: translate3d(var(--scx-a-x), var(--scx-a-y), 0);
    opacity: var(--scx-a-o);
    filter: blur(var(--scx-a-b));
    z-index: calc(var(--z-raise) * 4);

    /* NOTHING IN THE DECK TAKES THE POINTER. There is nothing to press: the
       cards carry no link and the section's one action is in the panel below.
       Stated rather than left to chance, so a card that scrolls under the
       cursor cannot eat a click meant for the page. */
    pointer-events: none;
  }

  /* ROTATION AND SIZE ARE SEPARATE PROPERTIES, NOT ONE `transform`, and that
     is the whole reason the arrival can overshoot in size without also
     overshooting in angle. A compound transform is ONE animatable value on one
     curve: an incoming card that grows 1.4% past its resting size would, on
     that same curve, also swing 4deg past level. Splitting them is what buys
     two curves, and it costs no extra element and no extra layer. */
  [data-scx-live] .scx-card {
    rotate: var(--scx-a-r);
    scale: var(--scx-a-s);
  }

  /* Promotion is GATED by the viewport gate's class: six composited layers
     while the deck is on screen, none while it is not. A permanently promoted
     element is a permanent compositor layer. */
  [data-scx-live] [data-scx-wrap].is-live .scx-card-w {
    will-change: transform, opacity, filter;
  }

  [data-scx-live] [data-scx-wrap].is-live .scx-card {
    will-change: transform;
  }

  /* ---- the six roles --------------------------------------------------- */

  /* ACTIVE. Uncovered from behind the falling card, unwinding from its lean
     into the centre with one small overshoot. Why its opacity ramp is faster
     than its geometry is on the transition rule below, which owns the ramp. */
  [data-scx-live] .scx-card-w[data-scx-r='active'] {
    transform: translate3d(var(--scx-a-x), var(--scx-a-y), 0);
    opacity: var(--scx-a-o);
    filter: blur(var(--scx-a-b));
    z-index: calc(var(--z-raise) * 4);
  }

  [data-scx-live] .scx-card-w[data-scx-r='active'] .scx-card {
    rotate: var(--scx-a-r);
    scale: var(--scx-a-s);
  }

  /* PREV. The discard. Down, left, clockwise, smaller, softer, dimmer — and
     ABOVE the card it is uncovering for the whole fall, which is what makes
     the reveal read as one object moving off another rather than as a
     crossfade with a z-index in it. */
  [data-scx-live] .scx-card-w[data-scx-r='prev'] {
    transform: translate3d(var(--scx-p-x), var(--scx-p-y), 0);
    opacity: var(--scx-p-o);
    filter: blur(var(--scx-p-b));
    z-index: calc(var(--z-raise) * 5);
  }

  [data-scx-live] .scx-card-w[data-scx-r='prev'] .scx-card {
    rotate: var(--scx-p-r);
    scale: var(--scx-p-s);
  }

  [data-scx-live] .scx-card-w[data-scx-r='next-1'] {
    --scx-lag: 40ms;

    transform: translate3d(var(--scx-1-x), var(--scx-1-y), 0);
    opacity: var(--scx-1-o);
    filter: blur(var(--scx-1-b));
    z-index: calc(var(--z-raise) * 3);
  }

  [data-scx-live] .scx-card-w[data-scx-r='next-1'] .scx-card {
    rotate: var(--scx-1-r);
    scale: var(--scx-1-s);
  }

  [data-scx-live] .scx-card-w[data-scx-r='next-2'] {
    --scx-lag: 60ms;

    transform: translate3d(var(--scx-2-x), var(--scx-2-y), 0);
    opacity: var(--scx-2-o);
    filter: blur(var(--scx-2-b));
    z-index: calc(var(--z-raise) * 2);
  }

  [data-scx-live] .scx-card-w[data-scx-r='next-2'] .scx-card {
    rotate: var(--scx-2-r);
    scale: var(--scx-2-s);
  }

  [data-scx-live] .scx-card-w[data-scx-r='next-3'] {
    --scx-lag: 80ms;

    transform: translate3d(var(--scx-3-x), var(--scx-3-y), 0);
    opacity: var(--scx-3-o);
    filter: blur(var(--scx-3-b));
    z-index: var(--z-raise);
  }

  [data-scx-live] .scx-card-w[data-scx-r='next-3'] .scx-card {
    rotate: var(--scx-3-r);
    scale: var(--scx-3-s);
  }

  /* NEXT-4 — THE STAGING SLOT. Deeper than the fan and at zero opacity: where
     the recycling card lands after its jump, invisible so the jump is. It fades
     up into `next-3` on the following deal, and it MOVES as it appears — which
     is why this pose is offset from `next-3` rather than identical to it. */
  [data-scx-live] .scx-card-w[data-scx-r='next-4'] {
    --scx-lag: 100ms;

    transform: translate3d(var(--scx-4-x), var(--scx-4-y), 0);
    opacity: var(--scx-4-o);
    filter: blur(var(--scx-4-b));
    z-index: 0;
  }

  [data-scx-live] .scx-card-w[data-scx-r='next-4'] .scx-card {
    rotate: var(--scx-4-r);
    scale: var(--scx-4-s);
  }

  /* ---- how a card ARRIVES at its role ----------------------------------

     EVERY TRANSITION IN THIS SECTION IS BEHIND `[data-scx-run]`, and the
     controller writes that attribute one forced reflow AFTER the first roles
     land. Without the split, the deck's opening pose is itself a deal: six
     cards sitting at the default centre and then fanning out over 440ms at
     boot, a viewport and a half before anybody can see it. Arming second means
     the first pose is instant and every pose after it is animated.

     CSS reads `transition` off the AFTER-change style, so a rule keyed on the
     DESTINATION role says "however a card gets here, this is how it arrives" —
     which is exactly the sentence this choreography wants, and the reason
     there is no per-transition JavaScript anywhere in the module. */

  /* ARRIVING AT ACTIVE. The settle, with one small overshoot. The opacity ramp
     is deliberately FASTER than the geometry: opaque early means the cards
     behind it are OCCLUDED rather than blended, which is the difference
     between a deck and a smear. Measured on the first pass of this section — a
     card that crossfades over the one it is taking leaves two titles legible
     at once and the reader is looking at neither. */
  [data-scx-run] .scx-card-w[data-scx-r='active'] {
    transition:
      transform var(--scx-deal) var(--scx-ease-settle),
      opacity 420ms cubic-bezier(0.22, 0.72, 0.32, 1),
      filter 480ms cubic-bezier(0.25, 0.8, 0.35, 1);
  }

  [data-scx-run] .scx-card-w[data-scx-r='active'] .scx-card {
    transition:
      rotate var(--scx-deal) var(--scx-ease-settle),
      scale var(--scx-deal) var(--scx-ease-settle-size);
  }

  /* ARRIVING AT PREV. The discard, and the one curve in the file that is worth
     reading twice: a hard ease-in on the translate against a nearly linear one
     on the turn. That is what puts the rotation AHEAD of the fall — at the
     halfway point the card has turned about 5deg and lost about 7% of its size
     while it has only fallen 55% of the way — and it is the difference between
     a card that gravity has taken and a card that slid. */
  [data-scx-run] .scx-card-w[data-scx-r='prev'] {
    transition:
      transform var(--scx-deal) var(--scx-ease-fall),
      opacity var(--scx-recede) var(--scx-ease-fade) var(--scx-recede-in),
      filter var(--scx-deal) var(--scx-ease-blur);
  }

  [data-scx-run] .scx-card-w[data-scx-r='prev'] .scx-card {
    transition:
      rotate var(--scx-deal) var(--scx-ease-fall-turn),
      scale var(--scx-deal) var(--scx-ease-fall-turn);
  }

  /* ARRIVING ANYWHERE IN THE FAN. Shorter, softer, and STAGGERED — 40 / 60 /
     80ms, so the back of the deck resettles behind the deal instead of
     teleporting with it. Without the offsets the fan moves as one rigid object
     and the whole illusion of separate cards goes with it. */
  [data-scx-run] .scx-card-w[data-scx-r='next-1'],
  [data-scx-run] .scx-card-w[data-scx-r='next-2'],
  [data-scx-run] .scx-card-w[data-scx-r='next-3'],
  [data-scx-run] .scx-card-w[data-scx-r='next-4'] {
    transition:
      transform var(--scx-rear) var(--scx-ease-rear) var(--scx-lag),
      opacity var(--scx-rear) var(--scx-ease-rear) var(--scx-lag),
      filter var(--scx-rear) var(--scx-ease-rear) var(--scx-lag);
  }

  [data-scx-run] .scx-card-w[data-scx-r='next-1'] .scx-card,
  [data-scx-run] .scx-card-w[data-scx-r='next-2'] .scx-card,
  [data-scx-run] .scx-card-w[data-scx-r='next-3'] .scx-card,
  [data-scx-run] .scx-card-w[data-scx-r='next-4'] .scx-card {
    transition:
      rotate var(--scx-rear) var(--scx-ease-rear) var(--scx-lag),
      scale var(--scx-rear) var(--scx-ease-rear) var(--scx-lag);
  }

  /* ---- the one transient state ----------------------------------------- */

  /* THE RECYCLE. Six cards fill six roles, so the card leaving `prev` is the
     card arriving at `next-4` — a jump from below the deck to behind it, which
     is the one move in this system that cannot be a transition.

     So it is a keyframe, and the jump is made at ZERO OPACITY. The card keeps
     falling out of the parked slot for the first 46% (276ms, which is why the
     stage never shows two cards parked in the same place) and reappears at the
     back of the deck INVISIBLE, where it stays until the next deal moves it up
     into `next-3` and fades it in as an ordinary role transition. It ends
     EXACTLY on the `next-4` pose, so when the attribute is dropped there is
     nothing to jump back to.

     P9-02 ended this animation on the visible `next-3` pose and did the fade-up
     inside it, because five cards left no dark slot to land in. The sixth card
     bought one, and the reveal is a transition again.

     The role's own transition is still running underneath — the animation
     simply wins, which is the cascade doing the arbitration rather than a
     `transition: none` somewhere that a browser might apply on the wrong side
     of the same style recalc. Both end on the same value. */
  [data-scx-run] .scx-card-w[data-scx-x='recycle'] {
    animation: scx-recycle-w var(--scx-deal) linear both;
  }

  [data-scx-run] .scx-card-w[data-scx-x='recycle'] .scx-card {
    animation: scx-recycle-c var(--scx-deal) linear both;
  }

  /* ---- the bloom -------------------------------------------------------- */

  /* TWO LAYERS, ALWAYS BOTH MOUNTED, one lit. The controller writes the
     incoming course's accent onto whichever layer is dark and then swaps the
     class, so the colour arrives as a crossfade and never as a repaint of one
     element's background. That is the difference between the page breathing
     and the page blinking.

     It is INSIDE the wrap and OUTSIDE the stage on purpose: the stage clips,
     and a straight line through a bloom is the one thing that would give away
     that it is an element rather than light. */
  [data-scx-live] .scx-aura {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: var(--z-base);
    display: block;

    /* SIZED OFF THE CARD, NOT OFF THE WRAP. Filling the wrap makes the bloom a
       wash across the whole viewport at 1920 and a tight halo at 390 — the same
       stylesheet describing two different lights. Two and a half card widths
       centred on the deck is one light at every size. Physical centring on
       purpose: a `translate` of -50% is symmetric, so RTL needs no second rule. */
    width: min(112%, calc(var(--scx-cw) * 2.55));
    height: calc(var(--scx-ch) * 1.45);
    transform: translate(-50%, -50%);
    pointer-events: none;
  }

  .scx-aura-l {
    position: absolute;
    inset: 0;
    opacity: 0;

    /* Two stops, both very wide and very weak, and then blurred on top of
       that. The blur is what stops the outer stop's edge from ever being
       findable — a radial gradient you can locate is a circle, and this has to
       be light. It is STATIC: only `opacity` ever animates here, so the blurred
       raster is produced once per layer and the crossfade is compositor work. */
    background-image:
      radial-gradient(
        46% 34% at 50% 46%,
        color-mix(in srgb, var(--scx-accent, transparent) 62%, transparent) 0%,
        transparent 72%
      ),
      radial-gradient(
        84% 58% at 50% 54%,
        color-mix(in srgb, var(--scx-accent, transparent) 26%, transparent) 0%,
        transparent 78%
      );
    filter: blur(34px);
  }

  /* Armed with the cards, and for the same reason: the section's opening colour
     is a state, not an event. It is only from the second course onward that a
     colour change means anything. */
  [data-scx-run] .scx-aura-l {
    transition: opacity var(--scx-bloom) cubic-bezier(0.4, 0, 0.4, 1) var(--scx-bloom-in);
  }

  .scx-aura-l.is-on {
    opacity: 1;
  }

  /* ---- the pointer and the keyboard -------------------------------------

     THE DECK IS PRESSABLE NOW (P9-03) AND THE HOVER PAUSE IS GONE. P9-02
     stopped the beat while the pointer was inside the stage; the stage is a
     full-width band, so a cursor resting near the middle of the page froze the
     deck and it appeared to move only when the reader scrolled. The beat is
     unconditional, and a CLICK anywhere on the stage deals the next card at
     once — see `home-certificate.js`.

     ONLY THE FRONT CARD TAKES THE POINTER. Every wrapper is `pointer-events:
     none` by default, which lets a click on a card leaning at -13deg behind two
     others fall through to the stage and simply advance the deck. The front
     card takes it because it holds the section's one control on the sixth deal,
     and because a cursor over a pressable object should say so. */
  [data-scx-live] .scx-card-w[data-scx-r='active'],
  [data-scx-live] .scx-card-w[data-scx-r]:focus-within {
    pointer-events: auto;
  }

  [data-scx-live] .scx-card-w[data-scx-r='active'] .scx-card {
    cursor: pointer;
  }

  /* THE FOCUSED CARD COMES TO THE FRONT, AND THIS IS THE WHOLE KEYBOARD STORY.

     The credential card carries a real link (`.scx-pill--verify`) and the deck
     deals on a timer, which is a link that moves. Three things make it safe and
     this is the second: whichever card holds focus is pulled to the ACTIVE pose
     — square, full size, opaque, sharp, above everything including the falling
     card — so a reader who tabs in is looking at the card they are on rather
     than at a blurred rectangle behind two others. The controller supplies the
     third by stopping the beat while focus is inside the deck; the first is
     that only the front card takes the pointer at all.

     `[data-scx-r]` is in the selector so this outranks every role rule above by
     one attribute rather than by document order. */
  [data-scx-live] .scx-card-w[data-scx-r]:focus-within {
    transform: translate3d(var(--scx-a-x), var(--scx-a-y), 0);
    opacity: var(--scx-a-o);
    filter: blur(var(--scx-a-b));
    z-index: calc(var(--z-raise) * 6);
  }

  [data-scx-live] .scx-card-w[data-scx-r]:focus-within .scx-card {
    rotate: var(--scx-a-r);
    scale: var(--scx-a-s);
  }

  [data-scx-run] .scx-card-w[data-scx-r]:focus-within {
    transition:
      transform var(--scx-deal) var(--scx-ease-settle),
      opacity 420ms cubic-bezier(0.22, 0.72, 0.32, 1),
      filter 480ms cubic-bezier(0.25, 0.8, 0.35, 1);
  }

  [data-scx-run] .scx-card-w[data-scx-r]:focus-within .scx-card {
    transition:
      rotate var(--scx-deal) var(--scx-ease-settle),
      scale var(--scx-deal) var(--scx-ease-settle);
  }

  /* A recycling card mid-jump would otherwise be animated by the keyframe and
     ignore the pull above — animations beat transitions. Focus cannot arrive
     mid-deal often (the beat stops on `focusin`), but "often" is not "never".
     Cancelling the animation on the focused card hands it back to the rule. */
  [data-scx-run] .scx-card-w[data-scx-x='recycle']:focus-within,
  [data-scx-run] .scx-card-w[data-scx-x='recycle']:focus-within .scx-card {
    animation: none;
  }
}

/* THE SWAP. One condition — the same one the whole live pose is behind — so
     the button appears exactly where, and only where, there is a beat to stop.
     See `home-certificate-cards.html` for why the chevron is not simply
     deleted: it is what a visitor with no JavaScript, and a visitor under
     `reduce`, and a failed install all still see. */
/* THE RECYCLE'S TWO HALVES. Declared outside the media query because
   `@keyframes` is not conditional in any useful sense — the rules that USE them
   are inside it, which is what actually gates the behaviour.

   Every value is a pose variable, so the endpoints cannot drift from the role
   rules that share them. `46%` and `46.01%` are the jump: one hundredth of a
   percent apart is a discontinuity the interpolator cannot smooth over, which
   is exactly what is wanted. */
@keyframes scx-recycle-w {
  0% {
    transform: translate3d(var(--scx-p-x), var(--scx-p-y), 0);
    opacity: var(--scx-p-o);
    filter: blur(var(--scx-p-b));
    animation-timing-function: cubic-bezier(0.4, 0, 0.8, 0.5);
  }

  46% {
    transform: translate3d(var(--scx-sk-x), var(--scx-sk-y), 0);
    opacity: 0;
    filter: blur(var(--scx-sk-b));
  }

  46.01%,
  100% {
    transform: translate3d(var(--scx-4-x), var(--scx-4-y), 0);
    opacity: var(--scx-4-o);
    filter: blur(var(--scx-4-b));
  }
}

@keyframes scx-recycle-c {
  0% {
    rotate: var(--scx-p-r);
    scale: var(--scx-p-s);
    animation-timing-function: cubic-bezier(0.4, 0, 0.8, 0.5);
  }

  46% {
    rotate: var(--scx-sk-r);
    scale: var(--scx-sk-s);
  }

  46.01%,
  100% {
    rotate: var(--scx-4-r);
    scale: var(--scx-4-s);
  }
}

/* ============================================================================
   WIDTHS. The deck's choreography is identical at every width — the same six
   roles, the same curves, the same timings. What changes here is how much room
   the fan is allowed to take.
   ========================================================================== */

@media screen and (max-width: 991px) {
  /* The arc shallows with the page — 8rem of dome over a 768px viewport is a
     band, not an arc — and the padding tracks it so the head still clears it. */
  .scx-s {
    padding-top: 5rem;
  }

  .scx-visor {
    height: 5rem;
  }

  .scx-head-top {
    height: 5.6rem;
  }

  .scx-head-gap {
    height: 3.2rem;
  }
}

@media screen and (max-width: 767px) {
  .scx-s {
    /* A NARROWER FAN, NOT A DIFFERENT ONE. Every offset below is the desktop
       value pulled in by about a fifth and every rotation by about a quarter:
       at 390 the desktop spread puts the back of the fan under the gutter, and
       a fan you cannot see the edges of is just a blurred card. The roles, the
       curves and the rhythm are untouched. */
    --scx-cw: min(72vw, 330px);

    --scx-1-x: -2.8%;
    --scx-1-y: -4.6%;
    --scx-1-r: -5.6deg;
    --scx-2-x: 5.4%;
    --scx-2-y: -2.8%;
    --scx-2-r: 6.8deg;
    --scx-3-x: -7.6%;
    --scx-3-y: 2%;
    --scx-3-r: -8.4deg;
    --scx-4-x: -9.8%;
    --scx-4-y: 4.2%;
    --scx-4-r: -10.2deg;
    --scx-p-x: -4.6%;
    --scx-p-r: 6.2deg;
    --scx-sk-x: -6%;
    --scx-sk-r: 8deg;

    /* THE PHONE FAN NEVER REACHED ITS HEADROOM, so it does not get more of it.
       Every offset here is the desktop value pulled in by about a fifth and
       every rotation by about a quarter, so `next-1` tops out 23.6px above the
       deck at 390 against the 35.8px this lead gives — six pixels of slack
       before P10 and six after. Growing it would buy nothing and cost the
       section 10px of height on the one viewport whose recorded document-height
       row has the least room left. */
    --scx-lead: calc(var(--scx-ch) * 0.085);

    /* A shallower tail. The parked card still reads, and the section below it
       does not have to start a third of a card lower to make room. */
    --scx-stage-h: calc(var(--scx-ch) * 1.26);

    padding-top: 3.5rem;
  }

  .scx-visor {
    height: 3.5rem;
  }

  .scx-head-top {
    height: 3.2rem;
  }
}
}

/* ---- sections · sections/home-projects-ring.css ---- */
@layer sections {
/* ============================================================================
   S08 — THE PROJECTS RING (`vx-`). A pinned 3D cylinder of fourteen cards.

   HOW THE RING IS BUILT. `.vx-persp` owns the camera and nothing else — no
   transform of its own — so the reveal wrapper above it and the rotating track
   below it can never share a transform with the perspective and flatten it.
   `.vx-track` holds `preserve-3d` and one rotation. Each card is placed by
   `rotateY(i * 360/14)` and then pushed BACKWARDS by `translateZ(-36.25rem)`,
   which is the radius; with `backface-visibility: hidden` the near half of the
   ring is invisible, so the camera looks through it at a concave wall.

   RADIUS MATH. Fourteen 15rem cards on a circle need at least
   (15/2) / sin(180/14) = 33.7rem before they overlap. 36.25rem leaves air.

   SETTLED STATE. `translateZ(6.25rem) rotateY(-20deg)` — a chosen angle, not
   zero, and the value `docs/rebuild/home-rebuild-order.md` Stage 3 criterion 5
   asserts under `reduce`. Zero would present the ring's seam to the camera.
   Stage 8 lerps that angle from the wrapper's scroll progress (measured travel
   -149.6deg → -183.5deg) and writes it as an inline transform; nothing else
   about the ring moves.

   Source: `site.shared.043b62fef.css` `== AB: visual explorations ring (vx-) ==`.
   ========================================================================== */

/* The ring's chapter inverts as it spins: the page turns from paper to ink
   under it while the heading, its lede and the CTA note are pinned to the stage
   for the whole turn, so they cannot hold one static colour. `--vx-ink` and
   `--vx-ink-soft` are declared in `tokens/color.css` at their settled (paper)
   values and stage 8's painter writes both from the same number that drives the
   page, so nothing can drift between them.

   `.c-lime-off` on "projects" is untouched on purpose: the one site green holds
   on both paper and ink, so the accent word is the fixed point the inversion
   turns around. */
.s.is-home-collabs {
  position: relative;
  color: var(--vx-ink);
}

.c.is-home-collabs {
  position: relative;
  z-index: var(--z-section);
}

/* THE ROTATION MUST NOT LENGTHEN THE PAGE.
   `.vx-track` is `preserve-3d` with fourteen cards on a carousel, so once it is
   turned its BORDER BOX is the union of fourteen rotated, depth-offset planes —
   measured at 1920, 17,976px tall against a 1,080px stage. Nothing clipped it,
   so that overflow reached the document: the page was 23,342px long at the top
   and 23,527px once the ring had turned. A document whose length depends on a
   carousel's angle breaks every anchored offset below it.

   The Golden Master never shows this because it wraps the whole route in
   `.page-w { overflow: clip }`. This rebuild deliberately has no such wrapper
   (Phase 3 records the same decision for the footer's portrait), so the clip
   goes where the overflow is.

   `clip`, NOT `hidden`: `hidden` would make this a scroll container and the
   stage inside it is `position: sticky`, which is resolved against the nearest
   SCROLLING ancestor. `overflow: hidden` here would pin the ring to this box
   instead of to the viewport and the whole scene would stop working. */
.vx-s {
  overflow: clip;
}

.vx-wrap {
  position: relative;
  z-index: var(--z-base);
  /* the scroll length the rotation is scrubbed over, on top of the pinned screen */
  height: calc(100svh + 62.5rem);
}

.vx-stage {
  position: sticky;
  top: 0;
  height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: calc(var(--gap) * 2);
  /* the stage fills the viewport, so it must clear the fixed nav itself */
  padding-top: var(--on-t-nav-spacer);
  padding-bottom: var(--gap);
}

/* NO RULE FOR `.vx-head-cell`. The Golden Master spans it across two of the
   four `.grid-main` tracks, and then `.title-layout.ab-mid > *` — a centred,
   single-track head — puts every child back in column 1 at higher specificity.
   The span has never applied here. The class stays in the markup because it is
   the shared vocabulary the evidence extracts use; reproducing the declaration
   would make it apply for the first time under this project's cascade layers,
   where `sections` outranks `layout` regardless of specificity, and conjure the
   phantom second track `page-shell.css` exists to prevent. */

.vx-viewport {
  position: relative;
  flex: 1;
  min-height: 16rem;
}

/* Perspective lives alone on this layer so the reveal transition above it and
   the track rotation below it never share a transform with it. */
.vx-persp {
  position: absolute;
  inset: 0;
  perspective: 50rem;
}

.vx-track {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform: translateZ(6.25rem) rotateY(-20deg);
}

.vx-card {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 15rem;
  height: 21.25rem;
  overflow: clip;
  background-color: var(--color--green-off-white-1);
  /* The rim. #282c20 at 18% — the Golden Master's `--nav-ink` fallback ink,
     which is not a palette entry and has no token. Kept exact rather than
     rounded to `--color--surface-deep` (#232520), which is a different colour;
     see the report's shared-file request. */
  border: 1px solid rgb(40 44 32 / 0.18);
  border-radius: var(--radius--small);
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}

/* One card every 360/14 = 25.7143deg, each pushed back to the ring's radius.
   Written out rather than computed so the geometry is readable in the file that
   owns it — and so a card added or removed is a visible edit, not a silent
   change of the whole ring's spacing. */
.vx-card:nth-child(1) {
  transform: translate(-50%, -50%) rotateY(0deg) translateZ(-36.25rem);
}
.vx-card:nth-child(2) {
  transform: translate(-50%, -50%) rotateY(25.7143deg) translateZ(-36.25rem);
}
.vx-card:nth-child(3) {
  transform: translate(-50%, -50%) rotateY(51.4286deg) translateZ(-36.25rem);
}
.vx-card:nth-child(4) {
  transform: translate(-50%, -50%) rotateY(77.1429deg) translateZ(-36.25rem);
}
.vx-card:nth-child(5) {
  transform: translate(-50%, -50%) rotateY(102.8571deg) translateZ(-36.25rem);
}
.vx-card:nth-child(6) {
  transform: translate(-50%, -50%) rotateY(128.5714deg) translateZ(-36.25rem);
}
.vx-card:nth-child(7) {
  transform: translate(-50%, -50%) rotateY(154.2857deg) translateZ(-36.25rem);
}
.vx-card:nth-child(8) {
  transform: translate(-50%, -50%) rotateY(180deg) translateZ(-36.25rem);
}
.vx-card:nth-child(9) {
  transform: translate(-50%, -50%) rotateY(205.7143deg) translateZ(-36.25rem);
}
.vx-card:nth-child(10) {
  transform: translate(-50%, -50%) rotateY(231.4286deg) translateZ(-36.25rem);
}
.vx-card:nth-child(11) {
  transform: translate(-50%, -50%) rotateY(257.1429deg) translateZ(-36.25rem);
}
.vx-card:nth-child(12) {
  transform: translate(-50%, -50%) rotateY(282.8571deg) translateZ(-36.25rem);
}
.vx-card:nth-child(13) {
  transform: translate(-50%, -50%) rotateY(308.5714deg) translateZ(-36.25rem);
}
.vx-card:nth-child(14) {
  transform: translate(-50%, -50%) rotateY(334.2857deg) translateZ(-36.25rem);
}

.vx-card-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}

/* The CTA row. It sits AFTER `.vx-wrap`, in normal flow — see the note in the
   markup for why it cannot live inside the sticky stage. */
.vx-cta-w {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: calc(var(--gap) * 1.5);
  padding-block: 1rem 5rem;
}

.vx-cta-note {
  margin: 0;
  color: var(--vx-ink-soft);
}

/* `.vx-reveal` GETS NO RULE HERE, and that is the point. The Golden Master
   starts it at `opacity: 0` behind a JS-present gate class and an entrance
   observer flips it. This phase ships the settled pose: the ring and its CTA
   are simply visible. Stage 5 adds the observer and the start state together,
   in that order, so no state exists that only JavaScript can leave. */

@media screen and (max-width: 767px) {
  /* Same trick as the source: scale the PERSPECTIVE element itself. Perspective
     applies to its children in the scaled local space, so the whole ring shrinks
     as one object instead of fourteen cards drifting off their circle. */
  .vx-persp {
    transform: scale(0.75);
  }

  /* the mobile nav pills sit lower than the desktop bar */
  .vx-stage {
    padding-top: 7rem;
  }

  .vx-cta-w {
    gap: var(--gap);
  }

  /* the note is the one line here that repeats what the head already said */
  .vx-cta-note {
    display: none;
  }
}

/* Under `reduce` the rotation never runs, so the runway it was scrubbed over is
   dead scroll: the wrapper collapses to the pinned screen it already fills. The
   ring keeps the -20deg pose — the same one this file ships by default. */
@media (prefers-reduced-motion: reduce) {
  .vx-wrap {
    height: 100svh;
  }
}
}

/* ---- sections · sections/home-disciplines.css ---- */
@layer sections {
/* ============================================================================
   S09 — THE DISCIPLINES DECK (`svc-`). Four cards that stack as you scroll,
   pinned by nothing but `position: sticky`.

   THE DECK IS THE MECHANISM. `.svc-deck` is a 360vh column — four 80vh cards,
   three 10vh gaps and a 10vh tail, which is 3,240 px at a 900px viewport — and
   every `.svc-card-w` inside it sticks at `top: 10vh`. That height IS the travel
   each card gets; there is no pin-spacer to keep in sync with it and no
   transform on the wrapper, so nothing can drift out of phase with the scroll.
   Stage 8 scales and translates the card INSIDE its wrapper and nothing else.

   Flat by design: no shadows, no filters, no keyframes. The previous version of
   this deck died on `backdrop-blur` over 80vh cards.

   Source: `site.shared.043b62fef.css` `== AB: services deck (svc-) ==`.
   ========================================================================== */

.svc-s {
  position: relative;

  /* THE CHAPTER'S GROUND IS INK FOR THE WHOLE TIME THIS HEADER IS ON SCREEN.
     `visual-state/colour-chapter.js` has the page at #040504 well before the
     deck arrives and only brightens it a viewport before the deck's bottom
     edge, so at every scroll position where the "what i explore / four
     disciplines" header intersects the viewport, `document.body` is
     rgb(4, 5, 4) — measured at 1440x900 at section entry, mid and exit.

     It had no colour of its own, and the note below used to say it needed
     none. That was inherited from the Golden Master, whose `body` rests
     light-on-dark; this rebuild's `body` rests the other way up
     (`--page-ink` on `--page-bg`, dark on paper), so the same absence
     resolved the header to #040504 on #040504 — 1.00:1, the black-on-black
     the owner reported (P6-09).

     ON THE SECTION, SO IT ARRIVES BY INHERITANCE. The split rewrites each
     heading into `.line` / `.word` / `.char` wrappers, and the Golden Master's
     own stylesheet records why a class-scoped rule is not enough here: its
     engine clones every `[split-text]` heading for the reveal wipe and the
     clone carries no classes, so a rule keyed on the heading's class colours
     the original and misses the copy. This split does not clone today. The
     rule should not depend on that staying true.

     THE REAL LESSON IS UPSTREAM, and `layout/container.css` already states it:
     "any rule that paints a ground owes its subtree an ink in the same block".
     `.s-group.bg-black` obeys it. The colour CHAPTER paints a ground too, from
     a runtime writer, and owes the same debt — which is what
     `golden:projects` now enforces by measuring the CONTRAST of every string in
     this deck against the ground actually painted at that moment, rather than
     by asserting that some rule exists. */
  color: var(--color--white);
}

/* NO RULE FOR `.svc-head-cell`, for the same reason `.vx-head-cell` has none:
   the Golden Master spans it across two of `.grid-main`'s four tracks and
   `.title-layout.ab-mid > *` overrides that at higher specificity, so the span
   has never applied. Reproducing it here would make it apply for the first time
   — `sections` outranks `layout` for normal declarations regardless of
   specificity — and conjure a phantom second track in a one-column grid.

   THE HEADER BLOCK DOES NEED AN INK OVERRIDE, and it is on `.svc-s` above.
   This paragraph used to say the opposite — "the heading reads on the body's
   own light-on-dark" — which is a true sentence about the GOLDEN MASTER's
   `body` and a false one about this rebuild's, where the resting ink is dark on
   paper. It is the whole of P6-09. */

.svc-deck {
  /* ── THE PIN FRACTION, DECLARED ONCE ──────────────────────────────────
     Where a card sticks, as a fraction of the viewport. It is a CUSTOM
     PROPERTY rather than a literal because TWO systems need the same number:
     `top` below, and the controller, whose card travel is
     `vh * (1 - pinFrac)` — a card's shrink has to finish exactly as the next
     one lands on the pin line, so a disagreement of a hundredth is a deck
     permanently out of phase with itself.

     The Golden Master holds it twice, in two files:
     `pinFrac = window.innerWidth >= 992 ? 0.1 : 0.05` in JavaScript, mirroring
     `.svc-card-w { top: 10vh }` in CSS. INV-11 is the same rule for
     breakpoints and this is the same failure — two encodings of one decision,
     free to drift. The controller reads this property; nothing else knows the
     number. */
  --svc-pin-frac: 0.1;

  position: relative;
  /* z:0 on a positioned box makes the deck its own stacking context, so the
     card ladder below can never paint over a later section's sticky content */
  z-index: var(--z-base);
  display: flex;
  flex-direction: column;
  /* The gap and the tail are NOT the pin fraction. They happen to be 10vh at
     both breakpoints while `top` halves below 992, so tying them together would
     invent a relationship the site does not have. */
  gap: 10vh;
  padding-bottom: 10vh;
}

.svc-card-w {
  position: sticky;
  top: calc(var(--svc-pin-frac) * 100vh);
  height: 80vh;
}

/* Card N sits one step over card N-1. Sticky boxes are positioned, so tree
   order already paints them this way — the ladder is declared anyway because
   the whole effect of the deck depends on it, and a reorder must not be able to
   break it silently. The step is `--z-raise`, counted by the sibling index, so
   the ladder is expressed in the token rather than in four magic numbers and it
   stays local to the deck's own stacking context. */
.svc-card-w:nth-child(1) {
  z-index: var(--z-raise);
}
.svc-card-w:nth-child(2) {
  z-index: calc(var(--z-raise) * 2);
}
.svc-card-w:nth-child(3) {
  z-index: calc(var(--z-raise) * 3);
}
.svc-card-w:nth-child(4) {
  z-index: calc(var(--z-raise) * 4);
}

.svc-card {
  position: relative;
  display: flex;
  height: 100%;
  overflow: clip;
  background-color: var(--color--surface-deep);
  /* THE CARD OWNS ITS OWN INK. The Golden Master leaves this to inheritance and
     gets away with it because its `body` is declared dark-on-light-text
     (`background: --color--dark-green; color: --color--white`) and the chapter
     painter only ever rewrites the BACKGROUND. This rebuild's `body` rests the
     other way up — `--page-bg` is paper and `--page-ink` is near-black
     (`tokens/color.css`) — so an inherited colour would put #040504 type on this
     #232520 surface. The card is a dark surface in both, so it declares the ink
     that a dark surface needs instead of borrowing whichever one the page
     happens to be resting at. `.svc-card-body` and the eyebrow already did
     exactly this; only the title was living on the inheritance. */
  color: var(--color--white);
  /* THE RIM AND THE FILL DO THE JOB IN TURN, which is why both are kept. The
     field behind the deck is the page dot-field and its luminance moves with
     scroll, so the fill alone can never guarantee the card's edge. Over the dark
     middle of the chapter the rim carries the edge and the fill is close to the
     field; over the paper at either end the rim washes out entirely and the fill
     carries it, with contrast to spare. Neither alone survives the whole scrub. */
  border: 1px solid color-mix(in srgb, var(--color--white) 20%, transparent);
  border-radius: 1.25rem;
  transform-origin: top center;
}

.svc-card-text {
  display: flex;
  width: 40%;
  flex-direction: column;
  justify-content: flex-start;
  gap: var(--gap);
  padding: calc(var(--gap) * 2);
}

.svc-card-text .text-eyebrow {
  color: var(--color--lime);
}

/* A REAL weight, not the Golden Master's inert `font-variation-settings:
   "wght" 750`. `--font-body` is named explicitly because the base stylesheet
   puts every heading on `--font-display`, and the Golden Master renders this one
   in the body face it inherits — `.scx-title` is the heading on this page that
   genuinely asks for the display face, and it asks for it by name. */
.svc-card-title {
  font-family: var(--font-body);
  font-size: var(--fs-h2);
  font-weight: 700;
  line-height: 0.9;
  letter-spacing: -0.024em;
  text-transform: uppercase;
}

.svc-card-body {
  /* The number and the title live at the top so the card stays identifiable
     while the next one rises over it; the body anchors to the bottom edge. */
  margin-top: auto;
  max-width: 25rem;
  color: var(--color--grey-on-track);
}

.svc-card-visual {
  width: 60%;
  overflow: clip;
}

.svc-card-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The dim layer, at rest. A card that nothing is rising over is not dimmed, so
   the settled opacity is 0 — this is the finished pose, not a hidden element
   waiting for a controller. Stage 8 fades it to `0.6 * p` from the travel of
   the NEXT wrapper. `--z-raise` puts it over the card's own two columns, which
   are the only things inside the card and are not positioned at all. */
.svc-card-dim {
  position: absolute;
  inset: 0;
  z-index: var(--z-raise);
  background-color: var(--color--dark-green);
  border-radius: inherit;
  opacity: 0;
  pointer-events: none;
}

/* `.svc-reveal` GETS NO RULE HERE. The Golden Master starts it at `opacity: 0`
   behind a JS-present gate class, and an entrance observer flips it. This phase
   ships the settled pose: the cards are visible. Stage 5 adds the observer and
   the start state together, in that order, so no state exists that only
   JavaScript can leave. */

@media screen and (max-width: 991px) {
  /* Half the pin below the boundary. Stated ON THE DECK, so `top` and the
     controller both move together — see `--svc-pin-frac` above. */
  .svc-deck {
    --svc-pin-frac: 0.05;
  }

  .svc-card-w {
    /* NO FLOOR HEIGHT HERE, deliberately. `column-reverse` puts the flex line's
       START at the BOTTOM of the card, so with the default justify-content the
       surplus between the content and a fixed floor pooled at the TOP — the band
       of empty surface above the image the owner reported. Letting the card take
       its content height removes the surplus rather than redistributing it, and
       it shortens a very tall mobile section as a side effect. */
    height: auto;
  }

  .svc-card {
    flex-direction: column-reverse;
  }

  .svc-card-text,
  .svc-card-visual {
    width: 100%;
  }

  .svc-card-visual {
    flex: none;
    aspect-ratio: 4 / 3;
  }

  .svc-card-body {
    margin-top: 0;
  }
}
}

/* ---- sections · sections/home-socials.css ---- */
@layer sections {
/* ============================================================================
   BRANDING & SOCIALS (`callout-socials-`, `lb-`).
   ========================================================================== */

.s.is-callout-socials {
  z-index: var(--z-section);
  color: var(--color--black);
}

.callout-socials-inner {
  display: flex;
  justify-content: center;
  align-items: center;
}

.callout-socials-layout {
  display: flex;
  flex-flow: column;
  justify-content: flex-start;
  align-items: center;
  gap: calc(var(--gap) * 2);
  width: 100%;
  text-align: center;
}

.callout-socials-rive-w {
  position: relative;
}

/* ── the logo construction diagram ────────────────────────────────────────
   Bigger than the icon it replaced — a 3.5rem box cannot carry a scaffold, and
   at 7rem every guide still reads at a glance. The lime leads the eye through
   the construction and the mark it resolves into is ink, so the accent is the
   transient and the brand is what stays. Both are drawn in user units rather
   than with `non-scaling-stroke`, which would fight the `pathLength="1"` dash
   normalisation the whole drawing is built on. */
.callout-icon {
  position: relative;
  display: flex;
  width: 5.625rem;
  margin-inline: auto;
  color: var(--color--lime);
}

.callout-icon.is-logo-build {
  width: 7rem;
  height: 7rem;
}

.callout-icon.is-logo-build svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* SETTLED STATE: `stroke-dashoffset: 0` — drawn. The Golden Master ships `1`
   here (undrawn) because JavaScript always arrives to animate it to 0; with the
   script absent the mark is simply invisible. A dash offset of 0 is both the end
   state and the correct answer for a visitor who never gets the animation. */
.callout-icon.is-logo-build [data-lb-grid] path {
  fill: none;
  stroke: var(--color--lime);
  stroke-width: 1.6;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
}

.callout-icon.is-logo-build [data-lb-mark] path {
  fill: var(--color--dark-green);
  fill-opacity: 1;
  stroke: var(--color--dark-green);
  stroke-width: 2.2;
  stroke-linejoin: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
}

/* ── the card fan ─────────────────────────────────────────────────────────
   Seven absolutely-positioned cards over one 36rem stage. The resting geometry
   below is the Golden Master's own table, read out of the fan routine rather
   than eyeballed:

       card  scale    rotation   x (desktop)   x (<=991)   y      depth
       1     0.7756   -21deg     -30rem        -15rem      7.3rem   1
       2     0.8498   -14deg     -22rem        -11rem      4rem     2
       3     0.9346    -7deg     -11rem         -6rem      1.3rem   3
       4     1          0         0              0         0       10
       5     0.9346     7deg      11rem          6rem      1.3rem   3
       6     0.8498    14deg      22rem         11rem      4rem     2
       7     0.7756    21deg      30rem         15rem      7.3rem   1

   `isolation: isolate` makes the stage its own stacking context, so the seven
   depths below are local sibling order and cannot leak into the page's z-index
   contract — which is why they are integers here and tokens everywhere else. */
.callout-socials-card-layout {
  position: relative;
  isolation: isolate;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  max-width: 80rem;
  height: 36rem;
}

.callout-socials-card-w {
  position: absolute;
  flex: none;
  width: 20rem;
  height: 35rem;

  /* The radius is a fixed rem while the card's WIDTH steps down four times
     (20 -> 17 -> 15 -> 14rem), so a single value gets relatively rounder the
     smaller the card gets: 16.6% of the card's width on desktop but 23.7% on a
     phone — which is why the fan reads as a stack of pills on mobile and as
     cards on desktop from one declaration. Each breakpoint carries its own,
     tuned to hold the ratio near 13%. */
  border-radius: 2.8rem;
  backface-visibility: hidden;
  overflow: clip;
  transform: translate(var(--card-x), var(--card-y)) rotate(var(--card-rot))
    scale(var(--card-scale));
  transform-origin: center center;
  z-index: var(--card-depth);
}

.callout-socials-card-w:nth-child(1) {
  --card-x: -30rem;
  --card-y: 7.3rem;
  --card-rot: -21deg;
  --card-scale: 0.7756;
  --card-depth: 1;
}

.callout-socials-card-w:nth-child(2) {
  --card-x: -22rem;
  --card-y: 4rem;
  --card-rot: -14deg;
  --card-scale: 0.8498;
  --card-depth: 2;
}

.callout-socials-card-w:nth-child(3) {
  --card-x: -11rem;
  --card-y: 1.3rem;
  --card-rot: -7deg;
  --card-scale: 0.9346;
  --card-depth: 3;
}

.callout-socials-card-w:nth-child(4) {
  --card-x: 0rem;
  --card-y: 0rem;
  --card-rot: 0deg;
  --card-scale: 1;
  --card-depth: 10;
}

.callout-socials-card-w:nth-child(5) {
  --card-x: 11rem;
  --card-y: 1.3rem;
  --card-rot: 7deg;
  --card-scale: 0.9346;
  --card-depth: 3;
}

.callout-socials-card-w:nth-child(6) {
  --card-x: 22rem;
  --card-y: 4rem;
  --card-rot: 14deg;
  --card-scale: 0.8498;
  --card-depth: 2;
}

.callout-socials-card-w:nth-child(7) {
  --card-x: 30rem;
  --card-y: 7.3rem;
  --card-rot: 21deg;
  --card-scale: 0.7756;
  --card-depth: 1;
}

.image.is-social-card {
  position: absolute;
  inset: 0;
  z-index: 10;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.callout-socials-intro-w {
  display: flex;
  flex-flow: column;
  justify-content: flex-start;
  align-items: center;
  margin-top: calc(var(--gap) * 3);
}

.callout-socials-links-layout {
  display: flex;
  gap: calc(var(--gap) * 1.5);
}

/* The heading is one sentence on one line, and `is-center` is what centres it in
   the column. It used to be `.split-flex` — a two-span STACK in `flex-flow:
   column`, the parts in two typefaces — which is the pattern P10 removed from
   every section head on the page. `.split-flex` had no other user and went with
   it; the ellipse reveal (`animation/oval-reveal.js`) handles a single line
   without being told. */
.text-title-lg-mona.is-center {
  text-align: center;
}

@media screen and (max-width: 991px) {
  /* The fan narrows rather than re-arranging: same seven angles, half the
     horizontal travel, so it still reads as a fan at a tablet's measure. */
  .callout-socials-card-w {
    aspect-ratio: 20 / 35;
    width: 17rem;
    height: auto;
    border-radius: 2.2rem;
  }

  .callout-socials-card-w:nth-child(1) {
    --card-x: -15rem;
  }

  .callout-socials-card-w:nth-child(2) {
    --card-x: -11rem;
  }

  .callout-socials-card-w:nth-child(3) {
    --card-x: -6rem;
  }

  .callout-socials-card-w:nth-child(5) {
    --card-x: 6rem;
  }

  .callout-socials-card-w:nth-child(6) {
    --card-x: 11rem;
  }

  .callout-socials-card-w:nth-child(7) {
    --card-x: 15rem;
  }
}

@media screen and (max-width: 767px) {
  .callout-socials-card-w {
    width: 15rem;
    border-radius: 1.8rem;
  }

  .callout-icon.is-logo-build {
    width: 5.5rem;
    height: 5.5rem;
  }
}

@media screen and (max-width: 479px) {
  .callout-socials-card-w {
    aspect-ratio: 20 / 35;
    width: 14rem;
    height: auto;
    border-radius: 1.6rem;
  }

  .callout-socials-links-layout {
    flex-flow: wrap;
    justify-content: center;
    align-items: flex-start;
    gap: calc(var(--gap) * 0.5) var(--gap);
    max-width: 20rem;
  }
}
}

/* ---- sections · sections/gl-layers.css ---- */
@layer sections {
/* The two fixed background hosts. See sections/gl-layers.html for the contract
   these z-indexes encode and why they ship before the canvases do. */
.gl-wrap {
  position: fixed;
  inset: 0;
  z-index: var(--z-gl);
  pointer-events: none;
}

.gl-background {
  position: fixed;
  inset: 0;
  z-index: var(--z-page-field);
  pointer-events: none;
}
}

/* ---- sections · sections/projects-index.css ---- */
@layer sections {
/* ============================================================================
   /projects — THE INDEX.

   Five sections on one light chapter: a type masthead, four numbers, the
   horizontal rail of ten, the same ten as a list, and six cards about how they
   are made.

   THREE THINGS ABOUT THIS SHEET ARE LOAD-BEARING.

   THE RAIL IS A NATIVE SCROLLER IN THE BASE STATE. `.pjx-rail-viewport` is
   `overflow-x: auto` with `scroll-snap-type: x mandatory` and the track is
   `width: max-content` — swipeable on a phone, arrow-key scrollable on a
   desktop, ten real links either way, with nothing running. The Golden Master's
   pinned scroll-scrubbed version is layered ON TOP of that under
   `[data-pjx-live]`, and taking that layer off is how both `prefers-reduced-
   motion` and a failed script land back on a section that works. The fallback is
   written once and reached two ways.

   NOTHING IS HIDDEN UNTIL SOMETHING IS WATCHING. `.pjx-reveal` and the hero's
   staged fade have NO start state of their own. The `opacity: 0` lives behind
   `[data-pjx-live]`, which the runtime sets only once it holds an observer over
   every marked element. A page whose copy is invisible when the script fails has
   traded its content for an effect.

   THE COPY PLATE ON A CARD IS INK, NOT PAPER. Half the ten are dark web pages
   and half are light, so the frost under a card's title has to finish fully
   opaque on a colour that survives both. It ramps to a near-black plate and the
   type on it is paper — which is the one inversion on an otherwise light page,
   and it is there because the SHOT is the card.
   ========================================================================== */

/* ── the page's own vocabulary ───────────────────────────────────────────────
   Declared once on the body class so every rule below reads a name. The paper,
   the ink and the accent are the site's; the card greys are LOCAL — the shared
   palette has no card tone, and inventing one in `tokens/` for a single page
   would be a site-wide decision taken for a local reason. Same call the journey
   card makes on the homepage, for the same reason. */
.page-projects {
  --pjx-paper: var(--color--white);
  --pjx-ink: var(--color--dark-green);
  --pjx-dim: var(--color--dark-green-tint-2);
  --pjx-line: color-mix(in srgb, var(--color--dark-green) 12%, transparent);
  --pjx-hover: color-mix(in srgb, var(--color--dark-green) 5%, transparent);

  /* The card as an OBJECT: a tone step down from the page plus a real rim, and
     no shadow anywhere. A white card on a near-white page has no body, which is
     how a design ends up needing a drop shadow to prove the card exists. */
  --pjx-card: #e2eae7;
  --pjx-rim: #c6d1cc;
  --pjx-rim-hover: #9aa8a3;

  /* the ink plate the card copy stands on, and its two inks */
  --pjx-plate-bg: #0a0c0c;
  --pjx-plate-fg: #f2f4ef;
  --pjx-plate-dim: #a7ada6;

  /* The accent, in its two tempers. `--color--lime` is 1.6:1 on this paper, so
     anything that is TYPE takes the ink-locked variant the palette already
     carries for exactly this case; the bright mint is kept for marks that sit on
     the ink plate, where it measures over 10:1. */
  --pjx-accent: var(--color--lime-ink);
  --pjx-mark: var(--color--lime);
  --pjx-on-mark: #10120f;

  /* The one monospace on this page. There is no mono face in `tokens/`, and the
     eyebrows, chips and counters here are instrument readings rather than prose,
     so the stack is named once instead of repeated at each site.

     `.pjx-blurb` WAS THE EXCEPTION AND IS NO LONGER (P8-03). It is the only
     running sentence on this page — the one-line description under each project
     name — and a system mono stack resolves to a different face on every
     operating system, so the ten cards had no settled voice. Everything else
     that still reads this token is a label, a counter or a chip. Same call, and
     the same reasoning, as `.jr-body` in `sections/home-journey.css`. */
  --pjx-mono: ui-monospace, sfmono-regular, menlo, consolas, monospace;
  --pjx-ease: var(--ease-signature);
}

/* The shared sheet ships no list reset, and every list on this page is a layout
   device rather than prose. Scoped to the page so no other route inherits an
   opinion about lists.

   `:where()`, NOT `:is()`, AND THE DIFFERENCE WAS WORTH FIVE DECLARATIONS
   (P8-13). `:is()` takes the specificity of its most specific argument — here a
   bare element — so `.page-projects :is(ol, ul)` scored (0,1,1) and its
   `margin: 0` shorthand beat every single-class rule further down this same
   file. Five intended declarations were silently dead as a result, all measured
   at 0 on the shipped page:

       .pjx-wall   margin-top: var(--sp-10)   the gap under "How they are made"
       .pjx-wall   margin-top: var(--sp-8)    the same, on a phone
       .pjx-stack  margin-top: auto           bottom-aligns the chip row
       .pjx-facts  padding-top: var(--sp-7)   the air under the section rule
       .pjx-track  padding-inline             the no-JS rail's gutter

   The reported symptom was "the spacing between the text block and the cards is
   too tight". It was not tight, it was ZERO — the two boxes touched at all 21
   widths measured. `:where()` contributes nothing to specificity, so this rule
   now ties with those five and loses on source order, which is what a reset is
   supposed to do. */
.page-projects :where(ol, ul) {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* ── shared furniture ───────────────────────────────────────────────────── */

.pjx-s {
  position: relative;
  padding-inline: var(--padding--container);
}

.pjx-wrap {
  width: 100%;
  max-width: var(--fluid-container);
  margin-inline: auto;
}

.pjx-eyebrow {
  margin: 0;
  font-family: var(--pjx-mono);
  font-size: var(--fs-micro);
  line-height: var(--lh-flat);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--pjx-dim);
}

.pjx-h2 {
  margin: 0;
  font-size: var(--fs-h2);
  line-height: var(--lh-tight);
  text-transform: uppercase;
  color: var(--pjx-ink);
}

/* Invisible until focused, then a real control. `opacity` and `pointer-events`
   rather than a transform alone: a translated-away element still peeks whenever
   an ancestor's clipping box does not start where you assumed it did. */
.pjx-skip {
  position: absolute;
  top: 0;
  left: 50%;
  z-index: var(--z-sticky-over);
  padding: var(--sp-4) var(--sp-6);
  border-radius: var(--radius--small);
  background-color: var(--pjx-mark);
  color: var(--pjx-on-mark);
  font-family: var(--pjx-mono);
  font-size: var(--fs-cap);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, -100%);
  transition:
    transform var(--duration-base) var(--pjx-ease),
    opacity var(--duration-base) var(--pjx-ease);
}

.pjx-skip:focus-visible {
  opacity: 1;
  pointer-events: auto;
  transform: translate(-50%, var(--space-m));
}

/* ── the masthead ───────────────────────────────────────────────────────── */

.pjx-hero {
  display: flex;
  flex-flow: column;
  justify-content: center;
  min-height: calc(var(--vh) * 100);
  padding-block: calc(var(--on-t-nav-spacer) + var(--sp-9)) var(--sp-11);
  overflow: clip;
}

/* The dot-field host. Empty in this phase — the particle system is a homepage
   subsystem and this page does not mount one yet — so it costs one absolutely
   positioned empty div and paints nothing. It is here because it is the
   documented seam, and a host that appears with its controller is a host nobody
   measured. */
.pjx-hero-field {
  position: absolute;
  inset: 0;
  z-index: var(--z-base);
  pointer-events: none;
}

.pjx-hero-inner {
  position: relative;
  z-index: var(--z-raise);
  display: flex;
  flex-flow: column;
  align-items: center;
  width: 100%;
  max-width: var(--fluid-container);
  margin-inline: auto;
  text-align: center;
}

.pjx-hero-mark {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-6);
  color: var(--pjx-ink);
}

.pjx-hero-glyph {
  flex: none;
  width: 1.5rem;
  height: 1.5rem;
}

.pjx-hero-mark-t {
  font-family: var(--pjx-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--pjx-dim);
}

.pjx-hero-title {
  max-width: 18ch;
  margin: 0;
  font-size: var(--fs-display);
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  color: var(--pjx-ink);
}

.pjx-hero-lede {
  max-width: 46ch;
  margin: var(--sp-7) 0 0;
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--pjx-dim);
}

.pjx-hero-cta {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: var(--sp-3);
  margin: var(--sp-8) 0 0;
}

.pjx-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-6);
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: var(--fs-sm);
  line-height: var(--lh-flat);
  font-weight: 600;
  transition:
    background-color var(--duration-base) var(--pjx-ease),
    border-color var(--duration-base) var(--pjx-ease);
}

.pjx-btn.is-primary {
  background-color: var(--pjx-mark);
  color: var(--pjx-on-mark);
}

.pjx-btn.is-secondary {
  border-color: var(--pjx-line);
  background-color: var(--pjx-card);
  color: var(--pjx-ink);
}

.pjx-btn.is-primary:hover,
.pjx-btn.is-primary:focus-visible {
  background-color: color-mix(in srgb, var(--pjx-mark) 82%, var(--pjx-ink));
}

.pjx-btn.is-secondary:hover,
.pjx-btn.is-secondary:focus-visible {
  border-color: var(--pjx-rim-hover);
}

/* ── THE DECK ─────────────────────────────────────────────────────────────
   Ten plates in a shallow fan, and ONE authored number per plate (P8-11).

   `--c` is a plate's distance from the centre of the fan in half-steps: -4.5 at
   the left end, 4.5 at the right, 0.5 either side of the middle. Every part of
   the fan's shape is that one value read three ways —

       across   `--c * --pjx-fan-x`         the step between plates
       down     `--c * --c * --pjx-fan-y`   the arc, because c-squared IS a
                                            parabola and CSS has no `pow()`
       around   `--c * --pjx-fan-r`         the splay

   — so the whole composition is three declarations rather than thirty, and
   re-shaping it is three numbers rather than a rewrite. The plates overlap by
   design: `--pjx-fan-x` is smaller than `--pjx-fan-w`, which is what makes it a
   deck rather than a row.

   `z-index` RISES LEFT TO RIGHT, AND THAT IS NOT A STYLE CHOICE. Each plate's
   badge sits at its own top-left corner, so a plate is only legible if its LEFT
   neighbour is underneath it. A stack that rises toward the middle — the first
   thing this looked like — put five badges on top and hid the other five under
   the plate beside them, which on a section whose whole argument is "there are
   ten" is the one failure that matters. `10 + --c * 2` is monotonic across the
   whole fan and stays positive at both ends.

   The fan sits in a box with `overflow: clip` and a height shorter than the
   plate, so the deck is CUT rather than fitted — the plates continue past the
   bottom of the hero into the rail that follows, which is where the same ten
   are shown whole. */

.pjx-deck {
  --pjx-fan-w: clamp(5.5rem, 14vw, 12rem);
  --pjx-fan-x: calc(var(--pjx-fan-w) * 0.62);
  --pjx-fan-y: 0.34rem;
  --pjx-fan-r: 2.6deg;

  position: relative;
  width: 100%;
  margin-top: var(--sp-9);
}

/* The plates are absolutely positioned from one origin, so the fan is centred
   by construction rather than by a gap arithmetic that changes with the count.
   The height is 78% of a plate: the deck is a hand held up, not a shelf.

   THE CUT IS A FADE, NOT AN EDGE. `overflow: clip` alone slices ten plates on
   one hard line, which reads as a mistake rather than as a crop — a mask takes
   the last eighth to nothing instead, so the deck runs out of the hero rather
   than stopping at it. The gradient is in the mask, not in a pseudo-element
   painted over the plates: an overlay would have to be the page's colour, and
   the page's colour is not the same on the two sides of the hero's own edge. */
.pjx-deck-fan {
  position: relative;
  height: calc(var(--pjx-fan-w) * (850 / 600) * 0.78);
  overflow: clip;
  -webkit-mask-image: linear-gradient(to bottom, #000 84%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 84%, transparent 100%);
}

/* `--shift` is 0 at every width but the phone's, where the fan breaks into two
   rows of five and each row re-centres itself. Adding it into `--cx` once means
   the three geometry rules below never learn about rows at all — the phone block
   sets two numbers and the fan reshapes. */
.pjx-deck-slot {
  --shift: 0;
  --cx: calc(var(--c) + var(--shift));

  position: absolute;
  top: 0;
  left: 50%;
  z-index: calc(10 + var(--c) * 2);
  width: var(--pjx-fan-w);
  margin-left: calc(var(--pjx-fan-w) / -2);
  transform: translateX(calc(var(--cx) * var(--pjx-fan-x)))
    translateY(calc(var(--cx) * var(--cx) * var(--pjx-fan-y)))
    rotate(calc(var(--cx) * var(--pjx-fan-r)));
  transform-origin: 50% 120%;
}

/* THE FAN MIRRORS UNDER RTL, AND THE STACK MUST NOT.

   Negating `--cx` puts 01 on the right and 10 on the left, which is where an
   Arabic reader starts. That one line is the whole mirror: `translateX` and the
   rotation both flip with it, and the arc does not, because `--cx * --cx` is
   even.

   THE `z-index` FORMULA IS DELIBERATELY LEFT ALONE, and it took a bug to see
   why. The badge sits at the plate's `inset-inline-start` — its physical LEFT in
   Latin, its physical RIGHT in Arabic — so in both directions the badge is on
   the side the READING starts from, and in both directions the plate on that
   side has to be underneath. `10 + --c * 2` already says that: in Latin it
   rises left to right, and in Arabic, where `--c` runs the other way across the
   screen, the same expression rises right to left. Mirroring it as well would
   invert the overlap and hide nine of the ten badges — which is exactly what
   `/ar/projects` did before this rule existed. */
html[dir='rtl'] .pjx-deck-slot {
  --cx: calc((var(--c) + var(--shift)) * -1);
}

/* THE DEAL, AND WHY IT IS TWO RULES RATHER THAN A REVEAL HOOK PER PLATE.

   The fanned transform above is the BASE state, so a document with no
   JavaScript — or one whose script failed — gets ten plates standing in a fan
   and nothing else. `[data-pjx-live]` is written by the runtime only when it
   holds an observer over every marked element AND motion is enabled, so these
   two rules are the whole of the animation and they cannot strand the deck: the
   collapsed pose only exists while something is watching for the class that
   undoes it.

   `--d` runs 4,3,2,1,0,0,1,2,3,4 from the markup, so the two middle plates go
   first and the ends last — a hand opening from its centre rather than a wipe.
   The delay is added to the container's own `--i` stagger, which is why the
   deck settles after the buttons above it rather than racing them.

   The slots are the ONLY thing that moves. The reveal hook is on the container,
   so the shared `[data-pjx-fade]` rules never touch a plate and there is no
   specificity contest between a fan transform and a fade transform — the
   failure mode that makes a deck collapse into a stack the moment a preference
   changes. */
[data-pjx-live] .pjx-deck-slot {
  transform: translateY(2.5rem) scale(0.94);
  opacity: 0;
  transition:
    transform 0.9s var(--pjx-ease),
    opacity 0.6s var(--pjx-ease);
  transition-delay: calc(320ms + var(--d) * 90ms);
}

[data-pjx-live] .pjx-deck.is-in .pjx-deck-slot {
  transform: translateX(calc(var(--cx) * var(--pjx-fan-x)))
    translateY(calc(var(--cx) * var(--cx) * var(--pjx-fan-y)))
    rotate(calc(var(--cx) * var(--pjx-fan-r)));
  opacity: 1;
}

/* The plate itself is the rail's card at deck scale — the same bezel, the same
   rim, the same radius pair — so a reader meets one object twice rather than two
   objects once. */
.pjx-deck-card {
  position: relative;
  display: block;
  padding: 0.25rem;
  border: 1px solid var(--pjx-rim);
  border-radius: 0.9rem;
  background-color: var(--pjx-card);
  transition:
    border-color var(--duration-base) var(--pjx-ease),
    transform var(--duration-base) var(--pjx-ease);
}

.pjx-deck-card:hover,
.pjx-deck-card:focus-visible {
  border-color: var(--pjx-rim-hover);
  transform: translateY(-0.5rem);
}

.pjx-deck-img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 0.7rem;
}

/* The number sits on the plate rather than under it: at this size a caption
   would be two plates wide. It is the rail's badge, shrunk. */
.pjx-deck-n {
  position: absolute;
  inset-block-start: 0.5rem;
  inset-inline-start: 0.5rem;
  display: grid;
  place-items: center;
  min-width: 1.5rem;
  height: 1.5rem;
  padding-inline: 0.25rem;
  border-radius: 999px;
  background-color: var(--pjx-mark);
  color: var(--pjx-on-mark);
  font-family: var(--pjx-mono);
  font-size: var(--fs-micro);
  line-height: var(--lh-flat);
}

/* ── the ledger ───────────────────────────────────────────────────────────
   Four claims, each drawn as ten of ten.

   THE TICKS ARE ONE GRADIENT. `repeating-linear-gradient` with a 10% period and
   a 6.4% mark draws exactly ten strokes across any width, so the count is
   arithmetic rather than forty elements that could fall out of step with it.
   `to right` is PHYSICAL on purpose: this is a tally, and a tally that mirrors
   under RTL is still ten marks — the direction carries no meaning, so flipping
   it would be work with no reader. */

.pjx-facts-s {
  padding-block: 0 var(--sp-10);
}

.pjx-tally {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-6) var(--sp-8);
  padding-top: var(--sp-7);
  border-top: 1px solid var(--pjx-line);
}

.pjx-tally-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: baseline;
  column-gap: var(--sp-3);
  row-gap: var(--sp-4);
}

.pjx-tally-ticks {
  grid-column: 1 / -1;
  height: 0.5rem;
  background-image: repeating-linear-gradient(
    to right,
    var(--pjx-accent) 0 5.4%,
    transparent 5.4% 10%
  );
}

.pjx-tally-fig {
  font-family: var(--pjx-mono);
  font-size: var(--fs-cap);
  line-height: var(--lh-flat);
  font-weight: 600;
  letter-spacing: var(--track-label);
  white-space: nowrap;
  color: var(--pjx-ink);
}

.pjx-tally-label {
  font-size: var(--fs-sm);
  line-height: var(--lh-label);
  color: var(--pjx-dim);
}

/* ── the rail ────────────────────────────────────────────────────────────
   THE BASE STATE IS THE NO-SCRIPT STATE. Everything in this block is a native
   horizontal scroller; the pinned version below only adds to it. */

.pjx-rail-s {
  padding-inline: 0;
}

.pjx-rail-wrap {
  position: relative;
}

.pjx-rail-stage {
  display: flex;
  flex-flow: column;
  gap: var(--sp-5);
  padding-bottom: var(--sp-4);
}

.pjx-rail-bar {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--gap);
  padding-inline: var(--padding--container);
}

.pjx-rail-count {
  margin: 0;
  font-family: var(--pjx-mono);
  font-size: var(--fs-micro);
  color: var(--pjx-dim);
}

.pjx-rail-count b {
  font-weight: 500;
  color: var(--pjx-ink);
}

.pjx-rail-viewport {
  padding-bottom: var(--sp-4);
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--padding--container);
  overscroll-behavior-x: contain;
}

/* Card geometry, one lever each. The plate crops are 700x1313 — an 8:15
   portrait — because these are WEB PAGES: the taller the frame, the more of the
   real page each card shows. The height is the ceiling a pinned stage allows,
   so the pinned branch below needs no second set of numbers. */
.pjx-track {
  --pjx-card-h: min(78vh, 50rem);
  --pjx-card-w: calc(var(--pjx-card-h) * 0.5333);
  --pjx-pad: 1.125rem;

  display: flex;
  align-items: center;
  width: max-content;
  padding-inline: var(--padding--container);
}

.pjx-slide {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  flex: none;
  width: calc(var(--pjx-card-w) + var(--pjx-pad) * 2);
  padding-inline: var(--pjx-pad);
  scroll-snap-align: center;
}

/* ── one card ─────────────────────────────────────────────────────────────
   The homepage journey card's language taken over to paper: a 5px bezel, outer
   radius 1.5rem against inner 1.1875rem, a 1px rim as the whole separation, and
   a shot that NEVER transforms so nothing can drift out of frame while the rail
   scrubs. */
.pjx-card {
  /* One number for the copy plate, read by the glass, the frost and the hover
     step, so the plate can never be shorter than the copy standing on it. */
  --pjx-copy-h: 58%;

  position: relative;
  z-index: var(--z-raise);
  display: block;
  flex: none;
  width: var(--pjx-card-w);
  height: var(--pjx-card-h);
  padding: 0.3125rem;
  border: 1px solid var(--pjx-rim);
  border-radius: 1.5rem;
  background-color: var(--pjx-card);
  color: var(--pjx-ink);
  overflow: clip;
  transition: border-color var(--duration-slow) var(--pjx-ease);
}

.pjx-frame {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 1.1875rem;
  overflow: clip;
  transition: filter var(--duration-slow) var(--pjx-ease);
}

.pjx-shot {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 0;
}

.pjx-glass,
.pjx-scrim,
.pjx-sheen {
  position: absolute;
  pointer-events: none;
}

/* Four backdrop copies rather than one. A single `blur()` ends on a hard line
   wherever its mask stops; four, each blurred harder and masked to finish lower
   than the last, overlap into a continuous ramp. */
.pjx-glass {
  inset: auto 0.3125rem 0.3125rem;
  z-index: var(--z-raise);
  height: var(--pjx-copy-h);
  border-radius: 0 0 1.1875rem 1.1875rem;
  overflow: clip;
  transition: height var(--duration-slow) var(--pjx-ease);
}

.pjx-glass span {
  position: absolute;
  inset: 0;
}

.pjx-glass span:nth-child(1) {
  -webkit-backdrop-filter: blur(1px);
  backdrop-filter: blur(1px);
  -webkit-mask-image: linear-gradient(to top, #000 0 30%, transparent 78%);
  mask-image: linear-gradient(to top, #000 0 30%, transparent 78%);
}

.pjx-glass span:nth-child(2) {
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  -webkit-mask-image: linear-gradient(to top, #000 0 18%, transparent 58%);
  mask-image: linear-gradient(to top, #000 0 18%, transparent 58%);
}

.pjx-glass span:nth-child(3) {
  -webkit-backdrop-filter: blur(7px);
  backdrop-filter: blur(7px);
  -webkit-mask-image: linear-gradient(to top, #000 0 8%, transparent 38%);
  mask-image: linear-gradient(to top, #000 0 8%, transparent 38%);
}

.pjx-glass span:nth-child(4) {
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  -webkit-mask-image: linear-gradient(to top, #000 0 0%, transparent 20%);
  mask-image: linear-gradient(to top, #000 0 0%, transparent 20%);
}

/* The frost has to finish FULLY OPAQUE: five of the ten shots are dark pages
   and five are light, and one copy colour has to survive both. */
.pjx-scrim {
  inset: auto 0.3125rem 0.3125rem;
  z-index: 2;
  height: var(--pjx-copy-h);
  border-radius: 0 0 1.1875rem 1.1875rem;
  background-image: linear-gradient(
    #080a0a00 0%,
    #080a0a1a 10%,
    #080a0a57 19%,
    #080a0aa3 27%,
    #080a0adb 34%,
    #080a0af2 41%,
    var(--pjx-plate-bg) 48%,
    var(--pjx-plate-bg) 100%
  );
}

/* No backdrop-filter (older Safari, a forced software GPU): the frost alone has
   to carry the contrast, so it starts earlier and lands sooner. */
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  .pjx-scrim {
    background-image: linear-gradient(
      #080a0a00 0%,
      #080a0a1a 10%,
      #080a0a57 20%,
      #080a0aa8 29%,
      #080a0ae0 37%,
      #080a0af7 46%,
      var(--pjx-plate-bg) 56%
    );
  }
}

/* A light sweep that crosses the card on hover. It starts and ends off the
   card, so none of it is visible at rest. */
.pjx-sheen {
  inset: 0.3125rem;
  z-index: 3;
  border-radius: 1.1875rem;
  background-image: linear-gradient(105deg, transparent 42%, #ffffff5c 50%, transparent 58%);
  opacity: 0;
  transform: translateX(-120%);
  transition:
    transform 0.85s var(--pjx-ease),
    opacity var(--duration-base) linear;
}

.pjx-info {
  position: absolute;
  inset: auto 0 0;
  z-index: 4;
  display: flex;
  flex-flow: column;
  gap: var(--sp-2);
  padding: 0 1.15rem 1.15rem;
}

.pjx-info-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--sp-2);
}

.pjx-title {
  font-family: var(--font-display);
  font-size: var(--fs-h4);
  line-height: var(--lh-tight);
  font-weight: 600;
  color: var(--pjx-plate-fg);
}

.pjx-badge {
  display: flex;
  justify-content: center;
  align-items: center;
  flex: none;
  width: 1.375rem;
  height: 1.375rem;
  border-radius: 50%;
  background-color: var(--pjx-mark);
  font-family: var(--pjx-mono);
  font-size: var(--fs-micro);
  color: var(--pjx-on-mark);
}

/* ONE LINE, ALWAYS. A kicker that wraps pushes the blurb and the chip row down
   into the frost's transparent half, where the copy lands on a screenshot. The
   plate is a fixed share of a fixed-ratio card, so the copy inside it has to be
   bounded — every rule in this block is that bound. */
.pjx-kicker {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-family: var(--pjx-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--pjx-plate-dim);
}

/* Four lines, and the tenth card is the same object as the first. Without the
   clamp the longest blurb is two lines taller than the shortest, so the plate
   would have to be sized for the worst case on every card. */
.pjx-blurb {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  /* 400, not 300 — Quicksand Light is declared but preloaded by nothing, and one
     paragraph is not worth a fourth face. See `sections/home-journey.css`. */
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  font-weight: 400;
  line-height: var(--lh-body);
  color: var(--pjx-plate-fg);
  opacity: 0.78;
  overflow: hidden;
}

.pjx-foot {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
}

/* `min-width: 0` is the load-bearing half: a flex item's default minimum is its
   content, so without it the two chips refuse to shrink and push the counter
   off the card instead of truncating. */
.pjx-chip {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-family: var(--pjx-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--pjx-plate-fg);
  opacity: 0.8;
}

/* The counter inverts — the only element on the card that does — so a reader
   always knows which of ten they are looking at without hunting for it. */
.pjx-pill {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  margin-left: auto;
  padding: 0.3rem 0.6rem;
  border-radius: 999px;
  background-color: var(--pjx-plate-fg);
  font-family: var(--pjx-mono);
  font-size: var(--fs-micro);
  color: var(--pjx-plate-bg);
  transition: background-color var(--duration-slow) var(--pjx-ease);
}

.pjx-pill-sep {
  opacity: 0.45;
}

.pjx-card:hover,
.pjx-card:focus-visible {
  border-color: var(--pjx-rim-hover);
}

.pjx-card:hover .pjx-frame,
.pjx-card:focus-visible .pjx-frame {
  filter: saturate(1.12) brightness(1.05);
}

.pjx-card:hover .pjx-glass,
.pjx-card:focus-visible .pjx-glass {
  height: calc(var(--pjx-copy-h) + 4%);
}

.pjx-card:hover .pjx-sheen,
.pjx-card:focus-visible .pjx-sheen {
  opacity: 1;
  transform: translateX(120%);
}

.pjx-card:hover .pjx-pill,
.pjx-card:focus-visible .pjx-pill {
  background-color: var(--pjx-mark);
  color: var(--pjx-on-mark);
}

/* ── THE PINNED RAIL, and the ONE gate that turns it on ──────────────────
   `[data-pjx-live]` is written by `runtime/pages/projects.js` on the section,
   and only after it has measured the track and holds the scrub. Everything in
   this block is therefore unreachable when the script fails, when the browser
   has no JavaScript, and — because the runtime does not set the attribute in
   that case — under `prefers-reduced-motion`. The native scroller above shows
   through in all three, unchanged. */
[data-pjx-live] .pjx-rail-wrap {
  /* Ten slides. The multiplier is the scroll budget per slide. */
  height: calc(100vh + 10 * 74vh);
}

[data-pjx-live] .pjx-rail-stage {
  position: sticky;
  top: 0;
  justify-content: center;
  height: calc(var(--vh) * 100);
  padding-top: var(--on-t-nav-spacer);
  padding-bottom: var(--sp-6);
  overflow: hidden;
}

[data-pjx-live] .pjx-rail-viewport {
  position: relative;
  flex: 1;
  min-height: 0;
  padding-bottom: 0;
  overflow: visible;
  scroll-snap-type: none;

  /* THE AXIS DECLARATION, and it belongs here rather than in JavaScript.
     `runtime/sections/projects-rail-drag.js` owns the inline axis inside this
     box on a touch device and converts a sideways finger into the page scroll
     that drives the rail; the block axis stays the browser's. Saying so in CSS
     is what stops iOS Safari claiming a horizontal swipe as a pan and firing
     `pointercancel` into the middle of the gesture. It is scoped to the live
     state on purpose: the base rule above is a real `overflow-x: auto` scroller
     and needs `auto`. */
  touch-action: pan-y;
}

/* A SWIPE MUST NOT BECOME A LONG PRESS OR AN IMAGE DRAG. Both are platform
   defaults on an `<img>` inside an `<a>`, both fire mid-gesture, and both end
   the pointer stream the drag is reading. Live-scoped, so the native scroller
   keeps every default it has. */
[data-pjx-live] .pjx-card {
  user-select: none;
  -webkit-user-drag: none;
}

[data-pjx-live] .pjx-shot {
  -webkit-touch-callout: none;
}

[data-pjx-live] .pjx-track {
  position: absolute;
  inset: 0;
  width: auto;
  padding-inline: 8vw;
  transform: translate3d(calc(var(--pjx-x, 0) * 1px), 0, 0);
  will-change: transform;
}

/* THE GESTURE'S OWN STATE, WRITTEN BY `projects-rail-drag.js` AND UNTIL NOW READ
   BY NOTHING. The attribute has been on the viewport since the drag was written
   and no rule anywhere matched it, so a finger moved ten cards that were still
   running their hover transitions — every card easing towards a state the drag
   was already past, which reads as lag in the gesture rather than as smoothing.

   `cursor: grabbing` is for the pointer that is not supposed to be here: the
   drag mounts on coarse pointers only, but a hybrid laptop has both, and a
   trackpad that reaches it should say what it is doing. */
[data-pjx-live] .pjx-rail-viewport[data-pjx-dragging] {
  cursor: grabbing;
}

[data-pjx-live] .pjx-rail-viewport[data-pjx-dragging] .pjx-card,
[data-pjx-live] .pjx-rail-viewport[data-pjx-dragging] .pjx-card * {
  transition: none;
}

[data-pjx-live] .pjx-slide {
  scroll-snap-align: none;
}

/* ── the index list ─────────────────────────────────────────────────────── */

.pjx-index-s {
  padding-block: var(--sp-11);
}

.pjx-index-head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--gap);
  margin-bottom: var(--sp-7);
}

.pjx-index-intro {
  max-width: 46ch;
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--pjx-dim);
}

.pjx-index {
  position: relative;
  border-top: 1px solid var(--pjx-line);
}

.pjx-row {
  border-bottom: 1px solid var(--pjx-line);
}

.pjx-row-link {
  display: grid;
  grid-template-columns: 3.5rem minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--gap);
  padding: 1.375rem 0.5rem;
  transition:
    background-color var(--duration-base) var(--pjx-ease),
    padding-left var(--duration-base) var(--pjx-ease);
}

.pjx-row-link:hover,
.pjx-row-link:focus-visible {
  padding-left: 1.25rem;
  background-color: var(--pjx-hover);
}

.pjx-row-n {
  font-family: var(--pjx-mono);
  font-size: var(--fs-micro);
  color: var(--pjx-dim);
}

.pjx-row-main {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-4);
  min-width: 0;
}

.pjx-row-name {
  font-family: var(--font-display);
  font-size: var(--fs-h4);
  line-height: var(--lh-tight);
  font-weight: 600;
}

.pjx-row-cat,
.pjx-row-year {
  font-family: var(--pjx-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--pjx-dim);
}

/* The inline thumbnail is the substitute for the floating plate, and it is the
   DEFAULT rather than the exception: it shows on touch, on a coarse pointer,
   under reduced motion and when the runtime never arrives. The plate hides it
   only where the plate can actually exist. */
.pjx-row-thumb {
  width: 4rem;
  height: 2.75rem;
  border: 1px solid var(--pjx-line);
  border-radius: 0.25rem;
  object-fit: cover;
  object-position: 50% 0;
}

/* The floating plate. `fixed` so the list's overflow can never clip it, and
   `pointer-events: none` so it cannot steal the pointermove that drives it. No
   opacity transition: the runtime lerps opacity itself, and a CSS transition
   restarted by every per-frame write only smears the fade. */
.pjx-plate {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 3;
  display: none;
  width: 14rem;
  padding: 0.1875rem;
  border: 1px solid var(--pjx-rim);
  border-radius: var(--radius--small);
  background-color: var(--pjx-card);
  aspect-ratio: 8 / 15;
  opacity: 0;
  pointer-events: none;
  overflow: clip;
  will-change: transform, opacity;
}

.pjx-plate img {
  width: 100%;
  height: 100%;
  border-radius: 0.8125rem;
  object-fit: cover;
  object-position: 50% 0;
}

/* Both conditions, and both are the runtime's: it sets `data-pjx-plate-live`
   only on a fine pointer with motion allowed. */
[data-pjx-plate-live] .pjx-plate {
  display: block;
}

[data-pjx-plate-live] .pjx-row-thumb {
  display: none;
}

/* ── how they are made ──────────────────────────────────────────────────── */

.pjx-wall-s {
  padding-block: var(--sp-10) var(--sp-11);
}

.pjx-wall-title {
  text-align: center;
}

.pjx-wall-intro {
  max-width: 56ch;
  margin: var(--sp-5) auto 0;
  text-align: center;
  color: var(--pjx-dim);
}

/* THREE ACROSS, TWICE — AN EXPLICIT TRACK COUNT, NOT `auto-fill` (P8-13).

   There are exactly six cards and there always will be: `t.wall.cards` is a
   six-entry array whose length is parity-checked across all three locales, so
   this grid is not a responsive unknown. `auto-fill` treated it as one and made
   a different mess at every width — measured, the shipped page was 5 + 1 above
   1571px, 4 + 2 from 992 to 1570, and 3 + 3 only in the 768–991 band. Worse,
   `auto-fill` keeps the empty tracks, so the short last row sat hard against the
   left edge with one or two ghost columns beside it. That ragged, left-hung
   remainder is the "unprofessional" the report names.

   `minmax(0, 1fr)` rather than `1fr`: a bare `1fr` floors at `auto`, and the
   longest chips — `IntersectionObserver`, `Custom properties`,
   `Fokus-Management` — would then push their track wider than its siblings and
   defeat `.pjx-stack li`'s ellipsis.

   THE CAP IS IN PIXELS, deliberately. `1rem` on this site is derived from the
   viewport, so a rem cap scales with the thing it is meant to cap and never
   bites. 1180px puts the three cards at ~380px from 1240px up, which is the
   width the card's own copy was written for; below that the grid is simply the
   container. `margin-inline: auto` centres it under the centred head. */
.pjx-wall {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-5);
  max-width: 1180px;
  margin-top: var(--sp-10);
  margin-inline: auto;
}

.pjx-wall-card {
  display: flex;
  flex-flow: column;
  gap: var(--sp-4);
  padding: 1.5rem;
  border: 1px solid var(--pjx-rim);
  border-radius: var(--radius--small);
  background-color: var(--pjx-card);
}

.pjx-wall-name {
  margin: 0;
  font-size: var(--fs-body);
  line-height: var(--lh-snug);
  font-weight: 600;
}

.pjx-wall-note {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--pjx-dim);
}

.pjx-stack {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: auto;
}

.pjx-stack li {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 0.375rem 0.75rem;
  border: 1px solid var(--pjx-line);
  border-radius: 999px;
  font-family: var(--pjx-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

/* ── the entrance ────────────────────────────────────────────────────────
   NOTHING IS HIDDEN UNTIL SOMETHING IS WATCHING. See the file header. */
[data-pjx-live] .pjx-reveal,
[data-pjx-live] [data-pjx-fade] {
  opacity: 0;
  transform: translateY(1.5rem);
  transition:
    opacity 0.8s var(--pjx-ease),
    transform 0.8s var(--pjx-ease);
  transition-delay: calc(var(--i, 0) * 80ms);
}

[data-pjx-live] .pjx-reveal.is-in,
[data-pjx-live] [data-pjx-fade].is-in {
  opacity: 1;
  transform: none;
}

/* ── 991 and below ──────────────────────────────────────────────────────── */

@media screen and (max-width: 991px) {
  .pjx-track {
    /* Sized from the viewport width, keeping the 8:15 ratio, and capped so the
       tall card still clears the stage. Whichever limit bites, the RATIO holds,
       so `object-fit` never has a side to crop. */
    --pjx-card-w: min(78vw, 24rem, calc(68vh / 1.875));
    --pjx-card-h: calc(var(--pjx-card-w) * 1.875);
    --pjx-pad: 0.5rem;
  }

  [data-pjx-live] .pjx-rail-wrap {
    height: calc(100vh + 10 * 62vh);
  }

  [data-pjx-live] .pjx-track {
    padding-inline: 6vw;
  }

  /* The 1px pass is invisible over a screenshot at this size, and each layer is
     a backdrop copy plus a two-pass gaussian over the card's full width. */
  .pjx-glass span:nth-child(1) {
    display: none;
  }

  /* The card is shorter here and the copy is not, so the plate takes a larger
     share of it, and the frost has to start earlier to meet it. */
  .pjx-card {
    --pjx-copy-h: 66%;
  }

  .pjx-scrim {
    background-image: linear-gradient(
      #080a0a00 0%,
      #080a0a29 9%,
      #080a0a80 17%,
      #080a0acc 24%,
      #080a0af0 30%,
      var(--pjx-plate-bg) 37%,
      var(--pjx-plate-bg) 100%
    );
  }

  .pjx-index-s,
  .pjx-wall-s {
    padding-block: var(--sp-9);
  }

  .pjx-row-link {
    grid-template-columns: 2.5rem minmax(0, 1fr) auto;
  }

  .pjx-row-year {
    display: none;
  }
}

/* ── 767 and below ──────────────────────────────────────────────────────── */

@media screen and (max-width: 767px) {
  .pjx-hero {
    padding-block: calc(var(--on-t-nav-spacer) + var(--sp-7)) var(--sp-9);
  }

  .pjx-hero-title {
    max-width: 14ch;
  }

  .pjx-hero-lede {
    margin-top: var(--sp-5);
  }

  .pjx-hero-cta {
    margin-top: var(--sp-6);
  }

  /* THE FAN BREAKS INTO TWO HANDS OF FIVE, IT DOES NOT SHRINK.

     Ten plates in one row need 6.6 plate-widths of room. At 390px that is 55px
     per plate — a thumbnail of a thumbnail, and at that size ten of them say
     "some pictures" rather than "ten builds". Two rows of five need 3 widths,
     which buys 120px a plate: enough that a reader can still tell Ember & Oak
     from NEONFRAME, which is the entire argument this section is making.

     `--shift` re-centres each row: plates 1-5 carry `--c` -4.5..-0.5, so +2.5
     puts them at -2..2, and plates 6-10 carry 0.5..4.5, so -2.5 does the same.
     No other rule changes. */
  .pjx-deck {
    --pjx-fan-w: min(32vw, 9rem);
    --pjx-fan-x: calc(var(--pjx-fan-w) * 0.5);
    --pjx-fan-y: 0.3rem;
    --pjx-fan-r: 3deg;

    margin-top: var(--sp-7);
  }

  .pjx-deck-fan {
    height: calc(var(--pjx-fan-w) * (850 / 600) * 1.46);
    -webkit-mask-image: linear-gradient(to bottom, #000 92%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 92%, transparent 100%);
  }

  .pjx-deck-slot:nth-child(-n + 5) {
    --shift: 2.5;
  }

  /* THE SECOND HAND SITS IN FRONT OF THE FIRST, and it does so for free: the
     stack is `10 + --c * 2` and row two's `--c` values are all positive, so
     every plate in it already outranks every plate in row one. Only the drop
     needs stating. */
  .pjx-deck-slot:nth-child(n + 6) {
    --shift: -2.5;

    top: calc(var(--pjx-fan-w) * (850 / 600) * 0.5);
  }

  /* Two rows of two. Four ten-tick tallies across a phone is 80px per tally and
     eight of the ten marks disappear into the gaps between them. */
  .pjx-tally {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .pjx-facts-s {
    padding-block: 0 var(--sp-8);
  }

  /* `flex-wrap` alone let the short names keep their category on the same line
     while the long ones pushed it to a second, so a ten-row list read as two
     different components. Stack, always. */
  .pjx-row-main {
    flex-flow: column;
    align-items: flex-start;
    gap: var(--sp-2);
  }

  .pjx-info {
    padding: 0 1rem 1rem;
  }

  /* Three lines on a phone: the card is shorter and the type is not. */
  .pjx-blurb {
    -webkit-line-clamp: 3;
    line-clamp: 3;
  }

  /* ONE CHIP, NOT TWO. At this width the card is about 245 px of copy column
     and two chips plus the counter do not fit in it — they truncate to
     ellipses, which is a worse answer than one fact stated whole. The second
     fact is not lost: it is in the case study's spec bar, one tap away. */
  .pjx-chip + .pjx-chip {
    display: none;
  }

  [data-pjx-live] .pjx-rail-stage {
    padding-top: var(--sp-10);
  }

  /* TWO ACROSS, THREE DEEP. Below 768 a third track leaves ~215px of copy
     column, which is under the measure the card bodies were written for — the
     German ones run to 248 characters. */
  .pjx-wall {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    margin-top: var(--sp-8);
  }
}

/* ── the phone ─────────────────────────────────────────────────────────── */
@media screen and (max-width: 479px) {
  /* One across. Two tracks at 390px is 168px of copy column; the card would be
     a chip list with a caption. */
  .pjx-wall {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ── reduced motion ──────────────────────────────────────────────────────
   The reset layer already flattens every transition and animation on the site
   (`base/reset.css`, ADR-009), so nothing here repeats that. What is left is the
   LAYOUT half, which a transition kill switch cannot reach: with the runtime
   declining to set `[data-pjx-live]` the pinned rail never exists, and these two
   rules make sure a stale attribute — a preference changed mid-session before
   the runtime has torn down — still cannot leave a reader inside an eight-screen
   pin they cannot scroll out of. */
@media (prefers-reduced-motion: reduce) {
  [data-pjx-live] .pjx-rail-wrap {
    height: auto;
  }

  [data-pjx-live] .pjx-rail-stage {
    position: static;
    height: auto;
    padding-bottom: var(--sp-4);
    overflow: visible;
  }

  [data-pjx-live] .pjx-rail-viewport {
    position: static;
    padding-bottom: var(--sp-4);
    overflow-x: auto;
    scroll-snap-type: x mandatory;

    /* THE AXIS GOES BACK TO THE BROWSER. This block restores the native
       scroller on a page whose root still carries a stale `[data-pjx-live]`, and
       `projects-rail-drag.js` is never installed under `reduce` — so a
       `touch-action: pan-y` inherited from the live rule would forbid the one
       gesture this fallback exists to allow. */
    touch-action: auto;
  }

  [data-pjx-live] .pjx-track {
    position: static;
    width: max-content;
    padding-inline: var(--padding--container);
    transform: none;
  }

  [data-pjx-live] .pjx-slide {
    scroll-snap-align: center;
  }

  [data-pjx-live] .pjx-reveal,
  [data-pjx-live] [data-pjx-fade] {
    opacity: 1;
    transform: none;
  }

  /* THE DECK'S TRANSFORM IS LAYOUT, NOT MOTION, so `transform: none` above must
     not reach it — flattening the fan would stack ten plates on one point and
     hide nine of them. It does not reach it (the reveal hook is on the
     container, not the slots), and this restores the settled pose explicitly
     anyway for the case the runtime has already written `[data-pjx-live]` when
     the preference is turned on mid-session. The `transition` goes with it: the
     fan is where the plates ARE, not somewhere they travel to. */
  [data-pjx-live] .pjx-deck-slot {
    transform: translateX(calc(var(--cx) * var(--pjx-fan-x)))
      translateY(calc(var(--cx) * var(--cx) * var(--pjx-fan-y)))
      rotate(calc(var(--cx) * var(--pjx-fan-r)));
    opacity: 1;
    transition: none;
  }

  .pjx-plate {
    display: none;
  }
}
}

/* ---- sections · sections/project-detail.css ---- */
@layer sections {
/* ============================================================================
   THE CASE STUDY — /projects/<slug>, ten times.

   ONE DESIGN FOR ALL TEN DOCUMENTS. There are no per-project theme rules and
   no `[data-cs="<slug>"]` table: every page is the same skeleton, the same
   rhythm and the same accent, and a project expresses itself through its words
   and its own imagery. The Golden Master used to carry a ten-row identity table
   here — Aurelia in bronze, Noctis in red, Brightloop in amber — and the owner's
   note on it was that the case studies read as ten different sites rather than
   as his. The table is gone; the site's green is the only accent.

   THE ACCENT HAS TWO TEMPERS AND THE REASON IS CONTRAST. `--color--lime` is
   #21f1a8 and measures 1.6:1 on this paper, so anything that is TYPE takes
   `--color--lime-ink`; the bright mint is kept for the marks that sit on ink,
   where it is over 10:1. The masthead's second line is the one exception and it
   is a size exception, not a colour one: at display size the bar is 3:1, so it
   takes the lighter ink-locked variant.
   ========================================================================== */

.page-project-detail {
  --cs-paper: var(--color--white);
  --cs-ink: var(--color--dark-green);
  --cs-dim: var(--color--dark-green-tint-2);
  --cs-line: color-mix(in srgb, var(--color--dark-green) 12%, transparent);

  /* The framed object as an object: a tone step down from the page plus a real
     rim. There are NO shadows on these pages and nothing may reintroduce one —
     the tone step and the 1px rim are the whole separation. */
  --cs-card: #e2eae7;
  --cs-rim: #c6d1cc;
  --cs-rim-hover: #9aa8a3;

  --cs-plate-bg: #0a0c0c;
  --cs-plate-fg: #f2f4ef;
  --cs-plate-dim: #a7ada6;

  /* MEASURED, NOT CHOSEN — and the same correction the discipline pages needed.
     `--color--lime-ink` is the cut that stays legible on `--color--white`, and it
     is 4.82:1 there. But this page prints it on `--cs-card`, which is #e2eae7, and
     against that surface the same pair measures 4.36:1 — under the 4.5:1 floor by
     fourteen hundredths, across 612 nodes and all thirty case-study
     documents. It is exactly the kind of miss a designer never sees and a scanner
     always does, and it was found by tools/golden/site-a11y.mjs.
     Fifteen per cent towards the page's own ink takes it to 5.55:1 on the card and
     6.13:1 on paper, and changes nothing anyone can perceive. */
  --cs-accent: color-mix(in srgb, var(--color--lime-ink) 85%, var(--color--dark-green));
  --cs-accent-lg: var(--color--lime-ink-lg);
  --cs-mark: var(--color--lime);
  --cs-on-mark: #10120f;

  --cs-mono: ui-monospace, sfmono-regular, menlo, consolas, monospace;
  --cs-ease: var(--ease-signature);

  /* One reading measure for the whole page, and one section rhythm — the site's,
     from `tokens/type.css`, so these pages breathe like every other route. */
  --cs-measure: 62ch;
  --cs-rhythm: var(--section-y);
}

.page-project-detail :is(ol, ul) {
  margin: 0;
  padding: 0;
  list-style: none;
}

.page-project-detail dd {
  margin: 0;
}

/* ── the two skip links ─────────────────────────────────────────────────────
   `fixed`, because one of them appears from inside the demo figure, most of the
   way down a very long document. */
.cs-skip {
  position: fixed;
  top: 1rem;
  left: 50%;
  z-index: var(--z-sticky-over);
  padding: 0.625rem 1rem;
  border-radius: 0.375rem;
  background-color: var(--cs-ink);
  color: var(--cs-paper);
  font-family: var(--cs-mono);
  font-size: var(--fs-cap);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, -150%);
  transition:
    transform var(--duration-base) var(--cs-ease),
    opacity var(--duration-base) linear;
}

.cs-skip:focus-visible {
  opacity: 1;
  pointer-events: auto;
  transform: translate(-50%, 0);
}

/* ── layout ─────────────────────────────────────────────────────────────── */

.cs-wrap {
  width: 100%;
  max-width: var(--fluid-container);
  margin-inline: auto;
  padding-inline: var(--padding--container);
}

.cs-mast {
  padding-top: calc(var(--on-t-nav-spacer) + var(--sp-8));
  padding-bottom: var(--sp-11);
}

/* ── the breadcrumb ─────────────────────────────────────────────────────────
   A real `<ol>` inside a labelled `<nav>`, with the separator drawn by CSS
   rather than typed into the markup. The Golden Master types a literal `/`
   between two links inside a `<p>`: not a list, not a landmark, and a slash a
   screen reader reads out. */
.cs-crumb {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-3);
}

.cs-crumb-list {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-3);
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--cs-dim);
}

.cs-crumb-list li + li::before {
  content: '/';
  margin-inline-end: var(--sp-3);
  color: var(--cs-line);
}

.cs-crumb-list a {
  transition: color var(--duration-base) linear;
}

.cs-crumb-list a:hover,
.cs-crumb-list a:focus-visible {
  color: var(--cs-accent);
}

.cs-crumb-list [aria-current] {
  color: var(--cs-ink);
}

.cs-crumb-n {
  margin: 0;
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--cs-accent);
}

.cs-mast-eyebrow {
  margin: var(--sp-8) 0 0;
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--cs-dim);
}

.cs-mast-title {
  margin: var(--sp-4) 0 0;
}

.cs-title-row {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-display);
  line-height: var(--lh-impact);
  letter-spacing: var(--track-display);
  text-transform: uppercase;
  color: var(--cs-ink);
}

/* The lighter ink-locked mint, and the reason is size: this line is display
   scale, where the contrast bar is 3:1 rather than 4.5:1. Every small label on
   these pages takes the darker one. */
.cs-title-sub {
  color: var(--cs-accent-lg);
}

.cs-mast-lede {
  max-width: 46ch;
  margin: var(--sp-6) 0 0;
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--cs-dim);
}

/* ── the spec bar ───────────────────────────────────────────────────────────
   A GRID, NOT A WRAPPING FLEX ROW. As flex with a wide gap it packed three
   cells onto one line and then wrapped inside each of them; a grid gives every
   value a column and the five cells read as tidy rows at every width. */
.cs-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: var(--sp-6) var(--sp-8);
  margin: var(--sp-9) 0 0;
  padding-block: var(--sp-6);
  border-top: 1px solid var(--cs-line);
  border-bottom: 1px solid var(--cs-line);
}

.cs-meta-cell {
  display: flex;
  flex-flow: column;
  gap: var(--sp-1);
}

.cs-meta dt {
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--cs-dim);
}

.cs-meta dd {
  font-size: var(--fs-sm);
  line-height: var(--lh-label);
  font-weight: 600;
  color: var(--cs-ink);
}

/* ── what this project demonstrates ─────────────────────────────────────────
   The knowledge-graph block, and the only new SECTION in the masthead. It is
   above the fold on a desktop on purpose: "what does this project demonstrate"
   is the question a reader arrives with, and on the Golden Master it is answered
   nowhere on any of thirty documents. */
.cs-demonstrates {
  margin-top: var(--sp-8);
  padding: var(--sp-6);
  border: 1px solid var(--cs-rim);
  border-radius: var(--radius--small);
  background-color: var(--cs-card);
}

.cs-demonstrates-h {
  margin: 0;
  font-size: var(--fs-h4);
  line-height: var(--lh-snug);
}

.cs-demonstrates-p {
  max-width: var(--cs-measure);
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-sm);
  color: var(--cs-dim);
}

.cs-demonstrates-list {
  display: grid;
  gap: var(--sp-4);
  margin-top: var(--sp-5);
}

.cs-dsc-link {
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--cs-accent);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.cs-dsc-note {
  display: block;
  max-width: var(--cs-measure);
  margin-top: var(--sp-1);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--cs-dim);
}

/* ── the full-page capture ──────────────────────────────────────────────────
   ONE FRAME, THE WHOLE CAPTURE, SCROLLED. The strips are between 2,423 and
   3,865 pixels tall against 640 wide, so no single ratio shows one usefully.
   The Golden Master slices each into three or four contiguous grid cells, which
   needs a per-project ratio stamped into a style attribute and shows four
   ribbons of nothing on a phone. This is a window with the image inside it:
   wheel, touch and — because the port is focusable and labelled — the keyboard
   all move it, and the arithmetic disappears. */
.cs-fold {
  margin-top: var(--sp-9);
}

.cs-fold-port {
  height: min(62vh, 34rem);
  padding: 0.3125rem;
  border: 1px solid var(--cs-rim);
  border-radius: 1.5rem;
  background-color: var(--cs-card);
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior-y: contain;
}

.cs-fold-img {
  width: 100%;
  border-radius: 1.1875rem;
}

.cs-fold-cap {
  margin-top: var(--sp-4);
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--cs-dim);
}

/* ── the chapter rail and the article ───────────────────────────────────── */

.cs-layout {
  width: 100%;
  max-width: var(--fluid-container);
  margin-inline: auto;
  padding-inline: var(--padding--container);
  padding-bottom: var(--cs-rhythm);
}

.cs-rail-list {
  display: flex;
  gap: var(--sp-5);
  padding-block: var(--sp-5);
  border-bottom: 1px solid var(--cs-line);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-padding-inline: var(--padding--container);
  overscroll-behavior-x: contain;
}

.cs-rail a {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding-block: var(--sp-3);
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--cs-dim);
  scroll-snap-align: start;
  transition: color var(--duration-base) linear;
}

.cs-rail a b {
  font-weight: inherit;
  color: var(--cs-accent);
}

.cs-rail a:hover,
.cs-rail a:focus-visible,
.cs-rail a.is-here {
  color: var(--cs-ink);
}

/* The progress track. It is a hairline with no runtime and a filled hairline
   with one, so it is never a control that does nothing. */
.cs-rail-line {
  display: none;
  margin: 0;
}

/* Every in-page anchor on this page has to clear the fixed header AND, below
   992, the sticky rail as well. Declared on the sections rather than in the
   scroll runtime so it holds with nothing running. */
.cs-section,
.cs-article {
  scroll-margin-top: calc(var(--on-t-nav-spacer) + var(--sp-6));
}

/* ── sections ───────────────────────────────────────────────────────────── */

.cs-section {
  padding-top: var(--cs-rhythm);
}

.cs-section + .cs-section {
  margin-top: var(--cs-rhythm);
  border-top: 1px solid var(--cs-line);
}

.cs-eyebrow {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin: 0;
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--cs-dim);
}

.cs-eyebrow b {
  font-weight: inherit;
  color: var(--cs-accent);
}

.cs-h2 {
  margin: var(--sp-5) 0 0;
  font-size: var(--fs-h2);
  line-height: var(--lh-display);
  text-transform: uppercase;
  color: var(--cs-ink);
}

.cs-h3 {
  margin: 0;
  font-size: var(--fs-lead);
  line-height: var(--lh-snug);
  font-weight: 600;
  color: var(--cs-ink);
}

.cs-body {
  max-width: var(--cs-measure);
  margin: var(--sp-5) 0 0;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--cs-dim);
}

.cs-note {
  max-width: var(--cs-measure);
  margin: var(--sp-6) 0 0;
  padding: var(--sp-5) var(--sp-6);
  border-inline-start: 2px solid var(--cs-accent);
  border-radius: 0 0.625rem 0.625rem 0;
  background-color: var(--cs-card);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--cs-dim);
}

.cs-note b {
  font-weight: 600;
  color: var(--cs-ink);
}

/* ── numbered steps ─────────────────────────────────────────────────────────
   The number comes from a CSS counter on a real `<ol>`, never from a hand-typed
   `01` in the markup: a list that renumbers itself cannot disagree with itself
   when a step is added or removed in one locale. */
.cs-steps {
  counter-reset: cs-step;
  display: grid;
  margin-top: var(--sp-6);
  border-top: 1px solid var(--cs-line);
}

.cs-step {
  counter-increment: cs-step;
  display: grid;
  grid-template-columns: 3rem minmax(0, 1fr);
  gap: var(--sp-2) var(--sp-4);
  padding-block: var(--sp-6);
  border-bottom: 1px solid var(--cs-line);
}

.cs-step::before {
  content: counter(cs-step, decimal-leading-zero);
  grid-row: 1 / span 2;
  padding-top: 0.25rem;
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  color: var(--cs-accent);
}

.cs-step-p {
  max-width: 56ch;
  margin: 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--cs-dim);
}

/* ── card grids ─────────────────────────────────────────────────────────── */

.cs-grid {
  display: grid;
  gap: var(--sp-5);
  margin-top: var(--sp-6);
}

.cs-card {
  display: flex;
  flex-flow: column;
  gap: var(--sp-3);
  padding: var(--sp-6);
  border: 1px solid var(--cs-rim);
  border-radius: var(--radius--small);
  background-color: var(--cs-card);
}

.cs-card-p {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--cs-dim);
}

.cs-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: auto;
}

.cs-chips li {
  padding: 0.375rem 0.75rem;
  border: 1px solid var(--cs-line);
  border-radius: 999px;
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

/* ── the file tree ──────────────────────────────────────────────────────────
   A THREE-ROW TABLE, NOT ASCII ART. The Golden Master draws it with box glyphs
   and runs of literal spaces held together by `white-space: pre`, which means it
   cannot wrap, has to scroll sideways on a phone, needs a wholesale LTR override
   on the Arabic documents, and reads to a screen reader as a stream of dashes
   and bars. The alignment IS the content, so a grid does it — and the grid
   collapses to two rows on a narrow screen instead of scrolling. */
.cs-tree {
  margin-top: var(--sp-6);
  padding: var(--sp-6);
  border: 1px solid var(--cs-rim);
  border-radius: var(--radius--small);
  background-color: var(--cs-card);
  font-family: var(--cs-mono);
  font-size: var(--fs-cap);
}

.cs-tree-root {
  margin: 0 0 var(--sp-4);
  font-weight: 600;
  color: var(--cs-ink);
}

.cs-tree-list {
  display: grid;
  gap: var(--sp-3);
}

.cs-tree-row {
  display: grid;
  grid-template-columns: 8rem 5rem minmax(0, 1fr);
  gap: var(--sp-2) var(--sp-4);
  align-items: baseline;
}

.cs-tree-name {
  color: var(--cs-ink);
}

.cs-tree-size {
  color: var(--cs-accent);
}

.cs-tree-note {
  color: var(--cs-dim);
}

/* ── challenge and solution ─────────────────────────────────────────────────
   BOTH COLUMNS ARE THE SAME SIZE, WEIGHT AND COLOUR. Only the label of the
   right-hand one takes the accent. Setting the problem column smaller than the
   solution is how a write-up turns its own difficulties into fine print, and
   this page is partly about not doing that. */
.cs-pairs {
  display: grid;
  gap: var(--sp-5);
  margin-top: var(--sp-6);
}

.cs-pair {
  display: grid;
  gap: var(--sp-5);
  padding: var(--sp-6);
  border: 1px solid var(--cs-line);
  border-radius: var(--radius--small);
  background-color: var(--cs-card);
}

.cs-pair-side {
  display: flex;
  flex-flow: column;
  gap: var(--sp-2);
}

.cs-pair-k {
  margin: 0;
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--cs-dim);
}

.is-fix .cs-pair-k {
  color: var(--cs-accent);
}

.cs-pair-t {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--cs-dim);
}

/* ── the live build ─────────────────────────────────────────────────────── */

.cs-demo {
  max-width: 68rem;
  margin: var(--sp-7) 0 0;
}

.cs-demo-chrome {
  padding: 0.3125rem;
  border: 1px solid var(--cs-rim);
  border-radius: 1.5rem;
  background-color: var(--cs-card);
  overflow: clip;
  transition: border-color var(--duration-slow) var(--cs-ease);
}

.cs-demo:hover .cs-demo-chrome {
  border-color: var(--cs-rim-hover);
}

.cs-demo-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  margin: 0;
  padding: 0.625rem 1rem;
}

.cs-demo-lights {
  display: inline-flex;
  gap: var(--sp-2);
}

.cs-demo-lights i {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background-color: var(--cs-line);
}

.cs-demo-lights i:first-child {
  background-color: var(--cs-mark);
}

.cs-demo-url {
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  color: var(--cs-dim);
}

.cs-demo-port {
  position: relative;
  border-radius: 0 0 1.1875rem 1.1875rem;
  background-color: var(--cs-paper);
  aspect-ratio: 1440 / 900;
  overflow: hidden;
  contain: layout paint;
}

.cs-demo-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 0;
  transition: opacity var(--duration-base) linear;
}

/* THE CONTROL IS AN ANCHOR. Without the runtime it navigates to the real
   artifact; with it, the runtime prevents the navigation and mounts the frame
   here instead. Either way it does something. */
.cs-demo-play {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 2;
  padding: 0.875rem 1.5rem;
  border-radius: 999px;
  background-color: var(--cs-mark);
  color: var(--cs-on-mark);
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  transform: translate(-50%, -50%);
  transition: background-color var(--duration-base) var(--cs-ease);
}

.cs-demo-play:hover,
.cs-demo-play:focus-visible {
  background-color: color-mix(in srgb, var(--cs-mark) 82%, var(--cs-ink));
}

/* The frame is rendered at a desktop width and scaled down by `--cs-demo-s`,
   which the runtime writes from a ResizeObserver, so a narrow window shows the
   whole layout rather than a cropped corner of it. */
.cs-demo-frame {
  position: absolute;
  inset: 0 auto auto 0;
  width: 1440px;
  height: 900px;
  border: 0;
  transform: scale(var(--cs-demo-s, 0.5));
  transform-origin: 0 0;
}

[data-cs-demo-port].is-live .cs-demo-poster {
  opacity: 0;
  pointer-events: none;
}

[data-cs-demo-port].is-live .cs-demo-play {
  display: none;
}

.cs-demo-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-4);
  margin-top: var(--sp-4);
}

.cs-demo-cap {
  margin: 0;
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  color: var(--cs-dim);
}

.cs-demo-full {
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--cs-accent);
  text-decoration: underline;
  text-underline-offset: 0.25em;
}

/* ── the way out ────────────────────────────────────────────────────────── */

.cs-next {
  padding-bottom: calc(var(--cs-rhythm) + var(--sp-8));
}

.cs-next-head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-4);
  padding-bottom: var(--sp-5);
  border-bottom: 1px solid var(--cs-line);
}

.cs-next-h {
  margin: 0;
  font-size: var(--fs-h4);
  line-height: var(--lh-tight);
}

.cs-next-sub {
  max-width: 46ch;
  margin: 0;
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--cs-dim);
}

.cs-more {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-5);
  margin-top: var(--sp-6);
}

.cs-more-card {
  display: block;
  padding: 0.25rem;
  border: 1px solid var(--cs-rim);
  border-radius: 1.125rem;
  background-color: var(--cs-card);
  color: var(--cs-ink);
  overflow: clip;
  transition: border-color var(--duration-slow) var(--cs-ease);
}

.cs-more-card:hover,
.cs-more-card:focus-visible {
  border-color: var(--cs-rim-hover);
}

.cs-more-frame {
  position: relative;
  display: block;
  border-radius: 0.875rem;
  aspect-ratio: 8 / 15;
  overflow: clip;
}

.cs-more-frame img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 0;
  filter: saturate(0.94);
  transition: filter var(--duration-slow) var(--cs-ease);
}

.cs-more-card:hover .cs-more-frame img,
.cs-more-card:focus-visible .cs-more-frame img {
  filter: saturate(1.06);
}

/* Half the plates are dark pages, so the ramp finishes opaque or the name lands
   on somebody else's body copy. Ink, not paper, and no taller than the copy
   needs so the shot stays the card. */
.cs-more-info {
  position: absolute;
  inset: auto 0 0;
  display: flex;
  flex-flow: column;
  gap: var(--space-3xs);
  padding: 2.25rem 0.75rem 0.875rem;
  background-image: linear-gradient(
    #080a0a00 0%,
    #080a0a12 12%,
    #080a0a3d 23%,
    #080a0a85 33%,
    #080a0ac7 42%,
    #080a0aeb 51%,
    #080a0af5 62%,
    #080a0af5 100%
  );
}

.cs-more-name {
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  line-height: var(--lh-tight);
  font-weight: 600;
  color: var(--cs-plate-fg);
}

.cs-more-cat {
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--cs-plate-dim);
}

/* The reason the link exists, in the document rather than in a title
   attribute — which is the half of this block a crawler and a screen reader
   both actually read. */
.cs-more-note {
  display: block;
  padding: var(--sp-4) var(--sp-3) var(--sp-3);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--cs-dim);
}

.cs-next-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4) var(--sp-7);
  margin-top: var(--sp-7);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--cs-line);
}

.cs-next-row a {
  font-family: var(--cs-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--cs-dim);
  transition: color var(--duration-base) linear;
}

.cs-next-row a:hover,
.cs-next-row a:focus-visible {
  color: var(--cs-accent);
}

.cs-hire {
  display: flex;
  flex-flow: column;
  align-items: flex-start;
  gap: var(--sp-4);
  margin-top: var(--sp-9);
  padding: var(--sp-8);
  border: 1px solid var(--cs-rim);
  border-radius: var(--radius--small);
  background-color: var(--cs-card);
}

.cs-hire-h {
  margin: 0;
  font-size: var(--fs-h3);
  line-height: var(--lh-snug);
}

.cs-hire-p {
  max-width: var(--cs-measure);
  margin: 0;
  color: var(--cs-dim);
}

/* ── 768 and above ──────────────────────────────────────────────────────── */

@media screen and (min-width: 768px) {
  .cs-grid-2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .cs-grid-3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .cs-pair {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--sp-6);
  }

  .cs-demonstrates-list {
    grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  }
}

/* ── 992 and above: the rail becomes a column ───────────────────────────── */

@media screen and (min-width: 992px) {
  .cs-layout {
    display: grid;
    grid-template-columns: 12rem minmax(0, 1fr);
    column-gap: 4rem;
    align-items: start;
  }

  .cs-rail {
    position: sticky;
    top: calc(var(--on-t-nav-spacer) + var(--sp-6));
    display: flex;
    gap: var(--sp-5);
    margin-top: var(--cs-rhythm);
  }

  /* The reading-progress line. `--cs-p` is written on `.cs-layout` by the
     runtime and inherits down to here; its fallback is 0, so with nothing
     running this is a plain hairline rather than a bar stuck at full. */
  .cs-rail-line {
    position: relative;
    display: block;
    align-self: stretch;
    width: 1px;
    background-color: var(--cs-line);
    overflow: hidden;
  }

  .cs-rail-line::after {
    content: '';
    position: absolute;
    inset: 0;
    background-color: var(--cs-accent);
    transform: scaleY(var(--cs-p, 0));
    transform-origin: top center;
  }

  .cs-rail-list {
    flex-flow: column;
    align-items: flex-start;
    gap: var(--sp-4);
    padding-block: 0;
    border-bottom: 0;
    overflow: visible;
  }

  .cs-rail a {
    padding-block: 0;
    white-space: normal;
  }
}

/* ── 991 and below ──────────────────────────────────────────────────────── */

@media screen and (max-width: 991px) {
  /* THE NAV STOPS CUTTING THE COPY IN HALF.
     The shared header is fixed, transparent and never hides, so on a narrow
     screen the CONNECT pill and the burger sit permanently on top of whatever
     paragraph is scrolling past — and these are the longest documents on the
     site, so it happens for six thousand pixels at a time. This is a SCRIM
     BEHIND THE NAV rather than a change to the nav: the header is shared by all
     sixty-nine routes and giving it an opaque bar everywhere is a site-wide
     redesign nobody asked for. It sits above the page and below the header
     (`--z-nav` is 120), and `pointer-events: none` so it can never eat a tap. */
  .page-project-detail::before {
    content: '';
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    z-index: calc(var(--z-nav) - 10);
    height: calc(var(--on-t-nav-spacer) + var(--sp-5));
    pointer-events: none;
    background-image: linear-gradient(
      var(--cs-paper) 0%,
      var(--cs-paper) 62%,
      color-mix(in srgb, var(--cs-paper) 70%, transparent) 82%,
      color-mix(in srgb, var(--cs-paper) 0%, transparent) 100%
    );
  }

  /* THE RAIL IS THE ONLY NAVIGATION A SIX-THOUSAND-PIXEL PAGE HAS, and as a
     static strip it scrolled away in the first screenful and never came back.
     Sticky under the header, full bleed, and painted so the copy passing behind
     it does not show through. */
  .cs-rail {
    position: sticky;
    top: calc(var(--on-t-nav-spacer) - 1px);
    z-index: var(--z-section);
    margin-inline: calc(var(--padding--container) * -1);
    padding-inline: var(--padding--container);
    background-color: var(--cs-paper);
  }

  /* An anchor now has to clear the header AND the sticky rail above it, or the
     chapter you asked for lands underneath the control you asked with. */
  .cs-section,
  .cs-article {
    scroll-margin-top: calc(var(--on-t-nav-spacer) + var(--sp-10));
  }

  .cs-mast {
    padding-top: calc(var(--on-t-nav-spacer) + var(--sp-6));
    padding-bottom: var(--sp-9);
  }

  .cs-crumb-n {
    display: none;
  }

  .cs-more {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .cs-more li:last-child {
    grid-column: 1 / -1;
  }

  .cs-tree-row {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .cs-tree-note {
    grid-column: 1 / -1;
  }

  .cs-demo {
    max-width: none;
  }
}

/* ── 767 and below ──────────────────────────────────────────────────────── */

@media screen and (max-width: 767px) {
  .cs-fold-port {
    height: min(56vh, 26rem);
  }

  .cs-more {
    grid-template-columns: minmax(0, 1fr);
  }

  .cs-more li:last-child {
    grid-column: auto;
  }

  .cs-more-frame {
    aspect-ratio: 16 / 11;
  }

  .cs-step {
    grid-template-columns: 2.25rem minmax(0, 1fr);
  }

  .cs-hire {
    padding: var(--sp-6);
  }
}

/* ── the entrance ────────────────────────────────────────────────────────
   NOTHING IS HIDDEN UNTIL SOMETHING IS WATCHING. `[data-cs-live]` is set by
   `runtime/pages/project-detail.js` only once it holds an observer over every
   marked element, so a failed script leaves ten long documents fully
   readable rather than blank. */
[data-cs-live] .cs-rise {
  opacity: 0;
  transform: translateY(1.5rem);
  transition:
    opacity 0.8s var(--cs-ease),
    transform 0.8s var(--cs-ease);
  transition-delay: calc(var(--i, 0) * 70ms);
}

[data-cs-live] .cs-rise.is-in {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  [data-cs-live] .cs-rise {
    opacity: 1;
    transform: none;
  }

  .cs-rail-line::after {
    transform: none;
  }
}
}

/* ---- sections · sections/disciplines-index.css ---- */
@layer sections {
/**
 * disciplines-index.css — /disciplines, the hub.
 *
 * Registered in src/styles/order.json before it had content, so that the
 * cascade order is a reviewed decision rather than a side effect of which file
 * was written first.
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * THE PAGE IS FINISHED WITHOUT JAVASCRIPT, AND THAT IS A CASCADE DECISION
 *
 * The Golden Master's version of this page gates every hidden start state,
 * every pin and every scrub behind a `widjs` class set by an inline script on
 * line one of the document. It works, and it costs a render-blocking inline
 * script plus a whole page that only exists once a class arrives.
 *
 * This sheet inverts it. THE SETTLED POSE IS THE DEFAULT and nothing is hidden:
 * the four panels stick by `position: sticky` with no gate at all, the hero
 * shows one complete drawing, every reveal is at full opacity. The runtime adds
 * exactly two things — `[data-wld-live]` on the page root, which arms the
 * entrance transitions, and `--wld-dim` / `--wld-s` per panel, which both have
 * settled fallbacks in the declaration that reads them. Remove the script and
 * the page loses two effects and no content.
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * THE FOUR GROUNDS ARE THE GOLDEN MASTER'S OWN VALUES
 *
 * `#e9eef5`, `#0b1030`, `#07100c`, `#16070f` and their inks are read from
 * `site/assets/css/whatido.css`, not sampled. They are the one part of this
 * page that is art direction rather than system, and each pairs a ground with
 * the ink that belongs on it in the same block — the rule `layout/container.css`
 * states and P6-09 was the cost of breaking.
 */

/* ── page tokens ────────────────────────────────────────────────────────────
   Scoped to the document rather than :root, because they describe THIS page's
   surfaces. The palette is the rebuild's own (`tokens/color.css`); only the
   four world grounds below are legacy hexes, and they are commented as such. */
[data-page='disciplines'] {
  --wld-surface: color-mix(in srgb, var(--color--dark-green) 4%, var(--color--white));
  --wld-surface-2: color-mix(in srgb, var(--color--dark-green) 8%, var(--color--white));
  --wld-line: color-mix(in srgb, var(--color--dark-green) 10%, transparent);
  --wld-rim: color-mix(in srgb, var(--color--dark-green) 18%, transparent);
  --wld-grey: var(--color--dark-green-tint-2);
  --wld-ease: cubic-bezier(0.215, 0.61, 0.355, 1);

  /* The accent, twice. `--wld-accent` is the mint the dark grounds carry;
     `--wld-accent-ink` is the darkened mint that is legible on paper. Mint on
     #fafcfb is 1.5:1 and unreadable; this is the pairing `tokens/color.css`
     already ships for exactly this case. */
  --wld-accent: var(--color--lime);
  /* MEASURED, NOT CHOSEN. `--color--lime-ink` is the cut that stays legible on
     `--color--white` and it is 4.82:1 there — but this page prints it at
     `--fs-micro` on `--wld-surface`, which is 4% ink darker, and the same pair
     measures 4.42:1. That is under the 4.5:1 small-text floor by six
     hundredths, which is exactly the kind of miss a designer never sees and a
     scanner always does. Fifteen per cent towards the page's own ink takes it
     to 5.62:1 and changes nothing anyone can perceive. */
  --wld-accent-ink: color-mix(in srgb, var(--color--lime-ink) 85%, var(--color--dark-green));

  background-color: var(--page-bg);
  color: var(--page-ink);
}

/* One measure for every band on the page. `.c` supplies the gutter and the
   fluid maximum; this narrows it to the page's own column so a paragraph does
   not run to 1,900px at the widest band. */
[data-page='disciplines'] .wld-wrap,
[data-page='disciplines'] .wld-hero-in,
[data-page='disciplines'] .wld-world-in {
  max-width: 82rem;
}

[data-page='disciplines'] .wld-s {
  padding-block: var(--section-y);
}

/* ── type roles ─────────────────────────────────────────────────────────── */

.wld-h1 {
  font-family: var(--font-display);
  font-size: var(--fs-display);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
}

.wld-h2 {
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
  max-width: 22ch;
}

.wld-eyebrow {
  color: var(--wld-grey);
}

.wld-lede {
  margin-top: var(--sp-5);
  max-width: 46ch;
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
}

.wld-body {
  margin-top: var(--sp-5);
  max-width: 62ch;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--wld-grey);
}

/* ── breadcrumb ─────────────────────────────────────────────────────────── */

.wld-crumb-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-cap);
  color: var(--wld-grey);
}

/* The separator is generated rather than authored, so it is not in the
   accessibility tree and a screen reader reads "Home, Disciplines" instead of
   "Home slash Disciplines". */
.wld-crumb-list li + li::before {
  content: '/';
  margin-inline-end: var(--sp-3);
  color: var(--wld-rim);
}

.wld-crumb-list a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.wld-crumb-list [aria-current='page'] {
  color: var(--page-ink);
}

/* ── the hero ───────────────────────────────────────────────────────────── */

.wld-hero {
  position: relative;
  /* THE WASH BELOW BLEEDS 20% PAST THE RIGHT EDGE and this is the only thing
     that stops it becoming a page-wide horizontal scrollbar. `clip` rather
     than `hidden` for the reason `layout/container.css` gives: `hidden` makes
     a scroll container and would re-anchor every sticky descendant. */
  overflow: clip;
  isolation: isolate;
}

/* The tint behind the drawing. It moves with the hero's state so the four
   subjects each get their own light, which is the only thing `data-wld-s` on
   the hero is read for. */
.wld-hero::before {
  content: '';
  position: absolute;
  inset: -10% -20% auto auto;
  width: 78%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(closest-side, var(--wld-hero-tint, transparent), transparent 72%);
  transition: background 1.1s var(--wld-ease);
  pointer-events: none;
  z-index: var(--z-base);
}

.wld-hero[data-wld-s='0'] {
  --wld-hero-tint: rgb(90 120 200 / 17%);
}
.wld-hero[data-wld-s='1'] {
  --wld-hero-tint: rgb(58 78 220 / 16%);
}
.wld-hero[data-wld-s='2'] {
  --wld-hero-tint: rgb(33 241 168 / 20%);
}
.wld-hero[data-wld-s='3'] {
  --wld-hero-tint: rgb(232 60 120 / 16%);
}

.wld-hero-in {
  position: relative;
  z-index: var(--z-raise);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 34rem);
  /* COLUMN GAP ONLY. The row gap is owned by the boxes that need one — the nav's
     own `margin-top` above 991, the drawing's below it — because the two rows
     want different air and a shared `gap` would give the larger of the two to
     both. Before the controls became a row of their own (P8-15) there was only
     one row here and this was a column gap wearing a shorthand. */
  column-gap: clamp(2rem, 5vw, 5rem);
  align-items: center;
  /* THE TWO ROWS PACK AND CENTRE AS A BLOCK. Without this they stretch to fill
     the 88svh minimum, which parked the row of controls at the bottom of the
     viewport with a lake of nothing between it and the lede it belongs to. */
  align-content: center;
  /* The header is fixed, so the first line of the page owes it its own height
     plus air. `--on-t-nav-spacer` is the token that holds it. */
  padding-block: calc(var(--on-t-nav-spacer) + 2rem) var(--section-y);
  min-height: 88svh;
}

.wld-hero-copy > * + * {
  margin-top: var(--sp-4);
}

.wld-hero-art {
  justify-self: end;
  width: 100%;
  max-width: min(34rem, 62vh);
}

.wld-line {
  display: block;
  width: 100%;
  height: auto;
  color: var(--page-ink);

  /* The illustration palette, as four numbers rather than four colours: every
     fill is a mix of the CURRENT ink, so the same drawing reads correctly
     whatever ground it is placed on. */
  --ill-fill: color-mix(in srgb, currentcolor 12%, transparent);
  --ill-fill-3: color-mix(in srgb, currentcolor 26%, transparent);
  --ill-panel: var(--page-bg);
  --ill-accent: var(--wld-accent-ink);
}

/* THE SETTLED POSE: one drawing, complete. Four stacked silhouettes with no
   switcher would be noise, so the other three are out of the render tree
   entirely rather than merely transparent. */
.wld-line-state {
  display: none;
}

.wld-line-state[data-wld-on] {
  display: block;
}

/* ARMED. With the runtime present all four become paintable and the outline
   retracts to nothing; `pathLength="1"` in the markup is what makes
   `stroke-dasharray: 1` mean "exactly one path" on every one of them. */
[data-wld-live] .wld-line-state {
  display: block;
}

[data-wld-live] .wld-line-path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  transition: stroke-dashoffset 0.85s var(--wld-ease);
}

[data-wld-live] .wld-line-state[data-wld-on] .wld-line-path {
  stroke-dashoffset: 0;
}

/* Out immediately, in on a delay — so the outline is most of the way drawn
   before its interior arrives, which is what makes the change read as one
   line becoming something rather than as a cross-fade. */
[data-wld-live] .wld-line-fill {
  opacity: 0;
  transition: opacity 0.25s var(--wld-ease);
}

[data-wld-live] .wld-line-state[data-wld-on] .wld-line-fill {
  opacity: 1;
  transition: opacity 0.5s var(--wld-ease) 0.5s;
}

/* The four controls. Real anchors into the four panels; `[data-on]` marks the
   one whose drawing is showing and is deliberately NOT `aria-current`, because
   the drawing is decorative and "current page" is not what it means.

   ONE ROW, IN THREE LANGUAGES, AT EVERY WIDTH (P8-15).

   `grid-column: 1 / -1` is the fix and the markup move is the other half of it:
   the nav is now a third grid row under BOTH columns rather than the last child
   of the drawing's column. Measured intrinsic width of the four pills against
   the space they had:

                        need EN   need DE   need AR   had (art col)   has now
       1440                507       495       564         453          1093
       1280                488       477       545         403           972
        992                469       457       524         374           902

   Arabic is the widest even though the Arabic ramp sets 4% smaller, because the
   Arabic face sets wide and the words are longer. `nowrap` is therefore a promise
   this can now keep rather than an instruction to overflow.

   THE 564px WAS MEASURED ON CAIRO AND P10 REPLACED IT WITH ZAIN. The prose
   family carries `size-adjust: 112%`, chosen to hold Zain's tooth height at
   Cairo's, so the measure should be within a percent of the figure above — but
   `has now` is 1,093 against a `need` of 564, nearly double, so the row is not
   close to its own bound and a percent either way cannot break it. Re-measure
   here only if that headroom is ever spent.

   BELOW 480 THE ROW SCROLLS RATHER THAN SHRINKING FURTHER. At 390 the copy
   column is 368px and the smallest legible Arabic row is 390 — 22px over. The
   alternatives were dropping the `01`–`04` index (which is half of what the
   control says) or a pill below the 44px target. A scroll strip keeps both, and
   the negative margin lets the first pill start at the page gutter while the row
   bleeds to the edge; `.wld-hero` is `overflow: clip`, so it cannot leak. */
.wld-line-nav {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: nowrap;
  gap: var(--sp-3);
  margin-top: var(--sp-6);
}

.wld-line-nav a {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 0.5ch;
  min-height: 2.75rem;
  box-sizing: border-box;
  padding: 0.5rem 0.9rem;
  white-space: nowrap;
  border: 1px solid var(--wld-line);
  border-radius: 999px;
  font-size: var(--fs-cap);
  line-height: var(--lh-flat);
  color: var(--wld-grey);
  text-decoration: none;
  transition:
    color 0.3s var(--wld-ease),
    border-color 0.3s var(--wld-ease),
    background-color 0.3s var(--wld-ease);
}

.wld-line-nav a b {
  font-weight: 600;
  color: var(--wld-accent-ink);
}

.wld-line-nav a:hover,
.wld-line-nav a[data-on] {
  color: var(--page-ink);
  border-color: var(--wld-rim);
  background-color: var(--wld-surface);
}

/* ── the four worlds ────────────────────────────────────────────────────── */

.wld-worlds {
  position: relative;
}

/* THE PIN IS NATIVE AND UNGATED. Four siblings, each sticking at the top, so
   panel n+1 slides over panel n with nothing running. The homepage deck does
   the same thing with the same reasoning (`home-disciplines.css`). */
.wld-world {
  position: sticky;
  top: 0;
  min-height: 100svh;
  display: flex;
  align-items: center;
  overflow: clip;
  isolation: isolate;
  background-color: var(--wld-w-bg);
  color: var(--wld-w-ink);
}

/* Each panel one step over the one before it. Sticky boxes are positioned, so
   tree order already paints them this way — the ladder is declared anyway
   because the whole effect depends on it and a reorder must not be able to
   break it silently. */
.wld-world:nth-child(1) {
  z-index: var(--z-raise);
}
.wld-world:nth-child(2) {
  z-index: calc(var(--z-raise) * 2);
}
.wld-world:nth-child(3) {
  z-index: calc(var(--z-raise) * 3);
}
.wld-world:nth-child(4) {
  z-index: calc(var(--z-raise) * 4);
}

/* THE FOUR GROUNDS, verbatim from the Golden Master. Each block pairs a ground
   with its ink, its soft ink and its rim, so no subtree inherits a colour from
   whatever the page happened to be resting at. */
.wld-world.is-web {
  --wld-w-bg: #e9eef5;
  --wld-w-ink: #0a0f18;
  --wld-w-soft: #33405a;
  --wld-w-rim: rgb(10 15 24 / 16%);
  --wld-w-tint: rgb(90 120 200 / 16%);
  /* Mint fails on this pale ground at 1.5:1; the darkened cut passes. */
  --wld-w-acc: var(--wld-accent-ink);
}

.wld-world.is-mobile {
  --wld-w-bg: #0b1030;
  --wld-w-ink: #eaf0ff;
  --wld-w-soft: #a8b4dd;
  --wld-w-rim: rgb(234 240 255 / 22%);
  --wld-w-tint: rgb(90 120 255 / 20%);
  --wld-w-acc: var(--wld-accent);
}

.wld-world.is-shop {
  --wld-w-bg: #07100c;
  --wld-w-ink: #e9f7f0;
  --wld-w-soft: #9fc4b3;
  --wld-w-rim: rgb(233 247 240 / 22%);
  --wld-w-tint: rgb(33 241 168 / 13%);
  --wld-w-acc: var(--wld-accent);
}

.wld-world.is-brand {
  --wld-w-bg: #16070f;
  --wld-w-ink: #fbeef4;
  --wld-w-soft: #d3a9bd;
  --wld-w-rim: rgb(251 238 244 / 22%);
  --wld-w-tint: rgb(232 60 120 / 18%);
  --wld-w-acc: var(--wld-accent);
}

.wld-world::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(120% 80% at 78% 30%, var(--wld-w-tint), transparent 62%);
  pointer-events: none;
}

/* THE DEPTH WASH. `--wld-dim` is the only thing the runtime writes here and its
   fallback is 0 — a panel that nothing is rising over is not washed out, which
   is the finished pose rather than a hidden element waiting for a controller.
   The wash is painted in the panel's OWN ground, so the outgoing panel fades
   into itself rather than towards an unrelated colour. */
.wld-world::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: var(--z-raise);
  background-color: var(--wld-w-bg);
  opacity: var(--wld-dim, 0);
  pointer-events: none;
}

.wld-world-in {
  position: relative;
  z-index: var(--z-raise);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 30rem);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
  padding-block: calc(var(--on-t-nav-spacer) + 2rem) clamp(3rem, 7vh, 4.5rem);
  /* `--wld-s` is the runtime's second and last write. Fallback 1 = unscaled. */
  transform: scale(var(--wld-s, 1));
  transform-origin: 50% 40%;
}

.wld-world-n {
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--wld-w-acc);
}

.wld-world-t {
  margin-top: var(--sp-4);
  font-family: var(--font-display);
  font-size: var(--fs-h1);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  /* 12ch, not 11: `Grafikdesign` and `التصميم الجرافيكي` are both longer than
     the English word this box was first measured against. */
  max-width: 12ch;
}

.wld-world-d {
  margin-top: var(--sp-5);
  max-width: 42ch;
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--wld-w-soft);
}

.wld-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-6);
  padding: 0;
  list-style: none;
}

.wld-chip {
  min-height: 1.75rem;
  box-sizing: border-box;
  padding: 0.4rem 0.75rem;
  border: 1px solid var(--wld-w-rim);
  border-radius: 999px;
  font-size: var(--fs-cap);
  line-height: var(--lh-flat);
  color: var(--wld-w-ink);
}

.wld-world-go {
  display: inline-flex;
  align-items: center;
  gap: 0.6ch;
  min-height: 2.75rem;
  box-sizing: border-box;
  margin-top: var(--sp-7);
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--wld-w-ink);
  text-decoration: underline;
  text-underline-offset: 0.25em;
  text-decoration-color: var(--wld-w-rim);
  transition: text-decoration-color 0.3s var(--wld-ease);
}

/* The arrow is generated, so it never reaches the accessibility tree and never
   becomes part of the link's name. A mask rather than a background image so it
   inherits the link's colour on every one of the four grounds. */
.wld-world-go::after {
  content: '';
  width: 1.1em;
  height: 1.1em;
  background-color: currentcolor;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E")
    center / contain no-repeat;
  transition: transform 0.3s var(--wld-ease);
}

.wld-world-go:hover {
  text-decoration-color: currentcolor;
}

.wld-world-go:hover::after {
  transform: translateX(0.25rem);
}

.wld-world-art {
  justify-self: end;
  width: 100%;
  max-width: min(28rem, 52vh);
}

.wld-world-img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--wld-w-rim);
  border-radius: var(--radius--small);
}

/* ── the shared practice ────────────────────────────────────────────────── */

/* A hairline plate: one background, 1px gaps, and cells that paint over it. It
   is one border and one gap rather than sixteen borders that would double up
   against each other. */
.wld-terms {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1px;
  margin-top: clamp(2.5rem, 6vw, 4rem);
  padding: 0;
  list-style: none;
  background-color: var(--wld-line);
  border: 1px solid var(--wld-line);
  border-radius: 1.25rem;
  overflow: clip;
}

.wld-term {
  background-color: var(--page-bg);
  padding: var(--sp-6);
}

.wld-term-t {
  font-size: var(--fs-h4);
  font-weight: 600;
  line-height: var(--lh-snug);
  letter-spacing: var(--track-tight);
}

.wld-term-b {
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--wld-grey);
}

/* ── how it gets checked ────────────────────────────────────────────────── */

.wld-measure-in {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  padding: clamp(2rem, 4vw, 3.25rem);
  border: 1px solid var(--wld-line);
  border-radius: 1.5rem;
  background-color: var(--wld-surface);
}

.wld-measure-copy > * + * {
  margin-top: var(--sp-4);
}

.wld-measure-list-t {
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--wld-grey);
}

.wld-measure-list ul {
  margin-top: var(--sp-5);
  padding: 0;
  list-style: none;
}

.wld-measure-list li {
  position: relative;
  padding-inline-start: 1.75rem;
  padding-block: var(--sp-3);
  border-top: 1px solid var(--wld-line);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
}

/* A tick, drawn as a mask so it takes the accent from the cascade and reaches
   no accessibility tree. `.wld-measure-list li` is a plain block, so this
   absolute child is positioned against real text rather than against a flex
   line — the same rule the discipline sheet's list follows. */
.wld-measure-list li::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  top: 0.9em;
  width: 1.0625rem;
  height: 1.0625rem;
  background-color: var(--wld-accent-ink);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='m8.5 12.2 2.5 2.5 4.5-5'/%3E%3C/svg%3E")
    center / contain no-repeat;
}

/* UNDERLINED, and that is a requirement rather than a style. A link inside a
   paragraph must be distinguishable from the text around it by something other
   than colour unless the two differ by 3:1 — `--page-ink` against `--wld-grey`
   measures 2.68:1, so the underline is what carries it (WCAG 1.4.1). */
.wld-body a {
  color: var(--page-ink);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ── proof ──────────────────────────────────────────────────────────────── */

.wld-proof-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-5);
  margin-top: clamp(2rem, 5vw, 3rem);
}

.wld-proof-card img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 600 / 850;
  object-fit: cover;
  border-radius: var(--radius--small);
  background-color: var(--wld-surface-2);
}

.wld-proof-b {
  margin-top: var(--sp-4);
}

.wld-proof-b h3 {
  font-size: var(--fs-h4);
  font-weight: 600;
  line-height: var(--lh-snug);
}

.wld-proof-b h3 a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-color: var(--wld-rim);
}

.wld-proof-b h3 a:hover {
  text-decoration-color: currentcolor;
}

.wld-proof-b p {
  margin-top: var(--sp-2);
  font-size: var(--fs-cap);
  color: var(--wld-grey);
}

/* ── the close ──────────────────────────────────────────────────────────── */

.wld-cta {
  padding-block: clamp(4.5rem, 12vh, 9rem);
  background: linear-gradient(
    180deg,
    var(--page-bg) 0%,
    color-mix(in srgb, var(--color--lime) 12%, var(--page-bg)) 58%,
    var(--page-bg) 100%
  );
}

.wld-cta-h {
  margin-top: var(--sp-4);
  font-family: var(--font-display);
  font-size: var(--fs-h1);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  max-width: 18ch;
}

.wld-cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  margin-top: var(--sp-8);
}

.wld-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 3rem;
  box-sizing: border-box;
  padding-inline: 1.5rem;
  border: 1px solid var(--wld-line);
  border-radius: 999px;
  background-color: var(--wld-surface);
  font-size: var(--fs-body);
  font-weight: 600;
  line-height: var(--lh-flat);
  color: var(--page-ink);
  text-decoration: none;
  transition:
    background-color 0.3s var(--wld-ease),
    color 0.3s var(--wld-ease);
}

.wld-pill.is-dark {
  background-color: var(--color--dark-green);
  border-color: var(--color--dark-green);
  color: var(--color--white);
}

.wld-pill:hover {
  background-color: var(--color--lime);
  border-color: var(--color--lime);
  color: var(--color--dark-green);
}

/* ── the entrance ───────────────────────────────────────────────────────────
   NOTHING IS HIDDEN UNTIL SOMETHING IS WATCHING. The start state lives behind
   `[data-wld-live]`, which `runtime/pages/disciplines.js` sets on the root only
   after it has an IntersectionObserver holding every marked element. A page
   whose text is invisible when the script fails is a page that has traded its
   content for an effect. */
[data-wld-live] .wld-reveal {
  opacity: 0;
  transform: translateY(1.25rem);
  transition:
    opacity 0.8s var(--wld-ease),
    transform 0.8s var(--wld-ease);
  transition-delay: calc(var(--i, 0) * 80ms);
}

[data-wld-live] .wld-reveal.is-in {
  opacity: 1;
  transform: none;
}

/* ── 991 and below ──────────────────────────────────────────────────────── */

@media screen and (max-width: 991px) {
  .wld-hero-in,
  .wld-world-in,
  .wld-measure-in {
    grid-template-columns: minmax(0, 1fr);
  }

  .wld-hero-in {
    min-height: 0;
    padding-bottom: clamp(3rem, 8vh, 5rem);
  }

  .wld-hero-art {
    justify-self: start;
    max-width: 22rem;
    margin-top: var(--sp-7);
  }

  /* The picture goes above the words on a narrow screen, which is where the
     desktop composition already puts it optically. `order` moves the box and
     not the DOM, so the reading and tab order are untouched. */
  .wld-world-art {
    justify-self: start;
    order: -1;
    max-width: min(15rem, 30vh);
  }

  .wld-world-in {
    align-content: center;
    gap: var(--sp-6);
    padding-bottom: clamp(2.5rem, 6vh, 4rem);
  }

  .wld-terms {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .wld-proof-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ── 767 and below ──────────────────────────────────────────────────────── */

@media screen and (max-width: 767px) {
  .wld-proof-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .wld-world-d {
    max-width: 38ch;
  }

  /* One size step for the pills, and no more than one. `min-height` is NOT
     reduced with them: the fluid root font already floors it at 30.25px below
     ~1188, which is the smallest this control may be. */
  .wld-line-nav {
    gap: 6px;
  }

  .wld-line-nav a {
    gap: 0.35ch;
    padding-inline: 0.55rem;
    font-size: var(--fs-micro);
  }
}

/* ── 479 and below ──────────────────────────────────────────────────────── */

@media screen and (max-width: 479px) {
  .wld-terms {
    grid-template-columns: minmax(0, 1fr);
  }

  /* THE ROW BLEEDS AND SCROLLS. See the note on `.wld-line-nav` — at 390 the
     Arabic row is 390px inside a 368px column, and the two ways to close 22px
     without a scroller both cost something the control needs. The negative
     margin puts the strip's edges at the viewport's while the first pill still
     starts on the page gutter, so a partly-visible fourth pill is the affordance
     rather than a cut-off one. `proximity`, not `mandatory`: this is a strip to
     glance along, not a carousel to page through.

     THE SCROLLBAR IS NOT HIDDEN, and that is the site's rule rather than an
     oversight — `tests/unit/chrome-contract.test.mjs` bans `scrollbar-width:
     none` and `::-webkit-scrollbar` anywhere in `src/styles`, because the
     browser's own scrollbar is the only affordance a scroller is guaranteed to
     have. The projects rail makes the same choice. Every device this block can
     reach draws an overlay scrollbar that fades, so the cost is a moment of
     honesty rather than a permanent bar. */
  .wld-line-nav {
    margin-inline: calc(var(--padding--container) * -1);
    padding-inline: var(--padding--container);
    overflow-x: auto;
    scroll-snap-type: x proximity;
  }

  .wld-line-nav a {
    scroll-snap-align: start;
  }
}

/* ── reduced motion ─────────────────────────────────────────────────────────
   `base/reset.css` already kills every transition and animation site-wide under
   the preference, and it does it in the FIRST cascade layer so nothing later
   can out-rank it. What that switch cannot do is undo LAYOUT: a `position:
   sticky` stack still travels, and a transform written by a controller still
   holds. So the two mechanisms are unwound here.

   The runtime does the same from its side (`disciplines.js` never starts a
   scrub under `reduce`), and both are needed: this handles the preference being
   turned on mid-session with values already written. */
@media (prefers-reduced-motion: reduce) {
  .wld-world {
    position: relative;
    top: auto;
  }

  .wld-world::after {
    opacity: 0;
  }

  .wld-world-in {
    transform: none;
  }

  [data-wld-live] .wld-reveal {
    opacity: 1;
    transform: none;
  }

  [data-wld-live] .wld-line-state {
    display: none;
  }

  [data-wld-live] .wld-line-state[data-wld-on] {
    display: block;
  }

  [data-wld-live] .wld-line-path {
    stroke-dasharray: none;
    stroke-dashoffset: 0;
  }

  [data-wld-live] .wld-line-fill,
  [data-wld-live] .wld-line-state[data-wld-on] .wld-line-fill {
    opacity: 1;
  }
}
}

/* ---- sections · sections/discipline-detail.css ---- */
@layer sections {
/**
 * discipline-detail.css — the four documents at /disciplines/<slug>.
 *
 * Registered in src/styles/order.json before it had content, so that the
 * cascade order is a reviewed decision rather than a side effect of which file
 * was written first.
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * ONE SHEET, FOUR GROUNDS, AND THE GROUND COMES FROM THE COLLECTION
 *
 * `data-discipline` on the page wrapper carries `item.id` — the collection
 * member's own structural fact — because `pageId` cannot: all four documents
 * share one `pageId` by design, since they share one behaviour. Six custom
 * properties are set from it and everything downstream reads those, so adding a
 * fifth discipline is one block here rather than a pass over the whole file.
 *
 * Each block sets a ground AND the ink that belongs on it, which is the rule
 * `layout/container.css` states and P6-09 was the cost of breaking.
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * THE SIGNATURE SCRUB ADDS NO PAGE HEIGHT
 *
 * The Golden Master's four signature mechanisms pin a stage and buy the travel
 * with document height — 300vh, 380vh and 420vh on three of the four pages.
 * That works, and it means the reduced-motion path has to give three whole
 * viewport-heights of scroll back, which its stylesheet does by hand.
 *
 * This one does not borrow the height in the first place. The stage's plate is
 * `position: sticky` inside a section of its own natural height, and progress
 * is measured from that section's travel THROUGH the viewport rather than
 * through a pin. The effect is the same — the numeral tracks the list beside it
 * — the page is 300vh shorter, and under `prefers-reduced-motion` there is no
 * height to unwind: only a `sticky` to turn off.
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * EVERY SETTLED POSE IS THE FINISHED ONE
 *
 * `--dsc-p` is read in two places and both declare the fallback that means
 * "done": the progress rail reads `var(--dsc-p, 0)` because a rail that has not
 * been told anything is at its start, and the fork's stroke reads
 * `var(--dsc-p, 1)` because a line that has not been told anything must be
 * fully drawn. `data-dsc-i` is absent until the runtime writes it, so step one
 * is active and every other step is legible but unmarked. Nothing on these four
 * pages is invisible without JavaScript.
 */

/* ── page tokens ────────────────────────────────────────────────────────── */

.dsc-page {
  --dsc-surface: color-mix(in srgb, var(--color--dark-green) 4%, var(--color--white));
  --dsc-line: color-mix(in srgb, var(--color--dark-green) 10%, transparent);
  --dsc-rim: color-mix(in srgb, var(--color--dark-green) 18%, transparent);
  --dsc-grey: var(--color--dark-green-tint-2);
  --dsc-ease: cubic-bezier(0.215, 0.61, 0.355, 1);
  /* MEASURED. `--color--lime-ink` is 4.82:1 on `--color--white` and 4.42:1 on
     this page's card surface, which is 4% darker — under the 4.5:1 small-text
     floor, and every use of it here is `--fs-micro`. Fifteen per cent towards
     the page's ink takes it to 5.62:1. */
  --dsc-accent-ink: color-mix(in srgb, var(--color--lime-ink) 85%, var(--color--dark-green));

  background-color: var(--page-bg);
  color: var(--page-ink);
}

/* THE FOUR GROUNDS, verbatim from the Golden Master's own stylesheet — the same
   four the hub gives these disciplines, so a reader who arrives from
   /disciplines lands on a page the colour of the panel they clicked. */
.dsc-page[data-discipline='websites'] {
  --g-bg: #e9eef5;
  --g-ink: #0a0f18;
  --g-soft: #33405a;
  --g-rim: rgb(10 15 24 / 16%);
  --g-tint: rgb(90 120 200 / 16%);
  /* The only one of the four that is not mint: this ground is pale, and mint on
     it measures 1.5:1. The darkened cut in `tokens/color.css` exists for this. */
  --g-acc: var(--color--lime-ink);
}

.dsc-page[data-discipline='mobile-apps'] {
  --g-bg: #0b1030;
  --g-ink: #eaf0ff;
  --g-soft: #a8b4dd;
  --g-rim: rgb(234 240 255 / 22%);
  --g-tint: rgb(90 120 255 / 20%);
  --g-acc: var(--color--lime);
}

.dsc-page[data-discipline='e-commerce'] {
  --g-bg: #07100c;
  --g-ink: #e9f7f0;
  --g-soft: #9fc4b3;
  --g-rim: rgb(233 247 240 / 22%);
  --g-tint: rgb(33 241 168 / 13%);
  --g-acc: var(--color--lime);
}

.dsc-page[data-discipline='graphic-design'] {
  --g-bg: #16070f;
  --g-ink: #fbeef4;
  --g-soft: #d3a9bd;
  --g-rim: rgb(251 238 244 / 22%);
  --g-tint: rgb(232 60 120 / 18%);
  --g-acc: var(--color--lime);
}

/* One measure for every band. `.c` supplies the gutter and the fluid maximum. */
.dsc-page .dsc-wrap {
  max-width: 82rem;
}

.dsc-page .dsc-s {
  padding-block: var(--section-y);
}

/* ── type roles ─────────────────────────────────────────────────────────── */

.dsc-h1 {
  margin-top: var(--sp-4);
  font-family: var(--font-display);
  font-size: var(--fs-display);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  max-width: 18ch;
}

.dsc-h2 {
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
  max-width: 26ch;
}

.dsc-eyebrow {
  color: var(--dsc-grey);
}

.dsc-lede {
  margin-top: var(--sp-5);
  max-width: 52ch;
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--g-soft);
}

.dsc-body {
  margin-top: var(--sp-5);
  max-width: 68ch;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--dsc-grey);
}

/* UNDERLINED. A link inside a paragraph needs a non-colour distinction unless
   it differs from the surrounding text by 3:1; `--page-ink` on `--dsc-grey`
   measures 2.68:1 (WCAG 1.4.1). */
.dsc-body a {
  color: var(--page-ink);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ── breadcrumb ─────────────────────────────────────────────────────────── */

.dsc-crumb-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-cap);
  color: var(--g-soft);
}

/* Generated, so the accessibility tree reads three names rather than three
   names and two slashes. */
.dsc-crumb-list li + li::before {
  content: '/';
  margin-inline-end: var(--sp-3);
  color: var(--g-rim);
}

.dsc-crumb-list a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.dsc-crumb-list [aria-current='page'] {
  color: var(--g-ink);
}

/* ── the hero ───────────────────────────────────────────────────────────── */

.dsc-hero {
  position: relative;
  isolation: isolate;
  background-color: var(--g-bg);
  color: var(--g-ink);
  /* The tint below is an inset pseudo, so nothing bleeds — but the hero image
     is a large box in a grid track and `clip` is the cheap guarantee that a
     rounding error at 390 cannot become a page-wide scrollbar. `clip` rather
     than `hidden` for the reason `layout/container.css` gives. */
  overflow: clip;
  /* Real padding rather than a child margin: `overflow: clip` does not make a
     block formatting context, so a margin here would collapse straight out of
     the coloured box and leave a white band under it. */
  padding-block: calc(var(--on-t-nav-spacer) + 2rem) clamp(2.5rem, 6vh, 4rem);
}

.dsc-hero::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(115% 78% at 82% 24%, var(--g-tint), transparent 62%);
  pointer-events: none;
  z-index: var(--z-base);
}

.dsc-hero > .c {
  position: relative;
  z-index: var(--z-raise);
}

.dsc-hero-in {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 26rem);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
  padding-block: clamp(2rem, 5vh, 3rem) clamp(3rem, 8vh, 5.5rem);
}

.dsc-hero-n {
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--g-acc);
}

.dsc-hero-art {
  justify-self: end;
  width: 100%;
  max-width: min(26rem, 52vh);
}

.dsc-hero-img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--g-rim);
  border-radius: var(--radius--small);
}

/* THE PLATE: five terms and their definitions, so a description list. One
   painted background with 1px gaps over it, rather than five borders that
   double up against each other at every seam.

   FIVE NAMED TRACKS, NEVER AUTO-FIT. Every one of the four pages has exactly
   five rows; an auto-fit that silently reflows to four is a plate nobody
   notices has lost one. */
.dsc-spec {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 1px;
  margin: 0;
  background-color: var(--g-rim);
  border: 1px solid var(--g-rim);
  border-radius: 1rem;
  overflow: clip;
  counter-reset: dsc-spec;
}

.dsc-spec-cell {
  counter-increment: dsc-spec;
  padding: 1.35rem 1.35rem 1.6rem;
  background-color: color-mix(in srgb, var(--g-ink) 4%, var(--g-bg));
}

.dsc-spec-cell::before {
  content: counter(dsc-spec, decimal-leading-zero);
  display: block;
  margin-bottom: var(--sp-4);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  color: var(--g-acc);
}

.dsc-spec dt {
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--g-soft);
}

.dsc-spec dd {
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--g-ink);
}

/* ── the capability cards ───────────────────────────────────────────────── */

.dsc-cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-5);
  margin-top: clamp(2.5rem, 6vw, 4rem);
}

.dsc-card {
  padding: var(--sp-6);
  border: 1px solid var(--dsc-line);
  border-radius: 1.25rem;
  background-color: var(--dsc-surface);
}

.dsc-card-k {
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--dsc-accent-ink);
}

.dsc-card-t {
  margin-top: var(--sp-4);
  font-size: var(--fs-h4);
  font-weight: 600;
  line-height: var(--lh-snug);
  letter-spacing: var(--track-tight);
}

.dsc-card-b {
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--dsc-grey);
}

/* ── the signature band ─────────────────────────────────────────────────── */

.dsc-band {
  position: relative;
  isolation: isolate;
  background-color: var(--g-bg);
  color: var(--g-ink);
  padding-block: clamp(4rem, 10vh, 7rem);
  /* NO `overflow: clip` HERE, deliberately: it would make this box the nearest
     scrollport for the sticky plate inside it and the pin would stop working.
     The tint is an inset pseudo and nothing else bleeds, so nothing needs it. */
}

.dsc-band::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(110% 70% at 20% 18%, var(--g-tint), transparent 60%);
  pointer-events: none;
  z-index: var(--z-base);
}

.dsc-band > .c {
  position: relative;
  z-index: var(--z-raise);
}

.dsc-band .dsc-body,
.dsc-band .dsc-eyebrow {
  color: var(--g-soft);
}

.dsc-steps {
  position: relative;
  z-index: var(--z-raise);
  margin-top: clamp(2.5rem, 6vh, 4rem);
}

.dsc-steps-stage {
  display: grid;
  grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: start;
}

/* THE PLATE STICKS; THE PAGE DOES NOT GROW. `top` clears the fixed header. */
.dsc-plate {
  position: sticky;
  top: calc(var(--on-t-nav-spacer) + 1.5rem);
  display: grid;
  /* Every figure occupies the same cell, so the box does not resize as the
     numeral changes and the list beside it never jumps. */
  grid-template-areas: 'figure';
  align-items: center;
  aspect-ratio: 4 / 3;
  padding: var(--sp-6);
  border: 1px solid var(--g-rim);
  border-radius: 1.25rem;
  background-color: color-mix(in srgb, var(--g-ink) 4%, var(--g-bg));
}

/* THE RAIL. The second and last consumer of `--dsc-p`, and the one that makes
   the property visible rather than only inferable: it fills the plate's lower
   edge with the section's own travel. FALLBACK 0 — a rail nothing has told
   anything is at its start, which is exactly where step one belongs. */
.dsc-plate::after {
  content: '';
  grid-area: figure;
  align-self: end;
  height: 2px;
  border-radius: 1px;
  background: linear-gradient(
    to right,
    var(--g-acc) calc(var(--dsc-p, 0) * 100%),
    var(--g-rim) calc(var(--dsc-p, 0) * 100%)
  );
}

.dsc-figure {
  grid-area: figure;
  display: flex;
  align-items: baseline;
  gap: 0.4ch;
  /* THE SETTLED POSE IS STEP ONE. Every figure is transparent and the first is
     painted, so a page with no runtime shows `01 / 06` rather than six numerals
     stacked on each other. */
  opacity: 0;
  transition: opacity 0.45s var(--dsc-ease);
}

.dsc-figure:first-child {
  opacity: 1;
}

/* The runtime writes `data-dsc-i` and the pairing below is the whole state
   machine. Six rules because six is the longest sequence any of the four has
   (graphic design's sheet); a seventh step would need a seventh line, which is
   the honest cost of having no conditionals and is two seconds of work. */
[data-dsc-i='0'] .dsc-figure:first-child,
[data-dsc-i='1'] .dsc-figure:nth-child(2),
[data-dsc-i='2'] .dsc-figure:nth-child(3),
[data-dsc-i='3'] .dsc-figure:nth-child(4),
[data-dsc-i='4'] .dsc-figure:nth-child(5),
[data-dsc-i='5'] .dsc-figure:nth-child(6) {
  opacity: 1;
}

[data-dsc-i='1'] .dsc-figure:first-child,
[data-dsc-i='2'] .dsc-figure:first-child,
[data-dsc-i='3'] .dsc-figure:first-child,
[data-dsc-i='4'] .dsc-figure:first-child,
[data-dsc-i='5'] .dsc-figure:first-child {
  opacity: 0;
}

.dsc-figure-n {
  font-family: var(--font-display);
  font-size: var(--fs-impact);
  font-weight: 600;
  line-height: var(--lh-impact);
  letter-spacing: var(--track-display);
  color: var(--g-acc);
}

.dsc-figure-of {
  font-size: var(--fs-h4);
  color: var(--g-soft);
}

.dsc-steps-read {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The marker hangs off the READING edge, so both halves are logical: an
   Arabic step's rule sits to its right, where the line begins. A physical pair
   here would put the rule under the last word instead of before the first. */
.dsc-steps-read li {
  position: relative;
  padding-inline-start: 2rem;
  padding-block: var(--sp-6);
}

/* The rule beside the active step. It is the marker, and it is the ONLY thing
   that changes — the inactive steps keep their full ink. Fading prose to 30% to
   make a neighbour stand out trades a contrast failure for an effect. */
.dsc-steps-read li::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  top: var(--sp-6);
  bottom: var(--sp-6);
  width: 2px;
  border-radius: 1px;
  background-color: var(--g-rim);
  transition: background-color 0.45s var(--dsc-ease);
}

.dsc-steps-read li:first-child::before {
  background-color: var(--g-acc);
}

[data-dsc-i='0'] .dsc-steps-read li:first-child::before,
[data-dsc-i='1'] .dsc-steps-read li:nth-child(2)::before,
[data-dsc-i='2'] .dsc-steps-read li:nth-child(3)::before,
[data-dsc-i='3'] .dsc-steps-read li:nth-child(4)::before,
[data-dsc-i='4'] .dsc-steps-read li:nth-child(5)::before,
[data-dsc-i='5'] .dsc-steps-read li:nth-child(6)::before {
  background-color: var(--g-acc);
}

[data-dsc-i='1'] .dsc-steps-read li:first-child::before,
[data-dsc-i='2'] .dsc-steps-read li:first-child::before,
[data-dsc-i='3'] .dsc-steps-read li:first-child::before,
[data-dsc-i='4'] .dsc-steps-read li:first-child::before,
[data-dsc-i='5'] .dsc-steps-read li:first-child::before {
  background-color: var(--g-rim);
}

.dsc-step-n {
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  color: var(--g-acc);
}

.dsc-step-t {
  margin-top: var(--sp-3);
  font-size: var(--fs-h4);
  font-weight: 600;
  line-height: var(--lh-snug);
  color: var(--g-ink);
}

.dsc-step-b {
  margin-top: var(--sp-3);
  max-width: 56ch;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--g-soft);
}

/* ── the fork ───────────────────────────────────────────────────────────── */

.dsc-fork {
  position: relative;
  margin-top: clamp(2.5rem, 6vh, 4rem);
  color: var(--g-ink);
}

.dsc-fork-line {
  display: block;
  width: 100%;
  height: auto;
  color: var(--g-rim);
}

/* `pathLength="1"` in the markup is what makes a dasharray of 1 mean exactly
   one path, whatever the real length is. THE FALLBACK IS 1, not 0: a line that
   nothing has told anything must be fully drawn, because that is the finished
   pose and this page has to be finished with no runtime. */
.dsc-fork-path {
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--dsc-p, 1));
}

.dsc-fork-cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.5rem, 4vw, 3rem);
}

.dsc-fork-branch {
  padding: clamp(1.5rem, 3vw, 2.25rem);
  border: 1px solid var(--g-rim);
  border-radius: 1.25rem;
  background-color: color-mix(in srgb, var(--g-ink) 5%, transparent);
}

.dsc-fork-k {
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--g-acc);
}

.dsc-fork-h {
  margin-top: var(--sp-4);
  font-size: var(--fs-h4);
  font-weight: 600;
  line-height: var(--lh-snug);
  color: var(--g-ink);
}

.dsc-fork-branch .dsc-list {
  margin-top: var(--sp-5);
  color: var(--g-soft);
}

.dsc-rejoin {
  margin-top: clamp(2.5rem, 6vh, 4rem);
  padding: clamp(1.5rem, 3vw, 2.25rem);
  border-radius: 1.25rem;
  background-color: color-mix(in srgb, var(--g-ink) 8%, transparent);
}

.dsc-rejoin p {
  max-width: 70ch;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--g-ink);
}

/* ── the list primitive ─────────────────────────────────────────────────────
   Used by the scope columns, the fork branches and the numbered method note.

   THE `<li>` IS A PLAIN BLOCK AND ITS MARKER IS ABSOLUTE. An `<li>` that is
   itself a flex or grid container cannot hold loose text beside an icon without
   the text becoming an anonymous flex item — which is where a long sentence
   stops wrapping against the icon and starts wrapping against the box. So the
   glyph is a positioned `::before` and the text is text.

   The glyph is a MASK, not a background image, so it takes its colour from the
   cascade — one drawing that is accent on paper and accent on ink. */
.dsc-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.dsc-list li {
  display: block;
  position: relative;
  padding-inline-start: 1.75rem;
  padding-block: var(--sp-3);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
}

.dsc-list li::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  top: calc(var(--sp-3) + 0.28em);
  width: 1.0625rem;
  height: 1.0625rem;
  background-color: var(--dsc-accent-ink);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='m8.5 12.2 2.5 2.5 4.5-5'/%3E%3C/svg%3E")
    center / contain no-repeat;
}

/* The exclusions get a different glyph AND a different colour, so the two
   columns are distinguishable without reading either heading — and without
   colour being the only signal, because the glyph differs too. */
.dsc-list.is-out li::before {
  background-color: var(--dsc-grey);
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M8 12h8'/%3E%3C/svg%3E");
}

/* The numbered note counts instead of ticking. */
.dsc-list.is-numbered {
  counter-reset: dsc-step;
}

.dsc-list.is-numbered li {
  counter-increment: dsc-step;
  padding-inline-start: 2.5rem;
}

.dsc-list.is-numbered li::before {
  content: counter(dsc-step, decimal-leading-zero);
  width: auto;
  height: auto;
  top: var(--sp-3);
  background: none;
  mask: none;
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  color: var(--dsc-accent-ink);
}

.dsc-list.is-numbered b {
  color: var(--page-ink);
}

/* ── the scope boundary ─────────────────────────────────────────────────── */

/* TWO EQUAL TRACKS. The lists are different lengths on every page and the
   columns are not allowed to be: the whole argument of this section is that
   both halves carry the same weight. */
.dsc-scope {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  margin-top: clamp(2rem, 5vw, 3rem);
  background-color: var(--dsc-line);
  border: 1px solid var(--dsc-line);
  border-radius: 1.25rem;
  overflow: clip;
}

.dsc-scope-col {
  padding: var(--sp-7);
  background-color: var(--page-bg);
}

.dsc-scope-h {
  margin-bottom: var(--sp-5);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--dsc-accent-ink);
}

.dsc-scope-col.is-out .dsc-scope-h {
  color: var(--dsc-grey);
}

/* ── the questions ──────────────────────────────────────────────────────── */

.dsc-q-block {
  margin-top: clamp(2rem, 5vw, 3rem);
  padding-top: var(--sp-7);
  border-top: 1px solid var(--dsc-line);
}

.dsc-q {
  max-width: 40ch;
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: var(--lh-snug);
  letter-spacing: var(--track-tight);
}

.dsc-a {
  margin-top: var(--sp-5);
  max-width: 68ch;
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--page-ink);
}

/* ── the identity sheets ────────────────────────────────────────────────── */

.dsc-sheet-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-6);
  margin-top: clamp(2rem, 5vw, 3rem);
}

.dsc-sheet {
  margin: 0;
}

.dsc-sheet img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--dsc-line);
  border-radius: var(--radius--small);
  background-color: var(--dsc-surface);
}

.dsc-sheet figcaption {
  margin-top: var(--sp-4);
  font-size: var(--fs-cap);
  color: var(--dsc-grey);
}

/* ── the work ───────────────────────────────────────────────────────────── */

.dsc-work {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-5);
  margin-top: clamp(2rem, 5vw, 3rem);
}

.dsc-piece {
  padding: var(--sp-6);
  border: 1px solid var(--dsc-line);
  border-radius: 1.25rem;
  background-color: var(--dsc-surface);
}

.dsc-piece-k {
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--dsc-grey);
}

.dsc-piece-t {
  margin-top: var(--sp-4);
  font-size: var(--fs-h4);
  font-weight: 600;
  line-height: var(--lh-snug);
}

.dsc-piece-t a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-color: var(--dsc-rim);
}

.dsc-piece-t a:hover {
  text-decoration-color: currentcolor;
}

.dsc-piece-b {
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--dsc-grey);
}

/* ── the close ──────────────────────────────────────────────────────────── */

.dsc-cta {
  padding-block: clamp(4rem, 11vh, 8rem);
  background: linear-gradient(
    180deg,
    var(--page-bg) 0%,
    color-mix(in srgb, var(--color--lime) 12%, var(--page-bg)) 100%
  );
}

.dsc-cta-h {
  margin-top: var(--sp-4);
  font-family: var(--font-display);
  font-size: var(--fs-h1);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  max-width: 20ch;
}

.dsc-cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  margin-top: var(--sp-8);
}

.dsc-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 3rem;
  box-sizing: border-box;
  padding-inline: 1.5rem;
  border: 1px solid var(--dsc-line);
  border-radius: 999px;
  background-color: var(--dsc-surface);
  font-size: var(--fs-body);
  font-weight: 600;
  line-height: var(--lh-flat);
  color: var(--page-ink);
  text-decoration: none;
  transition:
    background-color 0.3s var(--dsc-ease),
    color 0.3s var(--dsc-ease);
}

.dsc-pill.is-dark {
  background-color: var(--color--dark-green);
  border-color: var(--color--dark-green);
  color: var(--color--white);
}

.dsc-pill:hover {
  background-color: var(--color--lime);
  border-color: var(--color--lime);
  color: var(--color--dark-green);
}

/* ── the switcher ───────────────────────────────────────────────────────── */

.dsc-switch-s {
  padding-block: clamp(3.5rem, 8vh, 5.5rem) clamp(4rem, 9vh, 6rem);
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--color--lime) 12%, var(--page-bg)) 0%,
    var(--page-bg) 72%
  );
}

.dsc-switch-h {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-4);
  margin-bottom: var(--sp-7);
}

.dsc-switch-h h2 {
  font-size: var(--fs-h2);
  font-weight: 600;
  letter-spacing: var(--track-tight);
}

.dsc-tlink {
  min-height: 2.75rem;
  display: inline-flex;
  align-items: center;
  box-sizing: border-box;
  font-size: var(--fs-cap);
  color: var(--dsc-grey);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ── THE THREE DOORWAYS ──────────────────────────────────────────────────────
   Three plates of where you could go next, on the page's own paper (P8-16).

   WHAT THIS REPLACES, AND WHY IT HAD TO GO. The section shipped as three
   full-width list rows: a numeral swatch, a name, a note, an arrow. It was a
   faithful transcription of the Golden Master and it had no design equity to
   protect — but it was also, rule for rule, the same object as `.dsc-piece`
   (same `--dsc-surface`, same `1px var(--dsc-line)`, same radius), which the
   reader has already met twice higher up the page. The last thing the document
   did was repeat itself more quietly. And it showed nothing: four photographs
   exist for these four disciplines, the hub uses them and this page's own hero
   uses them, and the one section whose whole job is "here are the other three"
   presented them as a table of contents.

   NO DARK BAND, AND THAT IS STRUCTURAL RATHER THAN TASTE. This family carries no
   `data-nav-theme-target` anywhere, so `NavThemeFloor` never mounts and the
   header wears the static `dark` ink for the whole document. A dark full-bleed
   section here would put white-on-black under a dark-ink header. The lime wash
   that `.dsc-cta` opens and this section closes stays exactly as it was; the
   cards sit on it.

   ONE GROUND, ONE INK, DECLARED TOGETHER. The four ground/ink pairs used to be
   written out twice in this file — once for the page bands at the top and once
   for the numeral swatches down here — with nothing keeping them in step. They
   are read from the same two custom properties now, so a fifth discipline is one
   block rather than two that have to agree. */

.dsc-switch {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-5);
}

/* THE DESTINATION'S OWN COLOURS, ONCE. `data-switch-to` is the destination's id
   — the same value that page's own wrapper carries — so the card can be painted
   in the ground it leads to before a word of it is read. Ground and ink are
   always declared in the same block, which is the rule `layout/container.css`
   states for anything that paints a surface. */
[data-switch-to='websites'] {
  --sw-g: #e9eef5;
  --sw-i: #0a0f18;
}

[data-switch-to='mobile-apps'] {
  --sw-g: #0b1030;
  --sw-i: #eaf0ff;
}

[data-switch-to='e-commerce'] {
  --sw-g: #07100c;
  --sw-i: #e9f7f0;
}

[data-switch-to='graphic-design'] {
  --sw-g: #16070f;
  --sw-i: #fbeef4;
}

/* The card is a paper frame with 0.25rem of itself showing all round the plate,
   which is what makes the photograph read as mounted rather than pasted. Same
   construction as `.cs-more-card` on a case study, deliberately — a reader who
   has been through the projects has met this object before. */
.dsc-door {
  position: relative;
  display: flex;
  flex-flow: column;
  gap: var(--sp-4);
  padding: 0.25rem 0.25rem var(--sp-5);
  border: 1px solid var(--dsc-line);
  border-radius: 1.25rem;
  background-color: var(--dsc-surface);
  color: var(--page-ink);
  text-decoration: none;
  transition:
    border-color 0.35s var(--dsc-ease),
    transform 0.35s var(--dsc-ease);
}

.dsc-door:hover,
.dsc-door:focus-visible {
  border-color: var(--dsc-rim);
  transform: translateY(-0.25rem);
}

/* `--sw-g` under the photograph, not behind it: the four plates are opaque, but
   the frame is what the image is decoding INTO, and a pale card flashing white
   before a near-black plate arrives is the one frame a reader notices. */
.dsc-door-frame {
  position: relative;
  display: block;
  aspect-ratio: 4 / 3;
  border-radius: 1rem;
  background-color: var(--sw-g);
  overflow: clip;
}

.dsc-door-img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s var(--dsc-ease);
}

.dsc-door:hover .dsc-door-img,
.dsc-door:focus-visible .dsc-door-img {
  transform: scale(1.03);
}

/* The numeral sits ON the plate, in the destination's ink over a wash of its own
   ground. Over a photograph that is not a contrast promise, so the wash is 88%
   opaque — enough to be a surface rather than a tint, which is what makes the
   pair measurable at all. */
.dsc-door-n {
  position: absolute;
  inset-block-start: 0.625rem;
  inset-inline-start: 0.625rem;
  display: grid;
  place-items: center;
  min-width: 2.25rem;
  height: 2.25rem;
  padding-inline: 0.5rem;
  border-radius: 0.625rem;
  background-color: color-mix(in srgb, var(--sw-g) 88%, transparent);
  color: var(--sw-i);
  font-size: var(--fs-cap);
  letter-spacing: var(--track-label);
  backdrop-filter: blur(6px);
}

.dsc-door-body {
  display: flex;
  flex-flow: column;
  gap: var(--sp-2);
  padding-inline: calc(var(--sp-5) - 0.25rem);
}

/* The arrow rides the name's line, so the eye leaves the card where the sentence
   ends rather than at a corner. `::after` on the name and not on the card keeps
   it out of the flex column's gap arithmetic. */
.dsc-door-name {
  display: flex;
  align-items: center;
  gap: 0.5ch;
  font-size: var(--fs-h4);
  font-weight: 600;
  line-height: var(--lh-snug);
}

.dsc-door-name::after {
  content: '';
  flex: none;
  width: 1.125rem;
  height: 1.125rem;
  background-color: currentcolor;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E")
    center / contain no-repeat;
  transition: transform 0.3s var(--dsc-ease);
}

/* THE ARROW POINTS THE WAY THE PAGE READS. It is a right-pointing glyph, and on
   `/ar/` the way forward is left — so it is mirrored there and its hover travel
   is mirrored with it. Nothing else on this page flips, because nothing else on
   it is directional; this is the one glyph that means "onward" rather than
   "right". */
.dsc-door:hover .dsc-door-name::after,
.dsc-door:focus-visible .dsc-door-name::after {
  transform: translateX(0.25rem);
}

html[dir='rtl'] .dsc-door-name::after {
  transform: scaleX(-1);
}

html[dir='rtl'] .dsc-door:hover .dsc-door-name::after,
html[dir='rtl'] .dsc-door:focus-visible .dsc-door-name::after {
  transform: scaleX(-1) translateX(0.25rem);
}

.dsc-door-note {
  font-size: var(--fs-cap);
  line-height: var(--lh-label);
  color: var(--dsc-grey);
}

/* ── the entrance ───────────────────────────────────────────────────────────
   The start state is behind `[data-dsc-live]`, which the runtime sets on the
   page wrapper only once an observer is holding every marked element. A page
   whose text is invisible when a script fails has traded its content for an
   effect. */
[data-dsc-live] .dsc-reveal {
  opacity: 0;
  transform: translateY(1.25rem);
  transition:
    opacity 0.8s var(--dsc-ease),
    transform 0.8s var(--dsc-ease);
  transition-delay: calc(var(--i, 0) * 80ms);
}

[data-dsc-live] .dsc-reveal.is-in {
  opacity: 1;
  transform: none;
}

/* ── 991 and below ──────────────────────────────────────────────────────── */

@media screen and (max-width: 991px) {
  .dsc-hero-in,
  .dsc-steps-stage,
  .dsc-fork-cols,
  .dsc-scope {
    grid-template-columns: minmax(0, 1fr);
  }

  .dsc-hero-art {
    justify-self: start;
    order: -1;
    max-width: 20rem;
  }

  .dsc-spec {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .dsc-cards,
  .dsc-work,
  .dsc-sheet-grid,
  .dsc-switch {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  /* The plate stops pinning the moment it is above the list rather than beside
     it: a sticky box at the top of a single column would sit over the text it
     is describing for the whole section. */
  .dsc-plate {
    position: static;
    aspect-ratio: auto;
  }

  .dsc-fork-line {
    display: none;
  }
}

/* ── 767 and below ──────────────────────────────────────────────────────── */

@media screen and (max-width: 767px) {
  .dsc-spec,
  .dsc-cards,
  .dsc-work,
  .dsc-sheet-grid,
  .dsc-switch {
    grid-template-columns: minmax(0, 1fr);
  }

  .dsc-switch-h {
    flex-direction: column;
    align-items: flex-start;
  }

  /* One across, and the plate goes wide-screen rather than staying 4:3 — three
     stacked 4:3 photographs is most of a phone's page, and none of them is the
     content. The card is a doorway; it needs to be recognisable, not big. */
  .dsc-door-frame {
    aspect-ratio: 16 / 9;
  }
}

/* ── reduced motion ─────────────────────────────────────────────────────────
   `base/reset.css` kills every transition and animation site-wide in the first
   cascade layer, so nothing here has to repeat that. What it cannot undo is
   LAYOUT and STATE: a sticky plate still pins, and a scrubbed custom property
   still holds whatever was last written.

   The runtime never starts a scrub under the preference, so `--dsc-p` is absent
   and every fallback is the settled one. This block covers the other half — the
   preference being turned on mid-session with values already written — and
   unpins the plate, which is a position rather than an animation. */
@media (prefers-reduced-motion: reduce) {
  .dsc-plate {
    position: static;
  }

  .dsc-fork-path {
    stroke-dasharray: none;
    stroke-dashoffset: 0;
  }

  [data-dsc-live] .dsc-reveal {
    opacity: 1;
    transform: none;
  }
}
}

/* ---- sections · sections/contact.css ---- */
@layer sections {
/**
 * contact.css — /contact.
 *
 * Registered in src/styles/order.json before it had content, so that the
 * cascade order is a reviewed decision rather than a side effect of which file
 * was written first.
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * FORM USABILITY BEATS DECORATION, AND THIS SHEET IS WHERE THAT IS SPENT
 *
 * Three rules here exist because of a measured mobile failure rather than
 * because of taste, and all three are in PIXELS on purpose:
 *
 *   font-size: max(16px, …)  Mobile Safari zooms into any input whose computed
 *                            font-size is under 16px and does not zoom back out.
 *                            `--fs-body` is 14.2px on a phone, so the max() is
 *                            what keeps the page from jumping on first focus.
 *                            Raising it by removing pinch-zoom instead
 *                            (`maximum-scale=1`) is a WCAG 1.4.4 failure and is
 *                            not available.
 *   padding: 15px 16px       `1rem` is 10.83px at 390 and 5.56px at exactly 480
 *                            under the root ladder (`tokens/root-font.css`), so
 *                            a rem-based input pad puts the control under the
 *                            44px target height at the two widths phones are.
 *   min-height on the pills  Same reason: a target, not a rhythm.
 *
 * The error line is RESERVED rather than inserted: it holds its height from
 * first paint, so a validation message cannot reflow the form under a pointer
 * that is already moving towards the next field.
 *
 * NO RED. An invalid field is marked by a heavier border, a visible message and
 * `aria-invalid` — three signals, none of which is colour alone (WCAG 1.4.1).
 */

/* ── page tokens ────────────────────────────────────────────────────────── */

[data-page='contact'] {
  --ctx-surface: color-mix(in srgb, var(--color--dark-green) 4%, var(--color--white));
  --ctx-line: color-mix(in srgb, var(--color--dark-green) 10%, transparent);
  --ctx-rim: color-mix(in srgb, var(--color--dark-green) 22%, transparent);
  --ctx-grey: var(--color--dark-green-tint-2);
  --ctx-ease: cubic-bezier(0.215, 0.61, 0.355, 1);
  /* MEASURED, as on the discipline pages: the standard ink cut is 4.42:1 on
     this page's card surface at `--fs-micro`, which is under the small-text
     floor. Fifteen per cent towards the page ink takes it to 5.62:1. */
  --ctx-accent-ink: color-mix(in srgb, var(--color--lime-ink) 85%, var(--color--dark-green));

  background-color: var(--page-bg);
  color: var(--page-ink);
}

[data-page='contact'] .ctx-wrap {
  max-width: 82rem;
}

[data-page='contact'] .ctx-narrow {
  max-width: 42rem;
}

[data-page='contact'] .ctx-s {
  padding-block: var(--section-y);
}

/* ── type roles ─────────────────────────────────────────────────────────── */

.ctx-h1 {
  margin-top: var(--sp-6);
  font-family: var(--font-display);
  font-size: var(--fs-display);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
}

.ctx-h2 {
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
  max-width: 24ch;
}

.ctx-eyebrow {
  color: var(--ctx-grey);
}

.ctx-lede {
  margin-top: var(--sp-5);
  max-width: 40ch;
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--ctx-grey);
}

.ctx-body {
  margin-top: var(--sp-5);
  max-width: 62ch;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ctx-grey);
}

.ctx-note {
  margin-top: var(--sp-5);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--ctx-grey);
}

/* UNDERLINED. `--page-ink` against `--ctx-grey` is 2.68:1, so colour alone
   cannot mark a link inside a paragraph (WCAG 1.4.1). */
.ctx-note a,
.ctx-body a,
.ctx-status a {
  color: var(--page-ink);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ── breadcrumb ─────────────────────────────────────────────────────────── */

.ctx-crumb-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-cap);
  color: var(--ctx-grey);
}

.ctx-crumb-list li + li::before {
  content: '/';
  margin-inline-end: var(--sp-3);
  color: var(--ctx-rim);
}

.ctx-crumb-list a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.ctx-crumb-list [aria-current='page'] {
  color: var(--page-ink);
}

/* ── the hero ───────────────────────────────────────────────────────────── */

/* THE MASTHEAD STARTS BELOW THE HEADER, AND THE SELECTOR IS THE FIX (P11-02).

   This rule used to read `.ctx-hero { padding-top: … }` — ONE class, (0,1,0) —
   while `[data-page='contact'] .ctx-s` twelve rules above is (0,2,0) and sets
   `padding-block: var(--section-y)` on the same element. Specificity decided,
   the masthead's own offset never applied at all, and what a reader actually
   got was `--section-y`: 53.1px at 390 against a header 79.2px tall. Measured
   in WebKit and Chromium at 320, 390, 430 and 768 — the breadcrumb and the
   first line of the h1 sat BEHIND the bar at every one of them, in all three
   locales, and cleared it on desktop only because `--section-y` reaches 92px
   there. It was never a spacing preference; it was a rule that lost.

   It is now the same two classes plus the page attribute, so it wins on the
   quantity that matters and cannot be beaten back by a later `--section-y`
   change. And it measures against `--header-clear` — the bar's REAL height plus
   the notch inset — rather than `--on-t-nav-spacer`, which is a Golden Master
   spacing value smaller than the bar at every width. `--sp-5` is the air
   between the two; the bottom padding stays `--section-y`, unchanged. */
[data-page='contact'] .ctx-s.ctx-hero {
  padding-top: calc(var(--header-clear) + var(--sp-5));
}

/* A blinking bar after the heading. It is `step-end`, so it is a cursor rather
   than a pulse, and it settles VISIBLE — under `prefers-reduced-motion` the
   reset layer stops the animation and what is left is a static mark rather
   than a heading that lost a character. */
.ctx-caret {
  display: inline-block;
  width: 0.09em;
  height: 0.82em;
  margin-inline-start: 0.12em;
  vertical-align: -0.06em;
  border-radius: 0.045em;
  background-color: var(--color--lime-ink);
  animation: ctx-blink 1.1s step-end infinite;
}

@keyframes ctx-blink {
  50% {
    opacity: 0;
  }
}

.ctx-hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  margin-top: var(--sp-8);
}

.ctx-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 48px, in px: a target size is a physical measurement and must not track a
     root font that halves across the 480 boundary. */
  min-height: 48px;
  box-sizing: border-box;
  padding-inline: 1.5rem;
  border: 1px solid var(--ctx-line);
  border-radius: 999px;
  background-color: var(--ctx-surface);
  font-family: inherit;
  font-size: var(--fs-body);
  font-weight: 600;
  line-height: var(--lh-flat);
  color: var(--page-ink);
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color 0.3s var(--ctx-ease),
    color 0.3s var(--ctx-ease);
}

.ctx-pill.is-dark {
  background-color: var(--color--dark-green);
  border-color: var(--color--dark-green);
  color: var(--color--white);
}

.ctx-pill:hover {
  background-color: var(--color--lime);
  border-color: var(--color--lime);
  color: var(--color--dark-green);
}

/* ── what a message is about ────────────────────────────────────────────── */

.ctx-reasons {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-5);
  margin-top: clamp(2rem, 5vw, 3rem);
  padding: 0;
  list-style: none;
}

.ctx-reason {
  padding: var(--sp-6);
  border: 1px solid var(--ctx-line);
  border-radius: 1.25rem;
  background-color: var(--ctx-surface);
}

.ctx-reason-t {
  font-size: var(--fs-h4);
  font-weight: 600;
  line-height: var(--lh-snug);
  letter-spacing: var(--track-tight);
}

.ctx-reason-b {
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--ctx-grey);
}

.ctx-about {
  margin-top: clamp(2rem, 5vw, 3rem);
  padding-top: var(--sp-7);
  border-top: 1px solid var(--ctx-line);
}

.ctx-about-t {
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ctx-grey);
}

.ctx-about-b {
  margin-top: var(--sp-4);
  max-width: 62ch;
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
}

/* ── the five channels ──────────────────────────────────────────────────── */

.ctx-channel-list {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-5);
  margin-top: clamp(2rem, 5vw, 3rem);
  padding: 0;
  list-style: none;
}

.ctx-channel {
  display: flex;
  flex-direction: column;
  padding: var(--sp-6);
  border: 1px solid var(--ctx-line);
  border-radius: 1.25rem;
  background-color: var(--ctx-surface);
}

.ctx-channel-t {
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ctx-grey);
}

.ctx-channel-v {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-4);
  font-size: var(--fs-h4);
  font-weight: 600;
  /* THE ADDRESS AND THE NUMBER MAY NOT BE SHORTENED. `anywhere` rather than
     `break-word` so a long handle wraps inside a 1fr track instead of pushing
     the grid wider — the one thing on this page that must never be clipped is
     the value someone came here to read. */
  overflow-wrap: anywhere;
}

.ctx-channel-v a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-color: var(--ctx-rim);
}

.ctx-channel-v a:hover {
  text-decoration-color: currentcolor;
}

/* The copy button. It ships `hidden` (see the section's comment) and the
   runtime unhides it, so this rule describes a control that only exists when
   something can make it work. */
.ctx-copy {
  flex: none;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 1px solid var(--ctx-line);
  border-radius: 50%;
  background-color: var(--page-bg);
  color: var(--ctx-grey);
  cursor: pointer;
}

.ctx-copy::before {
  content: '';
  display: block;
  width: 1.1rem;
  height: 1.1rem;
  margin: 0 auto;
  background-color: currentcolor;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='9' y='9' width='11' height='11' rx='2'/%3E%3Cpath d='M5 15V5a2 2 0 0 1 2-2h10'/%3E%3C/svg%3E")
    center / contain no-repeat;
}

/* The confirmed state is a DIFFERENT GLYPH as well as a different colour, so it
   is not signalled by colour alone. The runtime sets the attribute; the
   accompanying words go to the live region beside the list. */
.ctx-copy[data-ctx-copied]::before {
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12.5 4.5 4.5L19 7'/%3E%3C/svg%3E");
  color: var(--ctx-accent-ink);
}

.ctx-channel-b {
  margin-top: var(--sp-4);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--ctx-grey);
}

.ctx-channel-go {
  display: inline-flex;
  align-items: center;
  gap: 0.6ch;
  align-self: flex-start;
  min-height: 44px;
  box-sizing: border-box;
  /* The link anchors to the bottom edge, so five cards of different text
     lengths still line their actions up. */
  margin-top: auto;
  padding-top: var(--sp-5);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--page-ink);
  text-decoration: underline;
  text-underline-offset: 0.25em;
  text-decoration-color: var(--ctx-rim);
}

.ctx-channel-go::after {
  content: '';
  width: 1.05em;
  height: 1.05em;
  background-color: currentcolor;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E")
    center / contain no-repeat;
  transition: transform 0.3s var(--ctx-ease);
}

.ctx-channel-go:hover::after {
  transform: translateX(0.25rem);
}

/* ── the form ───────────────────────────────────────────────────────────── */

.ctx-form {
  display: grid;
  gap: var(--sp-6);
  margin-top: clamp(2rem, 5vw, 3rem);
}

.ctx-field {
  display: grid;
}

.ctx-label {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6ch;
  margin-bottom: var(--sp-3);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.ctx-req {
  font-size: var(--fs-micro);
  font-weight: 400;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ctx-grey);
}

.ctx-in {
  width: 100%;
  box-sizing: border-box;
  appearance: none;
  /* SEE THE FILE HEADER. 16px is the floor Mobile Safari needs; the pad is in
     px because the root font halves across the 480 boundary and a rem pad puts
     the control under the target height at exactly the widths phones report. */
  font-family: inherit;
  font-size: max(16px, var(--fs-body));
  line-height: var(--lh-body);
  color: var(--page-ink);
  padding: 15px 16px;
  border: 1px solid var(--ctx-rim);
  border-radius: 12px;
  background-color: var(--page-bg);
  transition:
    border-color 0.25s var(--ctx-ease),
    background-color 0.25s var(--ctx-ease);
}

.ctx-in:hover {
  border-color: var(--ctx-grey);
}

/* A BORDER CHANGE IS NOT A FOCUS INDICATOR. The outline is, and it is drawn
   outside the box so it survives on any background this form is ever placed on. */
.ctx-in:focus-visible {
  outline: 2px solid var(--color--lime-ink);
  outline-offset: 2px;
  border-color: var(--color--lime-ink);
}

.ctx-in.is-area {
  resize: vertical;
  min-height: 148px;
}

/* Marked invalid by the runtime, or by the browser's own validation. The border
   is one of three signals and never the only one — the message below and
   `aria-invalid` are the other two. */
.ctx-in[aria-invalid='true'] {
  border-color: var(--page-ink);
  border-width: 2px;
}

.ctx-hint,
.ctx-err {
  margin-top: var(--sp-3);
  font-size: var(--fs-cap);
  line-height: var(--lh-label);
  color: var(--ctx-grey);
}

/* RESERVED, NOT INSERTED. The line holds its height from first paint so a
   message arriving cannot move the next field out from under a pointer. */
.ctx-err {
  min-height: 1.25rem;
  color: var(--page-ink);
}

/* The honeypot. Off-screen rather than `display: none`, because a bot that
   skips undisplayed fields would skip this one too — which is exactly the
   behaviour it is trying to catch. `inset-inline-start` rather than `left` so
   it leaves the page on the correct side under `dir="rtl"` and cannot become a
   horizontal scrollbar on the Arabic document. */
.ctx-hp {
  position: absolute;
  inset-inline-start: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.ctx-form-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-5);
}

.ctx-submit[disabled] {
  opacity: 0.55;
  cursor: wait;
}

/* The live region. It reserves its height too, for the same reason the error
   lines do — "Sending…" must not push the button it sits beside. */
.ctx-status {
  min-height: 1.5rem;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--ctx-grey);
}

.ctx-done {
  padding: var(--sp-8);
  border: 1px solid var(--ctx-line);
  border-radius: 1.5rem;
  background-color: var(--ctx-surface);
  text-align: center;
}

/* The runtime removes `hidden` when a send is acknowledged. Stated here so the
   element's resting state does not depend on a UA stylesheet the cascade could
   out-rank by accident. */
.ctx-done[hidden] {
  display: none;
}

.ctx-done-t {
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: var(--lh-snug);
}

.ctx-done-b {
  margin-top: var(--sp-4);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ctx-grey);
}

/* ── the entrance ─────────────────────────────────────────────────────────
   Behind `[data-ctx-live]`, which the runtime sets once an observer holds every
   marked element. A contact page whose address is invisible when a script fails
   is a contact page that cannot be contacted. */
[data-ctx-live] .ctx-reveal {
  opacity: 0;
  transform: translateY(1.25rem);
  transition:
    opacity 0.8s var(--ctx-ease),
    transform 0.8s var(--ctx-ease);
  transition-delay: calc(var(--i, 0) * 80ms);
}

[data-ctx-live] .ctx-reveal.is-in {
  opacity: 1;
  transform: none;
}

/* ── 991 and below ──────────────────────────────────────────────────────── */

@media screen and (max-width: 991px) {
  .ctx-reasons,
  .ctx-channel-list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ── 767 and below ──────────────────────────────────────────────────────── */

@media screen and (max-width: 767px) {
  .ctx-reasons,
  .ctx-channel-list {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ── 479 and below ──────────────────────────────────────────────────────── */

@media screen and (max-width: 479px) {
  /* A full-width action rather than two half-width ones: at 390 the two pills
     side by side leave neither with room for its own words. */
  .ctx-hero-actions .ctx-pill {
    width: 100%;
  }
}

/* ── reduced motion ─────────────────────────────────────────────────────────
   `base/reset.css` already stops every animation and transition site-wide from
   the first cascade layer, including the caret's blink, and it settles visible.
   What is restated here is the reveal's START state, because that is a
   position rather than an animation: without this the marked elements would
   hold at opacity 0 if the preference were switched on mid-session. */
@media (prefers-reduced-motion: reduce) {
  [data-ctx-live] .ctx-reveal {
    opacity: 1;
    transform: none;
  }
}
}

/* ---- sections · sections/blog-index.css ---- */
@layer sections {
/* ============================================================================
   /blog — THE WRITING, AS AN INDEX.

   THE GRID IS UNIFORM, AND THAT IS THE WHOLE REPAIR. The previous index was
   `repeat(12, 1fr)` with `grid-auto-flow: dense`, and every card carried a
   `wide` / `std` / `tall` shape stamped on it by a repeating ring in the build.
   Two things followed and both were visible:

     · Dense packing back-fills holes with any later item that fits. Press a
       topic chip, nine cards go `hidden`, and the survivors are re-packed into a
       composition nobody designed — a different one for each of the five chips.
       The page rearranged itself under the reader's hands.
     · A 4-column card and an 8-column card next to a 4-column card two rows tall
       do not share a baseline, so titles landed at three different heights and
       the footers at four. That reads as debris, and it was read as debris.

   Now: one column width, one card height, `align-items: stretch`, no dense. A
   hidden card leaves a hole where it was and moves nothing. The variety the
   shape ring was reaching for is carried by the FEATURED band above, which is
   one card at a size no other card has — which is what "featured" means.

   THE CARD IS A THREE-ROW SUBGRID. `grid-template-rows: auto 1fr auto` on the
   card and `subgrid` on the row means the plate, the body and the foot of every
   card in a row share the same three tracks: every title starts on the same
   line and every foot rule sits on the same line, whatever the copy does. That
   is the mechanism the old `md:h-[3.2rem]` fixed title box was approximating,
   without clamping anybody's title.

   THE PAGE IS LIGHT. There is no dark card, no glass, no gradient and no glow.
   The site has exactly one dark surface on the whole blog and it is the code
   plate inside an article.
   ========================================================================== */

[data-page='blog'] {
  --wx-gutter: clamp(1rem, 1.8vw, 1.75rem);
  --wx-rail-w: clamp(15rem, 21vw, 19rem);
  --wx-measure: 64ch;
}

[data-page='blog'] .wx-s {
  padding-block: var(--section-y);
}

[data-page='blog'] .wx-mast {
  padding-block-start: calc(var(--header-clear) + var(--sp-8));
  padding-block-end: var(--sp-8);
}

[data-page='blog'] .wx-wrap {
  max-width: 82rem;
}

/* ── the breadcrumb ─────────────────────────────────────────────────────── */

.wx-crumb-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-7);
  padding: 0;
  list-style: none;
  font-family: var(--wx-mono);
  font-size: var(--fs-cap);
  color: var(--wx-grey);
}

.wx-crumb-list li + li::before {
  content: '/';
  margin-inline-end: var(--sp-3);
  color: var(--wx-soft);
}

.wx-crumb-list a {
  color: var(--wx-grey);
  text-decoration: none;
  border-block-end: 1px solid transparent;
}

.wx-crumb-list a:hover,
.wx-crumb-list a:focus-visible {
  color: var(--wx-accent);
  border-block-end-color: currentColor;
}

.wx-crumb-list [aria-current='page'] {
  color: var(--wx-ink);
}

/* ── the masthead ───────────────────────────────────────────────────────── */

.wx-eyebrow {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-5);
  color: var(--wx-accent);
}

.wx-h1 {
  max-width: 18ch;
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-display);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  color: var(--wx-ink);
  text-wrap: balance;
}

.wx-lede {
  max-width: var(--wx-measure);
  margin: var(--sp-6) 0 0;
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--wx-dim);
}

/* The index names the person behind the writing without turning the masthead
   into another biography card. The supplied square portrait stays crisp in the
   small circular crop, and the whole signature is one useful route to contact. */
.wx-author {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  margin-block-start: var(--sp-6);
  padding: 0.25rem var(--sp-4) 0.25rem 0.25rem;
  border: 1px solid var(--wx-rim);
  border-radius: 999px;
  background: var(--wx-plate);
  color: var(--wx-ink);
  text-decoration: none;
  transition:
    border-color var(--duration-fast) var(--wx-ease),
    transform var(--duration-base) var(--wx-ease);
}

.wx-author:hover,
.wx-author:focus-visible {
  border-color: var(--wx-accent);
  transform: translateY(-2px);
}

.wx-author-face {
  display: block;
  width: 2.8rem;
  height: 2.8rem;
  border-radius: 50%;
  object-fit: cover;
}

.wx-author-name {
  font-family: var(--font-label);
  font-size: var(--fs-cap);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

/* THE THREE FACTS. A rule above, then three cells that read as one strip. On a
   phone they stack, and the count keeps its display size because it is the one
   value a reader takes away from a masthead. */
.wx-facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-6);
  margin: var(--sp-9) 0 0;
  padding-block-start: var(--sp-6);
  border-block-start: 1px solid var(--wx-rule);
}

.wx-fact dt {
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--wx-grey);
}

.wx-fact dd {
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-body);
  line-height: var(--lh-snug);
  color: var(--wx-ink);
}

.wx-fact-n {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--track-tight);
}

/* ── the featured card ──────────────────────────────────────────────────── */

[data-page='blog'] .wx-lead-s {
  padding-block-start: 0;
}

.wx-lead {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  align-items: stretch;
  overflow: hidden;
  border: 1px solid var(--wx-rim);
  border-radius: var(--wx-radius-lg);
  background: var(--wx-plate);
  color: inherit;
  text-decoration: none;
  transition:
    border-color var(--duration-base) var(--wx-ease),
    transform var(--duration-base) var(--wx-ease);
}

/* NO SHADOW, HERE OR ANYWHERE ON THIS BLOG. Both card families used to lift on
   a blurred drop shadow; the rim and the lift now do the whole job. A shadow is
   also the one hover property that costs a repaint of everything under the card
   rather than a composited transform. */
.wx-lead:hover,
.wx-lead:focus-visible {
  border-color: var(--wx-accent);
  transform: translateY(-3px);
}

.wx-lead-plate {
  display: block;
  overflow: hidden;
  /* `width: 100%` IS LOAD-BEARING. This cell is `align-items: stretch`, so its
     HEIGHT is the row's — definite — and with an `auto` width the ratio is
     transferred back to the inline axis (CSS Sizing 4 §5.1) and makes the box
     wider than the column it is in. Measured at 1440: 711px of picture inside a
     585px track, overflowing under the copy beside it. A definite width leaves
     the ratio to do its one job, which is giving the STACKED layout below 1200 a
     height. */
  width: 100%;
  aspect-ratio: 16 / 9;
  /* THE PICTURE'S OWN GROUND, and it is a ground rather than a mount now. This
     box stretches to the taller column, and what fills it is a PHOTOGRAPH: it
     may be cropped, because unlike the drawings this replaced it carries nothing
     at its own edges to lose. `contain` was the old rule and its cost was a band
     of empty paper above and below the figure the moment a title ran to three
     lines. `--wx-surface` shows only while the file is still arriving. */
  background: var(--wx-surface);
  border-inline-end: 1px solid var(--wx-line);
}

.wx-lead-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  transition: transform var(--duration-slow) var(--wx-ease);
}

.wx-lead:hover .wx-lead-img,
.wx-lead:focus-visible .wx-lead-img {
  transform: scale(1.025);
}

.wx-lead-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-5);
  padding: clamp(1.4rem, 2.6vw, 2.6rem);
}

.wx-lead-title {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
  color: var(--wx-ink);
  text-wrap: balance;
}

.wx-lead-note {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--wx-dim);
}

.wx-lead-go {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  margin-block-start: auto;
  padding-block-start: var(--sp-5);
  font-family: var(--font-label);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--wx-accent);
}

.wx-go-arrow,
.wx-card-go {
  display: inline-flex;
  width: 1.15em;
  height: 1.15em;
  transition: transform var(--duration-base) var(--wx-ease);
}

.wx-go-arrow svg,
.wx-card-go svg {
  width: 100%;
  height: 100%;
}

.wx-lead:hover .wx-go-arrow,
.wx-lead:focus-visible .wx-go-arrow,
.wx-card:hover .wx-card-go,
.wx-card:focus-visible .wx-card-go {
  transform: translate(calc(3px * var(--dir)), -3px);
}

/* ── the split: rail beside the listing ─────────────────────────────────── */

.wx-split {
  display: grid;
  grid-template-columns: var(--wx-rail-w) minmax(0, 1fr);
  gap: clamp(1.75rem, 3.4vw, 3.5rem);
  align-items: start;
}

.wx-rail-in {
  position: sticky;
  top: calc(var(--header-clear) + var(--sp-5));
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
  padding: var(--sp-6);
  border: 1px solid var(--wx-line);
  border-radius: var(--wx-radius-lg);
  background: var(--wx-plate);
}

.wx-rail-h,
.wx-rail-sub {
  margin: 0 0 var(--sp-4);
  color: var(--wx-grey);
}

.wx-rail-h {
  padding-block-end: var(--sp-4);
  border-block-end: 1px solid var(--wx-line);
}

/* ── the search field ───────────────────────────────────────────────────── */

.wx-search-field {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding-inline: var(--sp-4);
  border: 1px solid var(--wx-rim);
  border-radius: 999px;
  background: var(--page-bg);
  transition: border-color var(--duration-fast) var(--wx-ease);
}

.wx-search-field:focus-within {
  border-color: var(--wx-accent);
}

.wx-search-glyph {
  display: inline-flex;
  flex: none;
  width: 1.05rem;
  height: 1.05rem;
  color: var(--wx-soft);
}

.wx-search-glyph svg {
  width: 100%;
  height: 100%;
}

.wx-search-input {
  flex: 1 1 auto;
  min-width: 0;
  padding-block: 0.72rem;
  border: 0;
  background: transparent;
  color: var(--wx-ink);
  font: inherit;
  font-size: var(--fs-sm);
}

.wx-search-input:focus {
  outline: none;
}

.wx-search-input::placeholder {
  color: var(--wx-soft);
}

/* The UA's own clear affordance is removed because there is a real button
   beside it that the runtime keeps in step with the field's value; two clear
   controls in one field is one too many. */
.wx-search-input::-webkit-search-cancel-button,
.wx-search-input::-webkit-search-decoration {
  appearance: none;
}

/* Ships `hidden` and the runtime removes the attribute once the field has a
   value, so this rule is scoped for the same reason `.wx-empty` is — see the
   note there. */
.wx-search-clear:not([hidden]) {
  display: inline-flex;
}

.wx-search-clear {
  flex: none;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--wx-surface);
  color: var(--wx-grey);
  cursor: pointer;
}

.wx-search-clear svg {
  width: 0.85rem;
  height: 0.85rem;
}

.wx-search-clear:hover,
.wx-search-clear:focus-visible {
  background: var(--wx-wash);
  color: var(--wx-accent);
}

/* ── the topic list ─────────────────────────────────────────────────────── */

.wx-topic-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.wx-topic {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: 0.55rem var(--sp-4);
  border-radius: 10px;
  color: var(--wx-dim);
  font-size: var(--fs-sm);
  text-decoration: none;
  transition:
    background-color var(--duration-fast) var(--wx-ease),
    color var(--duration-fast) var(--wx-ease);
}

.wx-topic:hover,
.wx-topic:focus-visible {
  background: var(--wx-surface);
  color: var(--wx-ink);
}

/* THE SELECTED STATE IS AN ATTRIBUTE, WRITTEN BY THE RUNTIME. Behaviour selects
   on `data-*` and never on a styling class (INV-05), so the same attribute the
   script reads is the one the stylesheet paints. */
.wx-topic[data-on] {
  background: var(--wx-ink);
  color: var(--page-bg);
}

.wx-topic-c {
  font-family: var(--wx-mono);
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
  color: var(--wx-grey);
}

.wx-topic[data-on] .wx-topic-c {
  color: var(--wx-mark);
}

/* ── the topic mix: ten bars, length by reading time ────────────────────── */

/* THIS IS THE OLD MASTHEAD SPINE STRIP AT THE SIZE IT EARNS. Ten bars, one per
   piece, oldest first so the row reads left-to-right as time; each bar's height
   is its reading time. It answers "what is here?" — the count, the span, the
   length distribution — in one glance, which was worth forty pixels and was not
   worth a whole band.

   `--m` IS THE READING TIME, STAMPED BY THE BUILD. The height is a ratio of it
   against the longest piece, not a class, so an eleventh article of a length
   nobody has written yet still draws correctly. */
.wx-mix {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 3.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
  flex-direction: row-reverse;
}

.wx-mix-row {
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
  display: flex;
  align-items: flex-end;
}

.wx-mix-bar {
  display: block;
  width: 100%;
  opacity: 1;
  /* 12 is the longest reading time in the collection; the floor keeps a short
     piece from becoming a bar nobody can point at. */
  height: max(26%, calc(var(--m) / 12 * 100%));
  border-radius: 3px 3px 0 0;
  /* A BAR WHOSE HEIGHT IS DATA IS A MEANINGFUL GRAPHIC, so it owes 3:1 under
     WCAG 1.4.11 — `--wx-rim` is 1.82:1 and was a chart nobody could read. */
  background: var(--wx-mute);
  transition:
    background-color var(--duration-fast) var(--wx-ease),
    opacity var(--duration-base) var(--wx-ease),
    transform var(--duration-fast) var(--wx-ease);
}

/* Written by `selectTopic` on the bars whose topic is not the one chosen. The
   dimmed bar keeps its height — it is still part of the shape of the whole body
   of writing — it simply stops competing with the three the reader asked for. */
.wx-mix-bar[data-wrx-dim] {
  opacity: 0.28;
}

.wx-mix-bar:not([data-wrx-dim]):hover,
.wx-mix-bar:not([data-wrx-dim]):focus-visible {
  background: var(--wx-accent);
  transform: scaleY(1.06);
  transform-origin: bottom;
}

.wx-mix-read {
  min-height: 2.4em;
  margin: 0 0 var(--sp-4);
  font-family: var(--wx-mono);
  font-size: var(--fs-micro);
  line-height: 1.35;
  color: var(--wx-grey);
}

.wx-mix-read-i {
  display: block;
  transition: opacity var(--duration-fast) var(--wx-ease);
}

.wx-mix-read[data-swapping] .wx-mix-read-i {
  opacity: 0;
}

/* ── the count readout ──────────────────────────────────────────────────── */

.wx-count {
  margin: 0;
  padding-block-start: var(--sp-5);
  border-block-start: 1px solid var(--wx-line);
  font-family: var(--wx-mono);
  font-size: var(--fs-cap);
  font-variant-numeric: tabular-nums;
  color: var(--wx-grey);
}

.wx-count b {
  color: var(--wx-ink);
}

.wx-count-sep {
  margin-inline: 0.35em;
  color: var(--wx-soft);
}

/* ── THE GRID ───────────────────────────────────────────────────────────── */

.wx-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(19rem, 100%), 1fr));
  gap: var(--wx-gutter);
  align-items: stretch;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* THE ROW SHARES ITS TRACKS. `subgrid` on the cell and on the card means the
   plate, the body and the foot of every card in a row sit on the same three
   lines — so titles start level and foot rules land level, whatever the copy
   length. Without it the fix is a fixed-height title box that clamps somebody's
   title, which is what the reference designs do. */
.wx-cell {
  display: grid;
  grid-template-rows: auto 1fr auto;
}

.wx-card {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
  overflow: hidden;
  border: 1px solid var(--wx-line);
  border-radius: var(--wx-radius);
  background: var(--wx-plate);
  color: inherit;
  text-decoration: none;
  transition:
    border-color var(--duration-base) var(--wx-ease),
    transform var(--duration-base) var(--wx-ease);
}

@supports not (grid-template-rows: subgrid) {
  .wx-cell {
    display: block;
  }

  .wx-card {
    display: flex;
    flex-direction: column;
    height: 100%;
  }

  .wx-card-body {
    flex: 1 1 auto;
  }
}

.wx-card:hover,
.wx-card:focus-visible {
  border-color: var(--wx-accent);
  transform: translateY(-3px);
}

.wx-card-plate {
  position: relative;
  display: block;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  background: var(--wx-surface);
  border-block-end: 1px solid var(--wx-line);
}

.wx-card-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--duration-slow) var(--wx-ease);
}

.wx-card:hover .wx-card-img,
.wx-card:focus-visible .wx-card-img {
  transform: scale(1.03);
}

/* THE INDEX'S ORDINAL — where the piece sits in the list you are reading —
   `aria-hidden` because the list is already ordered.

   IT WEARS ITS OWN GROUND NOW. It used to be `--wx-soft` ink straight onto the
   plate, which was safe while every plate was a drawing on this site's own
   paper. Five of the ten photographs are dark and five are pale, so a bare
   numeral is unreadable on half of them by construction. A paper chip under it
   is one declaration and is correct on both. */
.wx-card-n {
  position: absolute;
  inset-block-start: var(--sp-4);
  inset-inline-start: var(--sp-4);
  padding: 0.25em 0.55em;
  border-radius: 999px;
  background: var(--page-bg);
  font-family: var(--wx-mono);
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.08em;
  color: var(--wx-soft);
}

.wx-card-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-6);
}

.wx-card-title {
  font-family: var(--font-display);
  font-size: var(--fs-h4);
  font-weight: 600;
  line-height: var(--lh-snug);
  letter-spacing: var(--track-tight);
  color: var(--wx-ink);
  text-wrap: balance;
}

.wx-card-note {
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--wx-dim);
}

.wx-card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  margin-inline: var(--sp-6);
  padding-block: var(--sp-5);
  border-block-start: 1px solid var(--wx-line);
  font-family: var(--wx-mono);
  font-size: var(--fs-cap);
  color: var(--wx-grey);
}

.wx-card-go {
  color: var(--wx-accent);
}

/* ── the empty state ────────────────────────────────────────────────────── */

/* `[hidden]` MUST WIN OVER THIS RULE. The UA sheet's `[hidden] { display: none }`
   is one attribute selector; a class rule beats it on specificity, so a plain
   `.wx-empty { display: flex }` renders the empty state on a page that is
   showing all ten pieces. Every rule here that sets `display` is therefore
   scoped `:not([hidden])`. Measured: the message shipped visible under the grid.

   IT COULD NOT BE FIXED IN THE RUNTIME EITHER, and that is why it is stated
   here: the runtime toggles the `hidden` ATTRIBUTE (INV-05 — state is an
   attribute, never a class), so the attribute is the only signal the stylesheet
   is allowed to read. */
.wx-empty:not([hidden]) {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  margin: var(--sp-8) 0 0;
  padding: var(--sp-10) var(--sp-6);
  border: 1px dashed var(--wx-rim);
  border-radius: var(--wx-radius);
  text-align: center;
  color: var(--wx-grey);
}

.wx-empty b {
  font-family: var(--font-display);
  font-size: var(--fs-h4);
  font-weight: 600;
  color: var(--wx-ink);
}

/* ── the close ──────────────────────────────────────────────────────────── */

.wx-cta {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-8);
  padding: clamp(1.6rem, 3.4vw, 3.2rem);
  border: 1px solid var(--wx-rim);
  border-radius: var(--wx-radius-lg);
  background: var(--wx-plate);
}

.wx-cta-h {
  max-width: 20ch;
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
  color: var(--wx-ink);
}

.wx-cta-h em {
  font-style: normal;
  color: var(--wx-accent);
}

.wx-cta-p {
  max-width: 52ch;
  margin: var(--sp-5) 0 0;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--wx-dim);
}

.wx-cta-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
}

/* ── THE CARD ENTRANCE ──────────────────────────────────────────────────
   ARMED ONLY ONCE THE OBSERVER EXISTS. `[data-wrx-live]` is written on the list
   by `blog-index.js` AFTER its IntersectionObserver is constructed, so a script
   that throws on the line before leaves every card at its settled state rather
   than leaving the page blank. Under `reduce` the installer returns null and the
   attribute is never written at all. */
.wx-grid[data-wrx-live] .wx-cell {
  opacity: 0;
  transform: translateY(14px);
}

.wx-grid[data-wrx-live] .wx-cell.is-in {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--duration-slow) var(--wx-ease) calc(var(--i, 0) * 45ms),
    transform var(--duration-slow) var(--wx-ease) calc(var(--i, 0) * 45ms);
}

/* ── breakpoints ────────────────────────────────────────────────────────── */

@media screen and (max-width: 1199px) {
  .wx-lead {
    grid-template-columns: minmax(0, 1fr);
  }

  .wx-lead-plate {
    border-inline-end: 0;
    border-block-end: 1px solid var(--wx-line);
  }
}

/* THE RAIL BECOMES A CONTROL STRIP AT 991, and the change is more than a grid
   declaration this time.

   WHAT WAS WRONG. Below the rail breakpoint the four controls simply re-flowed
   into a two-column plate ABOVE the list: a search field, five topic rows, a
   ten-bar chart with a hover readout, and a count. On a 390px screen that is a
   full viewport of chrome standing between a reader and the first article, and
   the reader has to scroll past all of it every time they come back.

   WHAT IT IS NOW. One quiet strip, in flow, with no plate around it: a search
   field, then the topics as a single scrolling row of chips, then the count on
   the same line as the label. It costs about a fifth of the screen it used to. */
@media screen and (max-width: 991px) {
  .wx-split {
    grid-template-columns: minmax(0, 1fr);
  }

  .wx-rail-in {
    position: static;
    display: flex;
    flex-direction: column;
    gap: var(--sp-5);
    padding: 0 0 var(--sp-6);
    border: 0;
    border-block-end: 1px solid var(--wx-line);
    border-radius: 0;
    background: transparent;
  }

  .wx-rail-h {
    padding-block-end: 0;
    border-block-end: 0;
  }

  /* THE TOPICS BECOME ONE SCROLLING ROW. Five stacked full-width rows is a list
     that reads as navigation; five chips on a line is a filter, which is what
     they are. It is a real inner scroller, so it says so to the smoother —
     `data-lenis-prevent-touch` is in the markup for the same reason the case
     study rails carry it. */
  .wx-topic-list {
    flex-direction: row;
    gap: var(--sp-3);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    /* THE BAR STAYS. The first version of this rule hid it with
       `scrollbar-width: none` and a `::-webkit-scrollbar` reset, and
       `chrome-contract.test.mjs` failed the build for it — correctly. That ban
       is blunt on purpose: this site deleted its OWN scrollbar and kept the
       platform's (P6-10), so a stylesheet that hides one anywhere is one edit
       away from a page with none. On the widths this rule covers the bar is an
       overlay that costs no space and appears only while the row is moving,
       which is the affordance saying the row moves. */
    padding-block-end: var(--sp-2);
  }

  .wx-topic {
    flex: none;
    gap: var(--sp-3);
    padding: 0.5rem var(--sp-5);
    border: 1px solid var(--wx-line);
    border-radius: 999px;
    scroll-snap-align: start;
    white-space: nowrap;
  }

  .wx-topic[data-on] {
    border-color: var(--wx-ink);
  }

  /* THE TOPIC MIX IS A HOVER AFFORDANCE, AND A PHONE HAS NO HOVER. Ten bars
     three pixels wide are not a glance at 390px, and the readout above them —
     the whole reason the bars are individually pointable — is driven by
     `pointerenter`, which a finger never sends. It is hidden rather than shrunk,
     because a control nobody can operate is not a smaller control.

     NOTHING IS LOST. Each bar is a duplicate link to an article the grid
     directly below already lists, and `emitted-families` asserts that list from
     the markup rather than from what is painted. */
  .wx-mix-w {
    display: none;
  }

  .wx-count {
    padding-block-start: 0;
    border-block-start: 0;
  }
}

@media screen and (max-width: 767px) {
  .wx-facts {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-5);
  }

  .wx-cta {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }

  .wx-lead-body,
  .wx-card-body {
    padding: var(--sp-5);
  }

  .wx-card-foot {
    margin-inline: var(--sp-5);
  }
}

/* ONE COLUMN BELOW 560, EXPLICITLY. `auto-fill` with a `19rem` floor already
   gives one column here — until the root ladder does not: `1rem` is a function
   of viewport width on this site (`tokens/root-font.css`), so 19rem is 209px at
   390 and the grid can fit two 174px cards inside a 358px content box. Two cards
   that narrow is a title per line and a picture the size of a stamp. */
@media screen and (max-width: 559px) {
  .wx-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ── REDUCED MOTION ─────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .wx-grid .wx-cell,
  .wx-grid[data-wrx-live] .wx-cell {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .wx-card:hover,
  .wx-card:focus-visible,
  .wx-lead:hover,
  .wx-lead:focus-visible {
    transform: none;
  }

  .wx-card-img,
  .wx-lead-img,
  .wx-mix-bar,
  .wx-go-arrow,
  .wx-card-go {
    transition: none;
  }

  .wx-card:hover .wx-card-img,
  .wx-lead:hover .wx-lead-img,
  .wx-mix-bar:hover {
    transform: none;
  }
}
}

/* ---- sections · sections/blog-post.css ---- */
@layer sections {
/* ============================================================================
   /blog/<slug> — ONE ARTICLE.

   THE DOCUMENT IS A BREAKOUT GRID AND THE RAIL SITS BESIDE IT.

   `.art-col` names five tracks. Prose lands in `text` by DEFAULT — the rule is
   `.art-col > * { grid-column: text }`, so a figure opts OUT to `wide` or `full`
   and a new element added without thinking lands at the reading measure rather
   than full bleed. Opt-out is the safe direction for a contract nobody will
   re-read.

   IT IS `grid-template-columns` AND NOT NEGATIVE MARGINS, and that is
   load-bearing rather than stylistic: `margin-inline: -7rem` overflows the
   container, gives the whole document a horizontal scrollbar and cannot be
   clipped without creating a scroll container that re-anchors every sticky
   descendant. `minmax(0, …)` simply cannot overflow.

   THE RAIL IS A NAVIGATOR, NOT A COLUMN. The version of this page that gave a
   standing contents list a third of a 1440px window was charging a reader for
   the whole article to serve a control they use twice. It is now 13rem, sticky,
   and it carries three things a reader wants WHILE reading: where they are, who
   wrote it, and how to send it on.
   ========================================================================== */

[data-page='blog-post'] {
  --art-measure: 68ch;
  --art-breakout: 7.5rem;
  --art-rail-w: clamp(12rem, 17vw, 16rem);
  --art-rhythm: var(--section-y);
}

[data-page='blog-post'] .art-s {
  padding-block: var(--art-rhythm);
}

[data-page='blog-post'] .art-wrap {
  max-width: 82rem;
}

/* SCOPED THE SAME WAY `.art-wrap` IS, and that is the whole reason the rule
   works. `[data-page='blog-post'] .art-wrap` is (0,1,1) with an attribute
   selector; a bare `.art-narrow` is (0,1,0) and loses, which is why the first
   version of this reached for `!important` — and `!important` in the `sections`
   layer is banned outright (`css-cascade.test.mjs`), because an earlier layer
   wins on important declarations and the escape hatch would be pointing the
   wrong way. Matching the specificity is the fix; there was never a cascade
   problem, only a selector that was one token short. */
[data-page='blog-post'] .art-wrap.art-narrow {
  max-width: calc(var(--art-measure) + var(--art-breakout) * 2);
}

/* ══════════════════════════════════════════════════ the reading progress */

/* IT SPANS THE ARTICLE, NOT THE DOCUMENT. `installChapterRail` writes `--p` from
   the first chapter's top to the last chapter's bottom, so the bar reaches 100%
   when the reading ends rather than when the footer does — a progress bar that
   still shows a third to go while a reader is looking at a contact block is
   measuring the wrong thing.

   WITH NO SCRIPT `--p` IS UNSET AND THE FALLBACK IS ZERO, which is the honest
   picture of a page whose progress nothing is measuring. */
.art-progress {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  /* THE HEADER'S OWN LAYER, not one above it. Equal z-index resolves by document
     order and `<main>` comes after the header in `base.html`, so the bar paints
     over the header's transparent top edge without being able to cover a control
     the header opens on top of itself. A literal here would also fail INV-11's
     sibling rule that z-index values live in `tokens/layer.css`. */
  z-index: var(--z-nav);
  height: 2px;
  background: transparent;
  pointer-events: none;
}

.art-progress-i {
  display: block;
  width: calc(var(--p, 0) * 100%);
  height: 100%;
  background: var(--wx-accent);
  transform-origin: inline-start;
}

/* ══════════════════════════════════════════════════════════ the masthead */

/* THE PICTURE, THEN ONE PLATE OVER IT. The old opening was six full-width bands
   in a column — crumb, meta, title, standfirst, a bled figure, a byline rule —
   and it read as a list rather than as an opening. It is now two objects: the
   article's own photograph, and a single flat plate carrying everything else,
   riding up over the picture's lower edge.

   NOTHING HERE CASTS A SHADOW. What separates the plate from the picture behind
   it is the plate's own 1px rim and the page's own paper. That is a rule this
   file now keeps everywhere, and the two blurred hover shadows the index used to
   draw were removed with it.

   THE SECTION KEEPS `--header-clear`. A full-bleed picture running up behind the
   bar would put a fixed header over a photograph that is dark on five of the ten
   articles and pale on the other five, against ONE `data-nav-theme-target` value
   for all thirty pages. The picture begins on paper instead. */
[data-page='blog-post'] .art-mast {
  padding-block-start: var(--header-clear);
  padding-block-end: 0;
}

/* ── the picture ────────────────────────────────────────────────────────── */

/* FULL BLEED, OUTSIDE `.c`. It is the article's opening figure and reads as one
   object edge to edge; containing it to the text measure would make it an
   illustration sitting in a column, which is what a figure inside the body
   already is.

   `cover`, NOT `contain`, AND THAT IS THE WHOLE OF WHAT CHANGED WITH THE ART.
   Every picture slot on this blog was `contain` because it held a DRAWING whose
   labels sat at its own edges, and a crop cut them off. A photograph has nothing
   at its edges to lose, so it may fill its box — and a box that is part empty
   mount is the thing `contain` cost us on every card and every article top.

   THE CAP IS `--vh`, WHICH IS `svh`. At 1440 a 16:9 figure is 810px tall and
   would be most of a laptop screen before the title appeared; the cap makes the
   opening a picture with a page under it rather than a picture instead of one.
   On a phone 16:9 of 390 is 219px and the cap never engages. */
.art-hero-fig {
  position: relative;
  margin: 0;
  overflow: hidden;
  /* `width: 100%` IS LOAD-BEARING AND IS NOT A DEFAULT BEING RESTATED. With
     `width: auto` the box has an indefinite inline size, so `max-height` is
     TRANSFERRED back through `aspect-ratio` (CSS Sizing 4 §5.1) and clamps the
     WIDTH as well — measured: 928px inside a 1440px viewport, a full-bleed
     figure two thirds of the way across the page. A definite width leaves the
     cap to do what it was written for: shorten the box and let `cover` crop. */
  width: 100%;
  aspect-ratio: 16 / 9;
  max-height: calc(var(--vh) * 58);
  background: var(--wx-surface);
}

.art-hero-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The parallax writes `--hero-y`; unset it is zero, so the still page is
     correct and the effect is an addition to a finished layout. The `scale(1.06)`
     is the overscan the drift travels inside — see `installHeroParallax`. */
  transform: translate3d(0, var(--hero-y, 0px), 0) scale(1.06);
  will-change: transform;
}

/* ── the plate ──────────────────────────────────────────────────────────── */

/* THE NEGATIVE MARGIN IS THE DESIGN. It lifts the plate over the picture's lower
   edge so the two read as one opening; `position: relative` puts it above the
   figure without a z-index, because a later sibling in the same stacking context
   already paints on top. Below the phone breakpoint the overlap is removed
   rather than reduced: a plate riding over a 219px picture covers most of it. */
.art-open {
  position: relative;
  margin-block-start: calc(var(--sp-10) * -1);
  padding: clamp(1.4rem, 3vw, 2.75rem);
  /* `--wx-plate`, NOT `--page-bg`. The plate has to read as a surface laid over
     the picture, and a panel painted in the page's own paper reads as a hole cut
     in it — the only thing separating the two would be a 1px rim, which is not
     enough at the size this box is. It is the same near-white every other panel
     on the blog stands on. */
  background: var(--wx-plate);
  border: 1px solid var(--wx-rim);
  border-radius: var(--wx-radius-lg);
}

.art-crumb-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-6);
  padding: 0;
  list-style: none;
  font-family: var(--wx-mono);
  font-size: var(--fs-cap);
  color: var(--wx-grey);
}

.art-crumb-list li + li::before {
  content: '/';
  margin-inline-end: var(--sp-3);
  color: var(--wx-soft);
}

.art-crumb-list a {
  color: var(--wx-grey);
  text-decoration: none;
  border-block-end: 1px solid transparent;
}

.art-crumb-list a:hover,
.art-crumb-list a:focus-visible {
  color: var(--wx-accent);
  border-block-end-color: currentColor;
}

.art-crumb-list [aria-current='page'] {
  color: var(--wx-ink);
}

/* THE META IS A LEDGER ROW, not a caption: a hairline under it separates what
   the piece IS from what it SAYS, and the same hairline reappears above the
   byline at the foot of the plate, so the block has a visible top, middle and
   bottom without a single box inside it. */
.art-kicker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-6);
  padding-block-end: var(--sp-5);
  border-block-end: 1px solid var(--wx-line);
  font-family: var(--wx-mono);
  font-size: var(--fs-cap);
  color: var(--wx-grey);
}

.art-h1 {
  max-width: 28ch;
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-h1);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  color: var(--wx-ink);
  text-wrap: balance;
}

.art-standfirst {
  max-width: 62ch;
  margin: var(--sp-6) 0 0;
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--wx-dim);
}

/* ONE HAIRLINE, NOT TWO. This used to be `border-block` — a rule above and a
   rule below — because it was a free-standing band on the page. It is inside the
   plate now, and the plate's own rim is its bottom edge. */
.art-byline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-5);
  margin-block-start: var(--sp-7);
  padding-block-start: var(--sp-6);
  border-block-start: 1px solid var(--wx-line);
}

.art-byline-face {
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--wx-rim);
}

.art-byline-who {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  font-size: var(--fs-sm);
}

.art-byline-who b {
  color: var(--wx-ink);
  font-weight: 600;
}

.art-byline-who span {
  color: var(--wx-grey);
  font-size: var(--fs-cap);
}

.art-byline-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-inline-start: auto;
}

/* ═══════════════════════════════════════════════ the answer and takeaways */

.art-answer {
  padding: var(--sp-7);
  border: 1px solid var(--wx-rim);
  border-radius: var(--wx-radius-lg);
  background: var(--wx-plate);
}

.art-answer-label {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-5);
  color: var(--wx-accent);
}

.art-answer-text {
  margin: 0;
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--wx-ink);
}

.art-takeaways {
  margin-block-start: var(--sp-9);
}

.art-take-label {
  margin: 0 0 var(--sp-5);
  color: var(--wx-grey);
}

.art-take-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: var(--sp-5);
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: none;
}

.art-take {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-6);
  border-block-start: 2px solid var(--wx-rim);
  transition: border-color var(--duration-base) var(--wx-ease);
}

.art-take:hover {
  border-block-start-color: var(--wx-accent);
}

.art-take-n {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: 1;
  color: var(--wx-accent);
}

.art-take-t {
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--wx-dim);
}

/* ═══════════════════════════════════════════════════ the body and the rail */

[data-page='blog-post'] .art-body-s {
  display: grid;
  grid-template-columns: var(--art-rail-w) minmax(0, 1fr);
  gap: clamp(2rem, 4vw, 4.5rem);
  width: 100%;
  max-width: var(--fluid-container);
  margin-inline: auto;
  padding-inline: var(--padding--container);
  align-items: start;
}

/* A COMPACT CHAPTER INSTRUMENT. The changing plate answers “where am I?” in
   full, while the ruled list answers “where can I go?” in one line per chapter.
   The list keeps a fixed row size because the runtime advances its seven-row
   window in whole rows; all visual emphasis is therefore paint and transform,
   never a layout change during scroll. */
.art-nav {
  --art-toc-row: 2.55rem;
  --art-toc-rows: 7;
  --art-toc-off: 0;

  position: sticky;
  top: calc(var(--header-clear) + var(--sp-5));
}

/* The toggle is the phone control and does nothing on a desktop, where the
   panel is always open. It is in the markup rather than created by the runtime
   because a control that appears late shifts the layout when it arrives.

   `display: none` HERE IS LOAD-BEARING BEYOND LAYOUT. `installChapterRail`
   decides whether the panel is a disclosure or a standing rail by asking
   `getComputedStyle(toggle).display !== 'none'` — INV-11 forbids repeating the
   992 literal in a runtime module, so the stylesheet is the single authority.
   If this ever computes to anything else above the breakpoint, all thirty
   article pages boot with the panel `hidden` and an empty column beside the
   prose. That is a defect this project has already shipped once. */
.art-nav-btn {
  display: none;
}

.art-nav-panel {
  position: relative;
  padding: var(--sp-3);
  border: 1px solid var(--wx-line);
  border-radius: var(--wx-radius-lg);
  background: color-mix(in srgb, var(--wx-plate) 88%, transparent);
}

/* ── the head: a label and a tally ──────────────────────────────────────── */

.art-toc-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-inline: var(--sp-2);
  margin-block-end: var(--sp-3);
}

.art-toc-label {
  margin: 0;
  color: var(--wx-grey);
}

/* `03 / 09`. The TOTAL is a build fact (`build.article.chapters`) so it is right
   with no script; the CURRENT number ships as an em dash and the runtime writes
   it. Tabular figures so the row cannot jog sideways as the number changes from
   one digit's shape to another.

   IT IS ONE COMPOSITE VALUE AND IT DECLARES ITS OWN DIRECTION, which is the
   defect this article's own chapter three is about. Three inline boxes — the
   number, the slash, the total — are three runs to the flex main axis, so on the
   Arabic document they laid out in reverse and the rail read `09 / —`.
   `golden:rtl` caught it on all thirty Arabic routes before anyone saw it.

   `ltr` PLUS `isolate`, the same pair `.art-src-file` uses for a file path and
   for the same reason: the value is Latin numerals in every one of the three
   locales, and the isolation stops it reordering the label beside it in the
   right-to-left row that contains it. Mirroring the ROW is correct — the label
   and the tally do swap ends — and mirroring the value inside it is not. */
.art-toc-tally {
  margin: 0;
  flex: none;
  font-family: var(--wx-mono);
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
  color: var(--wx-soft);
  direction: ltr;
  unicode-bidi: isolate;
}

.art-toc-tally b {
  color: var(--wx-ink);
  font-weight: 600;
}

.art-toc-slash {
  margin-inline: 0.3em;
}

/* The active chapter is the large item. Its title is deliberately one line: a
   rail is for orientation, and the heading in the document carries the long
   form. The plate changes only when the active chapter changes. */
.art-toc-now {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-3);
  min-height: 4.25rem;
  margin-block-end: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--wx-ink);
  border-radius: calc(var(--wx-radius) - 2px);
  background: var(--wx-ink);
  color: var(--page-bg);
  overflow: hidden;
}

/* THE NUMERAL, AT DISPLAY SIZE. It is the rail's one large mark, and it is the
   accent rather than the ink because it is the only thing in the column that
   answers "where am I" at a glance — the reader is looking for a number, not
   reading a word.

   `--wx-accent` is 4.82:1 on paper and this is text a reader is meant to read,
   which is why it is not `--wx-accent-lg` (3.23:1) even at this size: the AA
   large-text allowance is for 24 px at normal weight or 18.66 px bold, and the
   root font on this site is a function of viewport width — at 480 px this
   numeral is not large text at all. */
.art-toc-now-n {
  margin: 0;
  font-family: var(--wx-mono);
  font-size: var(--fs-h4);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--color--lime);
  direction: ltr;
  unicode-bidi: isolate;
}

.art-toc-now-t {
  margin: 0;
  font-size: var(--fs-cap);
  font-weight: 600;
  line-height: 1.2;
  color: var(--page-bg);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── the meter ──────────────────────────────────────────────────────────── */

/* HOW MUCH IS LEFT. `--p` is written by `installChapterRail` on the rail itself
   and on the root, measured over the ARTICLE rather than the document, so it
   reaches 100% when the reading ends rather than when the footer does. Unset it
   is zero, which is the honest picture of a page whose progress nothing is
   measuring.

   It lives INSIDE the panel above rather than beside the list, because the two
   things it belongs with are the chapter's number and its title: those three
   are one answer to "where am I", and the list is the answer to a different
   question. */
.art-toc-meter {
  grid-column: 1 / -1;
  height: 2px;
  margin-block-start: 0;
  background: color-mix(in srgb, var(--page-bg) 20%, transparent);
  border-radius: 2px;
  overflow: hidden;
}

.art-toc-meter-i {
  display: block;
  width: calc(var(--p, 0) * 100%);
  height: 100%;
  background: var(--color--lime);
  transition: width 80ms linear;
}

/* ── the window, and the list that glides inside it ─────────────────────── */

/* EXACTLY `--art-toc-rows` ROWS TALL, AND CLIPPED. The height is a declared
   number of rows rather than a `max-height` in viewport units, so the window can
   never end halfway through a row — which is what a partially visible chapter
   looks like, and it reads as a rendering fault rather than as an affordance.

   `clip` RATHER THAN `hidden`, as everywhere in this project: `hidden` would make
   this a scroll container, and then a focus move inside it could scroll a row
   into view with no scrollbar to explain why — and it would put a second
   scroller in front of the touch layer. `clip` cannot scroll at all, which is
   exactly the property the transform below depends on. */
.art-toc-win {
  height: calc(var(--art-toc-rows) * var(--art-toc-row));
  overflow: clip;
}

/* THE LIST MOVES, THE WINDOW DOES NOT. `--art-toc-off` is written by the runtime
   in ROWS — one integer, only when the chapter changes — so the stylesheet keeps
   the only copy of how tall a row is and the runtime never computes a pixel.

   THE TRANSITION IS THE WHOLE OF THE MOTION IN THIS RAIL, and it fires at most
   once per chapter. There is nothing here that animates while a reader scrolls,
   which is what the sliding marker this replaced got wrong twice. */
.art-toc-list {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
  transform: translateY(calc(var(--art-toc-off) * var(--art-toc-row) * -1));
  transition: transform var(--duration-base) var(--wx-ease);
}

/* A ROW IS A FIXED HEIGHT AND ONE LINE. Both halves are load-bearing: the window
   translates by whole rows, so a row that grows to fit a long title would put
   every row below it out of register with the offset. The full title is not lost
   — it is in the panel above the moment the reader is in that chapter, and it is
   still the link's complete accessible name here, because the text is clipped by
   the box and not truncated in the DOM. */
/* THE WINDOW OPENS FOR A KEYBOARD, and this is the whole of the rail's focus
   handling — no listener, no second authority for the list's position.

   `overflow: clip` has no scroll position, so a browser that focuses a clipped
   row cannot reveal it by scrolling the box; it scrolls the DOCUMENT instead,
   which moves the reader to somewhere they did not ask to go. Dropping the clip
   and the translation while anything in the rail has focus means every row is
   already on the screen before focus can reach one, so there is nothing to
   reveal. Nine rows are 97 px taller than seven; the rail still ends 158 px
   above the fold at 1440x900.

   THE RUNTIME TRIED TO DO THIS AND LOST. A `focusin` handler that set the offset
   from the focused row was overruled a frame later by the scroll handler, which
   was right to slide the window back to the chapter being read. See the note in
   `runtime/pages/blog-post.js`. */
.art-nav:focus-within .art-toc-win {
  height: auto;
}

.art-nav:focus-within .art-toc-list {
  transform: none;
}

.art-toc-a {
  position: relative;
  display: grid;
  grid-template-columns: 2.25em minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-3);
  height: var(--art-toc-row);
  padding-inline: var(--sp-2) var(--sp-3);
  border-block-end: 1px solid var(--wx-line);
  border-radius: 8px;
  color: var(--wx-soft);
  font-size: var(--fs-cap);
  line-height: 1.2;
  text-decoration: none;
  transition:
    color var(--duration-fast) var(--wx-ease),
    background-color var(--duration-fast) var(--wx-ease),
    transform var(--duration-base) var(--wx-ease);
}

/* Every visible label is one line; the complete DOM text remains the accessible
   name and the changing plate above shows the active title at greater weight. */
.art-toc-t {
  display: block;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.art-toc-a:hover,
.art-toc-a:focus-visible {
  background: var(--wx-wash-soft);
  color: var(--wx-ink);
  transform: translateX(0.3rem) scale(1.015);
}

html[dir='rtl'] .art-toc-a:hover,
html[dir='rtl'] .art-toc-a:focus-visible {
  transform: translateX(-0.3rem) scale(1.015);
}

.art-toc-a[data-on] {
  background: var(--wx-wash);
  color: var(--wx-ink);
  font-weight: 600;
  transform: translateX(0.2rem);
}

html[dir='rtl'] .art-toc-a[data-on] {
  transform: translateX(-0.2rem);
}

/* THE NUMBER IS THE STATE. Reading a chapter fills its own numeral block with
   the accent instead of sliding a separate mark down a track beside it: one
   element, no second position to keep in step, and nothing that animates while a
   reader scrolls.

   NOT `--wx-rim`. That is a border value at 1.82:1 and this is a NUMBER a reader
   is meant to read — 890 of the 895 contrast findings in one sweep were that one
   declaration, once per chapter, on twenty pages. `--wx-soft` measures 4.79:1;
   the filled state is `--page-bg` on `--wx-accent`, which is 4.82:1 inverted. */
.art-toc-n {
  display: block;
  padding: 0.15em 0;
  border-radius: 4px;
  font-family: var(--wx-mono);
  font-variant-numeric: tabular-nums;
  text-align: center;
  color: var(--wx-soft);
  transition:
    background-color var(--duration-fast) var(--wx-ease),
    color var(--duration-fast) var(--wx-ease),
    transform var(--duration-base) var(--wx-ease);
}

.art-toc-a[data-on] .art-toc-n {
  background: var(--wx-accent);
  color: var(--page-bg);
  transform: scale(1.08);
}

/* ── the document ───────────────────────────────────────────────────────── */

.art-doc {
  min-width: 0;
}

.art-sec {
  padding-block-end: var(--sp-10);
}

/* THE RHYTHM IS `row-gap`, AND EVERY CHILD'S MARGIN IS ZERO OR AN ADDITION TO
   IT. Margins do NOT collapse between grid items — so the first version of this
   file, which spaced paragraphs with `margin: var(--sp-6) 0`, put a top margin
   and a bottom margin between every pair and the prose ran at double the
   intended leading. One gap, declared once, in one place. */
.art-col {
  display: grid;
  row-gap: var(--sp-6);
  grid-template-columns:
    [full-start] minmax(0, 1fr)
    [wide-start] minmax(0, var(--art-breakout))
    [text-start] min(var(--art-measure), 100%) [text-end]
    minmax(0, var(--art-breakout))
    [wide-end] minmax(0, 1fr) [full-end];
}

.art-col > * {
  grid-column: text;
}

.art-col > [data-art-wide] {
  grid-column: wide;
}

.art-col > [data-art-full] {
  grid-column: full;
}

/* ── the chapter head ───────────────────────────────────────────────────── */

.art-sec-head {
  position: relative;
  margin-block: var(--sp-8) 0;
  padding-block-end: var(--sp-5);
  border-block-end: 1px solid var(--wx-rule);
}

/* THE GHOST NUMERAL. It sits behind the heading rather than beside it, at a
   weight that reads as a watermark on the page and never as a number a reader
   has to parse. `aria-hidden`, because the chapter's own `01` is already in the
   rail and in the anchor. */
/* IT SITS IN THE MARGIN THE BREAKOUT GRID ALREADY MAKES, not behind the
   heading. Behind was the first attempt and it was wrong twice over: at 8% ink
   it still showed through the provenance line above the title, and pushing it
   further back with `z-index: -1` put it behind the section's own background on
   any browser that promoted the section to a layer. In the gutter it is a
   chapter number in a book margin — legible, out of the text, and costing
   nothing, because that column exists whether or not anything is in it. */
/* IT IS A NUMBER, SO IT HAS TO BE READABLE — the watermark version failed.
   Drawn at `--fs-impact` in 7%-ink it was ornament by intent, and
   `golden:site-a11y` scored it 170 times across twenty articles: axe cannot tell
   decoration from content, and it is right not to try, because a sighted reader
   sees a numeral and cannot make it out. The choice was to excuse it by name or
   to stop shipping unreadable type, and the second is the honest one.

   So it is smaller, it is `--wx-mute` (3.25:1 — AA for large text, which at
   `--fs-h1` this is), and it reads as what it always claimed to be: a chapter
   number in the margin of a book. It keeps `aria-hidden` because the same number
   is already in the rail and in the anchor, and a screen reader does not need it
   three times. */
.art-sec-ghost {
  position: absolute;
  inset-block-start: -0.06em;
  inset-inline-end: calc(100% + var(--sp-5));
  font-family: var(--font-display);
  font-size: var(--fs-h1);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--track-display);
  color: var(--wx-mute);
  pointer-events: none;
  user-select: none;
}

.art-sec-note {
  margin: 0 0 var(--sp-4);
  font-family: var(--wx-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.02em;
  color: var(--wx-soft);
  overflow-wrap: anywhere;
}

.art-h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
  color: var(--wx-ink);
  text-wrap: balance;
}

.art-h2-plain {
  margin: 0 0 var(--sp-7);
  padding-block-end: var(--sp-5);
  border-block-end: 1px solid var(--wx-rule);
}

.art-p {
  margin: 0;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--wx-dim);
}

/* ── the inline cross-reference ─────────────────────────────────────────── */

/* A 1px underline that lives BELOW the baseline rather than through the
   descenders, and brightens on hover. `text-decoration` with an offset would do
   it in one property and would also be inherited by the surrounding sentence's
   own decoration on some engines; a border on the inline box will not. */
.art-xref-a {
  color: var(--wx-accent);
  text-decoration: none;
  border-block-end: 1px solid color-mix(in oklab, var(--color--lime-ink) 40%, transparent);
  transition: border-color var(--duration-fast) var(--wx-ease);
}

.art-xref-a:hover,
.art-xref-a:focus-visible {
  border-block-end-color: var(--wx-accent);
}

/* ═══════════════════════════════════════════════════════ questions ══════ */

.art-faq {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

.art-q {
  border: 1px solid var(--wx-line);
  border-radius: var(--wx-radius);
  background: var(--wx-plate);
}

.art-q-s {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
  padding: var(--sp-5) var(--sp-6);
  cursor: pointer;
  list-style: none;
}

.art-q-s::-webkit-details-marker {
  display: none;
}

.art-q-t {
  font-size: var(--fs-body);
  font-weight: 600;
  line-height: var(--lh-snug);
  color: var(--wx-ink);
}

/* A drawn plus that becomes a minus. Two rules and a rotation, so it is the same
   mark in every font and on every document. */
.art-q-mark {
  position: relative;
  flex: none;
  width: 14px;
  height: 14px;
}

.art-q-mark::before,
.art-q-mark::after {
  content: '';
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 0;
  width: 100%;
  height: 2px;
  margin-block-start: -1px;
  border-radius: 1px;
  background: var(--wx-accent);
  transition: transform var(--duration-base) var(--wx-ease);
}

.art-q-mark::after {
  transform: rotate(90deg);
}

.art-q[open] .art-q-mark::after {
  transform: rotate(0deg);
}

.art-q-a {
  margin: 0;
  padding: 0 var(--sp-6) var(--sp-6);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--wx-dim);
}

/* ═══════════════════════════════════════════════════════════ sources ════ */

.art-src-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
  gap: var(--sp-8);
}

.art-src-label {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-5);
  color: var(--wx-grey);
}

.art-src-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

.art-src-row {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding-block-start: var(--sp-4);
  border-block-start: 1px solid var(--wx-line);
}

.art-src-file {
  font-family: var(--wx-mono);
  font-size: var(--fs-cap);
  color: var(--wx-ink);
  overflow-wrap: anywhere;
  /* A file path is Latin on every document, including the Arabic one. */
  direction: ltr;
  unicode-bidi: isolate;
}

.art-src-a {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--wx-accent);
  text-decoration: none;
  border-block-end: 1px solid transparent;
}

.art-src-a:hover,
.art-src-a:focus-visible {
  border-block-end-color: currentColor;
}

.art-src-out {
  display: inline-flex;
  width: 0.75em;
  height: 0.75em;
}

.art-src-out svg {
  width: 100%;
  height: 100%;
}

.art-src-note {
  font-size: var(--fs-cap);
  line-height: var(--lh-body);
  color: var(--wx-grey);
}

/* ═══════════════════════════════════════════════════════════ the author ══ */

.art-author {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-7);
  padding: var(--sp-7);
  border: 1px solid var(--wx-line);
  border-radius: var(--wx-radius-lg);
  background: var(--wx-plate);
}

.art-author-face {
  width: 5rem;
  height: 5rem;
  border-radius: var(--wx-radius);
  object-fit: cover;
  border: 1px solid var(--wx-rim);
}

.art-author-say {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.art-author-label {
  margin: 0;
  color: var(--wx-accent);
}

.art-author-name {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-h4);
  font-weight: 600;
  color: var(--wx-ink);
}

.art-author-role {
  margin: 0;
  font-size: var(--fs-cap);
  color: var(--wx-grey);
}

.art-author-bio {
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--wx-dim);
}

.art-author-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-5);
  margin: var(--sp-4) 0 0;
  padding: 0;
  list-style: none;
}

.art-author-go {
  font-size: var(--fs-sm);
  color: var(--wx-accent);
  text-decoration: none;
  border-block-end: 1px solid transparent;
}

.art-author-go:hover,
.art-author-go:focus-visible {
  border-block-end-color: currentColor;
}

.art-author-actions,
.art-hire-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-block-start: var(--sp-5);
}

/* ═══════════════════════════════════════════════════════════ read next ══ */

.art-next {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: var(--sp-6);
  margin: 0 0 var(--sp-9);
  padding: 0;
  list-style: none;
}

.art-next-a {
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  border: 1px solid var(--wx-line);
  border-radius: var(--wx-radius);
  background: var(--wx-plate);
  color: inherit;
  text-decoration: none;
  transition:
    border-color var(--duration-base) var(--wx-ease),
    transform var(--duration-base) var(--wx-ease);
}

.art-next-a:hover,
.art-next-a:focus-visible {
  border-color: var(--wx-accent);
  transform: translateY(-3px);
}

.art-next-plate {
  display: block;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  background: var(--wx-surface);
  border-block-end: 1px solid var(--wx-line);
}

.art-next-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--duration-slow) var(--wx-ease);
}

.art-next-a:hover .art-next-img,
.art-next-a:focus-visible .art-next-img {
  transform: scale(1.03);
}

.art-next-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-6);
}

.art-next-k {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  font-family: var(--wx-mono);
  font-size: var(--fs-micro);
  color: var(--wx-grey);
}

.art-next-t {
  font-family: var(--font-display);
  font-size: var(--fs-h4);
  font-weight: 600;
  line-height: var(--lh-snug);
  color: var(--wx-ink);
}

.art-next-n {
  font-size: var(--fs-cap);
  line-height: var(--lh-body);
  color: var(--wx-dim);
}

/* ── the previous / next rail ───────────────────────────────────────────── */

.art-seq {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  margin: 0 0 var(--sp-9);
  padding: 0;
  list-style: none;
  border: 1px solid var(--wx-line);
  border-radius: var(--wx-radius);
  background: var(--wx-plate);
  overflow: hidden;
}

.art-seq-a {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  height: 100%;
  padding: var(--sp-6);
  background: var(--wx-plate);
  box-shadow: 0 0 0 1px var(--wx-line);
  color: inherit;
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--wx-ease);
}

.art-seq-a:hover,
.art-seq-a:focus-visible {
  background: var(--wx-wash-soft);
}

.art-seq-a.is-next {
  text-align: end;
}

.art-seq-k {
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--wx-accent);
}

.art-seq-t {
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
  color: var(--wx-ink);
}

/* ── the close ──────────────────────────────────────────────────────────── */

.art-hire {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-7);
  padding: clamp(1.4rem, 2.8vw, 2.6rem);
  border: 1px solid var(--wx-rim);
  border-radius: var(--wx-radius-lg);
  background: var(--wx-plate);
}

.art-hire-face {
  width: 5rem;
  height: 5rem;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--wx-rim);
}

.art-hire-h {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: var(--lh-snug);
  color: var(--wx-ink);
  text-wrap: balance;
}

.art-hire-p {
  max-width: 56ch;
  margin: var(--sp-4) 0 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--wx-dim);
}

/* ══════════════════════════════════════════════════════ THE ENTRANCE ════ */

/* ARMED ONLY ONCE THE OBSERVER EXISTS, for the same reason the index's is:
   `[data-art-live]` is written on the document AFTER the IntersectionObserver
   is constructed, so a script that throws leaves a complete article rather than
   a blank one. Under `reduce` the installer returns null and it is never
   written. */
[data-art-live] .art-reveal {
  opacity: 0;
  transform: translateY(12px);
}

[data-art-live] .art-reveal.is-in {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--duration-slow) var(--wx-ease),
    transform var(--duration-slow) var(--wx-ease);
}

/* ══════════════════════════════════════════════════════ breakpoints ═════ */

@media screen and (max-width: 991px) {
  [data-page='blog-post'] {
    --art-breakout: 0rem;
  }

  .art-sec-ghost {
    display: none;
  }

  [data-page='blog-post'] .art-body-s {
    display: block;
  }

  /* THE CHAPTER RAIL IS NOT ON A PHONE AT ALL — owner decision, and the reason
     is the screen rather than the rail.

     WHAT WAS HERE. Two designs, both shipped and both withdrawn: a bordered card
     between the masthead and the prose, and then a sticky one-line chapter bar
     pinned under the header with the ledger as a drop-down sheet. The sticky bar
     was the better of the two and it is still the wrong thing on 390 px, because
     a permanent bar under a floating header takes a line of a screen that is
     already short, sits over the paragraph being read the moment it opens, and
     answers a question — "which of nine chapters am I in" — that a reader who
     can see only one paragraph at a time is not asking.

     THE READER DOES NOT LOSE THE ANSWER. `.art-progress` is a two-pixel bar
     fixed to the top of the window on EVERY width, fed by the same `--p` this
     rail writes, so how far through the article they are is still on the screen.
     What goes is the chapter list, which on a phone is a menu for a document the
     reader is already scrolling linearly.

     `installChapterRail` STILL RUNS, and that is deliberate rather than an
     oversight. It is what writes `--p` to the root element for that bar. It also
     keeps `data-on` correct, so nothing has to be re-established if the window is
     widened past 992 — which is the same document, live, with no reload.

     THE TOGGLE KEEPS `display: flex` BELOW THIS BREAKPOINT. It is invisible
     either way inside a hidden nav, and it is the ONE thing the runtime reads to
     decide whether the panel is a disclosure or a standing rail:
     `getComputedStyle(toggle).display !== 'none'`, because INV-11 forbids the
     992 literal in a runtime module. Computed style resolves inside a
     `display: none` subtree — a child of a hidden parent still reports its own
     `display` — so the answer stays "disclosure" here and "standing rail" above
     992, which is what it has always been. Delete that declaration and every
     article page above the breakpoint keeps working while every one below it
     silently switches arrangement for no reason. */
  .art-nav {
    display: none;
  }

  .art-nav-btn {
    display: flex;
  }
}

@media screen and (max-width: 767px) {
  /* THE PLATE STOPS RIDING OVER THE PICTURE. At 390 a 16:9 figure is 219px tall
     and a plate lifted `--sp-10` into it covers more than half of what it is
     meant to be introducing. It sits flush below instead, and the picture keeps
     its whole self. */
  .art-open {
    margin-block-start: 0;
    border-start-start-radius: 0;
    border-start-end-radius: 0;
    border-block-start: 0;
  }

  [data-page='blog-post'] .art-mast {
    padding-block-start: calc(var(--header-clear) - var(--sp-4));
  }

  /* THE RHYTHM IS THE DESKTOP'S AND IT IS TOO WIDE HERE. `--sp-8` between a
     paragraph and the next chapter head is 32px at 390, which reads as a gap
     rather than as a break; `--sp-6` keeps the break and gives the words back
     the screen. */
  .art-sec {
    padding-block-end: var(--sp-8);
  }

  .art-sec-head {
    margin-block-start: var(--sp-6);
  }

  .art-answer {
    padding: var(--sp-6);
  }

  .art-take {
    padding: var(--sp-5);
  }

  .art-author,
  .art-hire {
    grid-template-columns: minmax(0, 1fr);
    padding: var(--sp-6);
  }

  .art-next-body {
    padding: var(--sp-5);
  }

  .art-seq {
    grid-template-columns: minmax(0, 1fr);
  }

  .art-seq-a {
    padding: var(--sp-5);
  }

  .art-seq-a.is-next {
    text-align: start;
  }

  /* The two actions get their own row at full width rather than being pushed to
     the end of a row that has already wrapped. */
  .art-byline-actions {
    margin-inline-start: 0;
    width: 100%;
  }

  .art-byline-actions > * {
    flex: 1 1 auto;
    justify-content: center;
  }
}

/* ══════════════════════════════════════════════════ REDUCED MOTION ══════ */

@media (prefers-reduced-motion: reduce) {
  [data-art-live] .art-reveal,
  [data-art-live] .art-reveal.is-in {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .art-hero-img {
    transform: none;
  }

  /* THE RAIL'S WINDOW STILL SHOWS THE RIGHT ROWS. `--art-toc-off` is a POSITION,
     not an animation — dropping the transition drops how it is reached, and the
     list is still where the reader is. */
  .art-toc-list,
  .art-toc-n,
  .art-q-mark::before,
  .art-q-mark::after,
  .art-next-img {
    transition: none;
  }

  .art-next-a:hover,
  .art-next-a:focus-visible {
    transform: none;
  }

  .art-next-a:hover .art-next-img {
    transform: none;
  }
}
}

/* ---- sections · sections/blog-charts.css ---- */
@layer sections {
/* ============================================================================
   THE CHARTS.

   TWO FORMS AND ONE COLOUR, and every decision below is from the numbers rather
   than from taste.

   ── ONE SERIES, SO ONE COLOUR, AND NO LEGEND ─────────────────────────────
   Every chart on this blog plots a single measured column. Shading each bar by
   its own value — the reflex — would encode length twice, spend the only free
   channel on information the length already carries, and require a ramp that
   cannot hold contrast across its light end. So every mark is `--wx-accent`,
   and there is no legend box, because a box with one swatch restates the
   caption underneath it.

   ── THE FILL IS #0c7a55 AND NEVER #21f1a8 ────────────────────────────────
   Measured, not estimated: the site's bright lime is **1.33:1** against this
   paper. WCAG asks 3:1 of a graphic that carries meaning. `--wx-accent` is
   4.82:1 and `--wx-accent-lg` — the hover step — is 3.23:1, so the hover cannot
   drop the mark below the floor either.

   ── EVERY BAR IS LABELLED, SO THERE ARE NO GRIDLINES ─────────────────────
   The usual rule is to label selectively and let an axis carry the rest. That
   rule is about dense series; these are four to eight rows with a name on each,
   and the value is the entire point of the figure. With every value printed at
   its own tip, gridlines and numeric ticks would print the same numbers a second
   time in a fainter colour. What remains is one hairline baseline.

   ── BAR VERSUS DOT IS DECIDED BY THE SPREAD, IN THE BUILD ────────────────
   A bar carries LENGTH, and length is read from zero — so a bar chart of 0.697,
   0.702, 0.644, 0.658 draws four bars nobody can tell apart and hides the exact
   difference the section is about. Below a 25% spread `chartFor()` emits a DOT
   plot instead, which encodes POSITION and may therefore honestly use an axis
   that does not start at zero. It prints both ends of that axis, because an axis
   that does not start at zero has to say where it does.

   ── THE MARKS ARE THIN AND THE END IS ROUNDED AT THE DATA END ONLY ───────
   10px bars, capped; the radius is on the data end and the baseline end stays
   square, so the origin reads as an origin. Both are logical properties, so the
   Arabic document rounds the other end without a second rule.

   ── NO TOOLTIP LAYER, AND THE REASON IS A CONTRACT ───────────────────────
   A hover tooltip is the usual sixth step. It is not here because nothing on
   this blog may create content with JavaScript (`article-figures.js` states the
   rule), and a tooltip that only exists once a script has run is a value some
   readers never get. The obligation it discharges — "the reader can get the
   number" — is discharged better here: every value is printed on the chart, and
   the derived charts sit directly under the table they were read out of, which
   is the table view in full.
   ========================================================================== */

/* NO EXTRA MARGIN, DELIBERATELY. A derived chart sits directly under the table
   it was read out of, and the pair reads as one artefact only if nothing pushes
   them apart — so the chart takes the column's plain row gap and the `--sp-4`
   that `.art-fig` adds is explicitly given back. */
.art-chart {
  margin-block: 0;
}

.art-chart-plot {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-7) var(--sp-6);
  border: 1px solid var(--wx-line);
  border-radius: var(--wx-radius);
  background: var(--wx-plate);
}

.art-chart-row {
  display: grid;
  grid-template-columns: minmax(6rem, 15ch) minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-4);
}

.art-chart-name {
  font-size: var(--fs-cap);
  line-height: 1.35;
  color: var(--wx-dim);
}

/* THE TRACK IS THE AXIS. A hairline the full width of the plot area, so a bar's
   share of it is legible without a single gridline; the baseline itself is the
   inline-start edge, drawn as the track's own leading rule. */
.art-chart-track {
  position: relative;
  display: block;
  height: 10px;
  border-radius: 5px;
  background: var(--wx-surface);
}

.art-chart-track::before {
  content: '';
  position: absolute;
  inset-block: -5px;
  inset-inline-start: 0;
  width: 1px;
  background: var(--wx-rim);
}

.art-chart-fill {
  display: block;
  width: var(--v, 0%);
  height: 100%;
  border-radius: 0;
  border-start-end-radius: 5px;
  border-end-end-radius: 5px;
  background: var(--wx-accent);
}

/* TEXT NEVER WEARS THE DATA COLOUR. The mark beside it carries the identity;
   the number is ink, at the weight a number wants. */
.art-chart-value {
  min-width: 5ch;
  font-family: var(--wx-mono);
  font-size: var(--fs-cap);
  font-variant-numeric: tabular-nums;
  text-align: end;
  color: var(--wx-ink);
}

/* ── the dot plot ───────────────────────────────────────────────────────── */

/* The dot carries a 3px ring in the surface colour so it stays legible where it
   sits on the track — the ring is the separator, never a stroke around the
   mark, which would add ink that is not data. */
.art-chart.is-dot .art-chart-track {
  height: 1px;
  border-radius: 0;
  background: var(--wx-rule);
}

.art-chart.is-dot .art-chart-track::before {
  inset-block: -6px;
}

.art-chart-dot {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: var(--v, 0%);
  width: 11px;
  height: 11px;
  margin-inline-start: -5.5px;
  margin-block-start: -5.5px;
  border-radius: 50%;
  background: var(--wx-accent);
  box-shadow: 0 0 0 3px var(--wx-plate);
}

/* `inset-inline-start` on a percentage already mirrors under `dir="rtl"`, so the
   dot travels from the right on the Arabic document with no second rule — which
   is the whole reason the track is a logical box rather than a transform. */

/* ── the foot: the axis ends and what is plotted ────────────────────────── */

.art-chart-foot {
  display: grid;
  grid-template-columns: minmax(6rem, 15ch) minmax(0, 1fr) auto;
  gap: var(--sp-4);
  margin-block-start: var(--sp-4);
  padding-block-start: var(--sp-4);
  border-block-start: 1px solid var(--wx-line);
  font-family: var(--wx-mono);
  font-size: var(--fs-micro);
  color: var(--wx-grey);
}

.art-chart-key {
  grid-column: 2;
  color: var(--wx-accent);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.art-chart-axis {
  font-variant-numeric: tabular-nums;
}

.art-chart-foot .art-chart-axis:last-child {
  text-align: end;
}

/* ── the growth, which is CSS and not JavaScript ────────────────────────── */

/* THE FIGURE IS CORRECT BEFORE ANYTHING RUNS. A bar's width is `--v`, printed
   into the markup by the build, so with no script every mark is already at its
   measured length. What `.is-in` adds is the TRAVEL from zero — an enhancement
   on a correct figure, which is why the start state is armed by the reveal class
   rather than by a rule that would hide the data if the script never arrived. */
.art-chart.is-in .art-chart-fill {
  animation: art-chart-grow var(--duration-slow) var(--wx-ease) calc(var(--i, 0) * 60ms) both;
}

.art-chart.is-in .art-chart-dot {
  animation: art-chart-land var(--duration-slow) var(--wx-ease) calc(var(--i, 0) * 60ms) both;
}

@keyframes art-chart-grow {
  from {
    width: 0;
  }
  to {
    width: var(--v, 0%);
  }
}

@keyframes art-chart-land {
  from {
    opacity: 0;
    transform: scale(0.4);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media screen and (max-width: 767px) {
  .art-chart-row,
  .art-chart-foot {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .art-chart-name {
    grid-column: 1 / -1;
  }

  .art-chart-key {
    grid-column: 1;
  }
}

/* ── REDUCED MOTION ──────────────────────────────────────────────────────
   The settled state, restored explicitly. `animation: none` alone would leave
   the fill at its `--v` width, which is correct — but the dot's `both` fill
   mode would strand it at `scale(0.4)`, so the end state is written back. */
@media (prefers-reduced-motion: reduce) {
  .art-chart.is-in .art-chart-fill,
  .art-chart.is-in .art-chart-dot {
    animation: none;
  }

  .art-chart-dot {
    opacity: 1;
    transform: none;
  }
}
}

/* ---- sections · sections/legal.css ---- */
@layer sections {
/**
 * legal.css — /impressum and /datenschutz, two pages and one stylesheet.
 *
 * THE CONCEPT, in one sentence: a typeset legal document with a citation
 * gutter — a single hairline spine runs the length of the page, the statute
 * that requires each clause hangs outside it in monospace, and the clause
 * itself sits inside. Every section names the rule it exists to satisfy, so
 * the page can be checked line by line instead of taken on trust. That is the
 * same argument the rest of this site makes; here it costs nothing to make it.
 *
 * These are deliberately the plainest pages on the site. No imagery, no
 * reveal, no scroll effect, no page module. On a site that argues for
 * restraint, the legal pages are where restraint is free — and the restraint
 * is the design decision, not the absence of one.
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * THE SCOPE IS `[data-page='legal']`, WHICH IS THE HTML ELEMENT
 *
 * So the nav, the footer, the loader and the orientation notice are all inside
 * it. Every rule below is therefore written against an `lg-*` class and never
 * against a bare element — a `li { list-style: none }` here would reach the
 * footer's link lists, and a `p { margin: 0 }` would reach the nav.
 *
 * The two documents share this sheet completely. They differ only in length,
 * and the one place that shows is `.lg-toc.is-two`, which folds ten contents
 * rows into two columns on a wide screen and leaves three alone.
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * WHAT WAS DROPPED FROM THE GOLDEN MASTER'S SHEET, AND WHY
 *
 *   .lg-skip           the legacy page carries a second, page-local skip link.
 *                      The shell already has one and it is now localised, so a
 *                      German page no longer offers two ways to skip in two
 *                      languages.
 *   .lgjs              a pre-paint class that, by its own comment, "deliberately
 *                      gates nothing". A gate that gates nothing is a gate
 *                      somebody will one day trust.
 *   the reduce block   `base/reset.css` owns the reduced-motion kill switch for
 *                      the whole site, and under CSS Cascade 5 an `!important`
 *                      in an EARLIER layer beats one in a later layer — so a
 *                      copy here would be strictly weaker than the original.
 *                      Only the one hover offset that reduce cannot flatten on
 *                      its own is restated at the bottom.
 *   #fafcfb            a one-off paper. The rebuilt site's paper is
 *                      `--page-bg`, and a legal page is not the place to
 *                      introduce a second white.
 *
 * Remember 1rem is not 16px: `tokens/root-font.css` makes the root size a
 * fluid function of viewport width, so every rem below is a proportional
 * design unit rather than a pixel count.
 */

/* ── page tokens ────────────────────────────────────────────────────────── */

[data-page='legal'] {
  --lg-ink: var(--color--dark-green);
  /* MEASURED on this page's paper: 6.89:1, comfortably past the 4.5:1 body
     floor, and 6.55:1 on the card tone the callout paints. */
  --lg-dim: var(--color--dark-green-tint-2);
  --lg-line: color-mix(in srgb, var(--color--dark-green) 9%, transparent);
  /* The spine is the only structural graphic on either page, so it is one step
     stronger than a hairline divider — still decorative, still ink, and still
     not a second hue. */
  --lg-rule: color-mix(in srgb, var(--color--dark-green) 15%, transparent);
  --lg-card: color-mix(in srgb, var(--color--dark-green) 6%, var(--color--white));
  --lg-rim: color-mix(in srgb, var(--color--dark-green) 20%, transparent);
  --lg-mono: ui-monospace, SFMono-Regular, Menlo, 'Courier New', monospace;
  --lg-ease: cubic-bezier(0.215, 0.61, 0.355, 1);

  --lg-measure: 62ch; /* the site-wide reading cap */
  --lg-rhythm: var(--section-y); /* one site rhythm */
  --lg-rail: 9.5rem; /* the citation gutter */
  --lg-gutter: 2.25rem; /* rail -> spine */
  --lg-hang: 2.5rem; /* spine -> text; the clause number hangs here */

  background-color: var(--page-bg);
  color: var(--page-ink);
}

/* ── shared ─────────────────────────────────────────────────────────────── */

[data-page='legal'] .lg-s {
  padding-inline: var(--padding--container);
}
[data-page='legal'] .lg-wrap {
  width: 100%;
  /* rail + gutter + hang + measure, and nothing wider. The document is a column
     on a large screen because that is what a document is. */
  max-width: calc(var(--lg-rail) + var(--lg-gutter) + var(--lg-hang) + var(--lg-measure));
  margin-inline: auto;
}

[data-page='legal'] .lg-h1,
[data-page='legal'] .lg-h2,
[data-page='legal'] .lg-h3,
[data-page='legal'] .lg-toc-h,
[data-page='legal'] .lg-side-h,
[data-page='legal'] .lg-callout-h {
  margin: 0;
  text-transform: none;
  font-weight: normal;
}
[data-page='legal'] .lg-p,
[data-page='legal'] .lg-lede,
[data-page='legal'] .lg-dd,
[data-page='legal'] .lg-side-p,
[data-page='legal'] .lg-end-note,
[data-page='legal'] .lg-toc-t {
  margin: 0;
  font-size: var(--fs-body);
  font-weight: normal;
}
[data-page='legal'] .lg-eyebrow {
  margin: 0;
  font-family: var(--lg-mono);
  font-size: var(--fs-micro);
  line-height: var(--lh-flat);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--lg-dim);
}

/* Body links keep an underline at rest — colour alone is not a distinction,
   and on a page with no accent there is no colour to spare anyway. The rest
   state is a quiet rim-grey rule; hover and focus bring it up to ink. */
[data-page='legal'] .lg-a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: color-mix(in srgb, var(--color--dark-green) 40%, transparent);
  text-underline-offset: 0.2em;
  transition:
    text-decoration-color 0.25s var(--lg-ease),
    text-decoration-thickness 0.25s var(--lg-ease);
}
[data-page='legal'] .lg-a:hover {
  text-decoration-color: currentColor;
  text-decoration-thickness: 2px;
}
[data-page='legal'] .lg-code {
  font-family: var(--lg-mono);
  font-size: 0.875em;
  background-color: color-mix(in srgb, var(--color--dark-green) 6%, transparent);
  border-radius: 0.25rem;
  padding: 0.1em 0.35em;
  word-break: break-word;
}

/* ONE FOCUS TREATMENT, for everything focusable on both pages. `:focus-visible`
   only — a permanent ring on a document full of links would be noise, and
   `outline: none` on `:focus` is not written anywhere below. */
[data-page='legal'] .lg-a:focus-visible,
[data-page='legal'] .lg-toc-a:focus-visible,
[data-page='legal'] .lg-end-a:focus-visible {
  outline: 2px solid var(--lg-ink);
  outline-offset: 3px;
}
/* The end links sit in a clipped, rounded pair of cells, so their ring is drawn
   inside the cell — an offset ring there would be cut off by the parent's
   overflow, which is a focus indicator that only looks present. */
[data-page='legal'] .lg-end-a:focus-visible {
  outline-offset: -3px;
}

/* ── masthead ───────────────────────────────────────────────────────────── */

[data-page='legal'] .lg-mast {
  padding-block: calc(var(--on-t-nav-spacer, 5.375rem) + 4.5rem) 0;
}
[data-page='legal'] .lg-h1 {
  margin-top: var(--sp-5);
  font-family: var(--font-display);
  font-size: var(--fs-h1);
  line-height: var(--lh-display);
  letter-spacing: -0.0625rem;
  text-transform: uppercase;
  color: var(--lg-ink);
}
[data-page='legal'] .lg-lede {
  margin-top: var(--sp-6);
  max-width: var(--lg-measure);
  line-height: var(--lh-body);
  color: var(--lg-dim);
}
[data-page='legal'] .lg-lede.is-quiet {
  margin-top: var(--sp-4);
  font-size: var(--fs-sm);
}

/* Date and status, set as the document's own header block: monospace keys,
   because both are values a reader is invited to check. */
[data-page='legal'] .lg-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-7);
  margin: 2.5rem 0 0;
  padding-top: var(--sp-5);
  border-top: 1px solid var(--lg-line);
}
[data-page='legal'] .lg-meta-i {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}
[data-page='legal'] .lg-meta dt {
  font-family: var(--lg-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--lg-dim);
}
[data-page='legal'] .lg-meta dd {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-label);
  color: var(--lg-ink);
}

/* ── contents ───────────────────────────────────────────────────────────── */

[data-page='legal'] .lg-toc-s {
  padding-block: var(--sp-8) 0;
}
[data-page='legal'] .lg-toc-h {
  font-family: var(--lg-mono);
  font-size: var(--fs-micro);
  line-height: var(--lh-flat);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--lg-dim);
}
[data-page='legal'] .lg-toc {
  list-style: none;
  margin: 1.25rem 0 0;
  padding: 0;
  display: grid;
  /* the gap IS the rule: rows sit on the page's own line colour with no border
     to double up */
  gap: 1px;
  background-color: var(--lg-line);
  border-block: 1px solid var(--lg-line);
}
/* Scoped to these two lists, never to `li`, because the footer's own lists are
   inside this page scope too. */
[data-page='legal'] .lg-toc li,
[data-page='legal'] .lg-end-links li {
  list-style: none;
  list-style-type: none;
  background-color: var(--page-bg);
}
[data-page='legal'] .lg-toc li::marker,
[data-page='legal'] .lg-end-links li::marker {
  content: none;
}

[data-page='legal'] .lg-toc-a {
  display: flex;
  align-items: baseline;
  gap: var(--sp-4);
  /* 2.75rem tall at the smallest root size — comfortably past the 24x24 floor
     at 320px, where these rows are the only touch targets on the page. */
  min-height: 2.75rem;
  padding: 0.8125rem 0.875rem;
  color: inherit;
  text-decoration: none;
  transition:
    background-color 0.3s var(--lg-ease),
    padding-left 0.3s var(--lg-ease);
}
[data-page='legal'] .lg-toc-a:hover {
  background-color: color-mix(in srgb, var(--color--dark-green) 4.5%, transparent);
  padding-left: 1.5rem;
}
[data-page='legal'] .lg-toc-n {
  flex: none;
  width: 1.5rem;
  font-family: var(--lg-mono);
  font-size: var(--fs-cap);
  font-variant-numeric: tabular-nums;
  color: var(--lg-dim);
}
[data-page='legal'] .lg-toc-t {
  font-size: var(--fs-sm);
  line-height: var(--lh-label);
}

/* ── the document ───────────────────────────────────────────────────────────
 *
 * THE SPINE. One absolutely-positioned hairline, drawn once for the whole
 * document rather than per section, so consecutive clauses cannot leave a seam
 * where their paddings meet. It starts at the first clause's heading and stops
 * at the last clause's last line — the sections carry their rhythm as top
 * padding only, and the closing full stop is the rule simply ending.
 */

[data-page='legal'] .lg-doc-s {
  padding-block: var(--lg-rhythm) 0;
}
[data-page='legal'] .lg-doc {
  position: relative;
}
[data-page='legal'] .lg-doc::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--lg-rail) + var(--lg-gutter));
  width: 1px;
  background-color: var(--lg-rule);
}

[data-page='legal'] .lg-sec {
  display: grid;
  grid-template-columns: var(--lg-rail) minmax(0, 1fr);
  column-gap: var(--lg-gutter);
  padding-block: var(--lg-rhythm) 0;
}
[data-page='legal'] .lg-sec:first-child {
  padding-top: 0;
}

/* The contents list jumps into the document and the nav is fixed — without this
   the clause you asked for lands underneath it. Ten anchors is enough for that
   to be a defect rather than a nuisance. */
[data-page='legal'] .lg-doc-s,
[data-page='legal'] .lg-sec {
  scroll-margin-top: calc(var(--on-t-nav-spacer, 5.375rem) + 1.5rem);
}

/* The citation hangs outside the spine, right-aligned against it. The nudge is
   optical: a 0.625rem monospace cap sits above a 1.6rem heading's cap. */
[data-page='legal'] .lg-cite {
  margin: 0;
  padding-top: var(--sp-2);
  text-align: right;
  font-family: var(--lg-mono);
  font-size: var(--fs-micro);
  line-height: var(--lh-body);
  letter-spacing: 0.04em;
  color: var(--lg-dim);
}

[data-page='legal'] .lg-body {
  position: relative;
  padding-left: var(--lg-hang);
  max-width: calc(var(--lg-hang) + var(--lg-measure));
}
/* The clause tick: a short ink crossbar laid across the spine at the height of
   the heading, joining the statute on the left to the clause on the right.
   Scanning the rule alone tells you where the clauses begin, and which two
   things each one ties together. It is the only ornament on either page and it
   carries information, which is the only reason it is here. */
[data-page='legal'] .lg-body::before {
  content: '';
  position: absolute;
  left: -0.625rem;
  top: 0.8125rem;
  width: 1.25rem;
  height: 1px;
  background-color: var(--lg-ink);
}

/* The clause number hangs into the gutter between spine and text: negative
   text-indent on the first line, a fixed-width inline-block to land the title
   back on the text axis. It is real text, not a counter, because a clause
   number is how you cite a clause and a screen-reader user needs it too. */
[data-page='legal'] .lg-h2 {
  text-indent: calc(var(--lg-hang) * -1);
  font-size: var(--fs-h2);
  line-height: var(--lh-snug);
  letter-spacing: -0.0125rem;
  font-weight: 650;
  color: var(--lg-ink);
}
/* Right-aligned inside its hanging box, so 1 and 10 end on the same axis a
   little short of the text — the way a numbered document sets its numbers, and
   far enough off the spine that the rule never runs through a digit. */
[data-page='legal'] .lg-num {
  display: inline-block;
  width: var(--lg-hang);
  padding-right: 0.75rem;
  text-indent: 0;
  text-align: right;
  font-family: var(--lg-mono);
  font-size: var(--fs-cap);
  font-variant-numeric: tabular-nums;
  vertical-align: 0.1em;
  color: var(--lg-dim);
}

[data-page='legal'] .lg-p {
  margin-top: var(--sp-5);
  max-width: var(--lg-measure);
  line-height: var(--lh-body);
  color: var(--lg-dim);
}

/* THE LEDGER. Every clause that describes a processing operation answers the
   same four questions in the same order and the same shape — purpose, legal
   basis, recipient, retention. Consistency is what makes a legal document
   scannable, and it is what makes a missing value visible: the gap always
   appears in the same column. */
[data-page='legal'] .lg-dl {
  margin: 1.5rem 0 0;
  max-width: var(--lg-measure);
  border-top: 1px solid var(--lg-line);
}
[data-page='legal'] .lg-dl-i {
  display: grid;
  grid-template-columns: 9.5rem minmax(0, 1fr);
  gap: var(--sp-1) var(--sp-5);
  padding-block: var(--sp-4);
  border-bottom: 1px solid var(--lg-line);
}
[data-page='legal'] .lg-dt {
  font-family: var(--font-label);
  font-size: var(--fs-cap);
  line-height: var(--lh-label);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--lg-ink);
}
[data-page='legal'] .lg-dd {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--lg-dim);
}

/* ── the sidenote ───────────────────────────────────────────────────────────
 *
 * A definition or an aside that is genuinely quieter than the clause it sits
 * under — smaller, dimmer, no fill, one hairline down its left edge. It is
 * never used for anything the reader must not miss.
 */

[data-page='legal'] .lg-side {
  margin-top: var(--sp-5);
  padding-left: 1.25rem;
  border-left: 1px solid var(--lg-rim);
  max-width: var(--lg-measure);
}
[data-page='legal'] .lg-side-h {
  font-family: var(--font-label);
  font-size: var(--fs-cap);
  line-height: var(--lh-label);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--lg-ink);
}
[data-page='legal'] .lg-side-p {
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--lg-dim);
}
/* The DevTools panel names in the verification aside. `em` is the right element
   — it is a different voice, not a louder one — so it is set as ink rather than
   as italics, which a monospace-adjacent UI name reads badly in. */
[data-page='legal'] .lg-side-p em {
  font-style: normal;
  font-weight: 600;
  color: var(--lg-ink);
}

/* Art. 21 DSGVO wants the right to object presented separately and clearly.
   A tone step and a rim is how this site says "separately". */
[data-page='legal'] .lg-callout {
  margin-top: var(--sp-6);
  padding: 1.375rem 1.5rem 1.5rem;
  max-width: var(--lg-measure);
  background-color: var(--lg-card);
  border: 1px solid var(--lg-rim);
  border-radius: 0.625rem;
}
[data-page='legal'] .lg-callout-h {
  font-size: var(--fs-body);
  line-height: var(--lh-label);
  font-weight: 650;
  color: var(--lg-ink);
}
[data-page='legal'] .lg-callout .lg-p {
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
}

/* ── the end ────────────────────────────────────────────────────────────── */

[data-page='legal'] .lg-end-s {
  padding-block: var(--lg-rhythm) 2rem;
}
[data-page='legal'] .lg-end {
  border-top: 1px solid var(--lg-line);
  padding-top: var(--sp-6);
}
[data-page='legal'] .lg-end-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  background-color: var(--lg-line);
  border: 1px solid var(--lg-line);
  border-radius: 0.625rem;
  overflow: hidden;
}
[data-page='legal'] .lg-end-a {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  height: 100%;
  padding: 1.25rem 1.25rem 1.375rem;
  color: inherit;
  text-decoration: none;
  transition: background-color 0.3s var(--lg-ease);
}
[data-page='legal'] .lg-end-a:hover {
  background-color: var(--lg-card);
}
[data-page='legal'] .lg-end-k {
  font-family: var(--lg-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--lg-dim);
}
[data-page='legal'] .lg-end-t {
  font-size: var(--fs-lead);
  line-height: var(--lh-snug);
  font-weight: 620;
}
[data-page='legal'] .lg-end-note {
  margin-top: var(--sp-5);
  max-width: var(--lg-measure);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--lg-dim);
}

/* ── 991 and down ───────────────────────────────────────────────────────────
 *
 * The citation gutter is a wide-screen luxury. Below 992px the spine goes and
 * each clause becomes a ruled block with its statute set above the heading —
 * the same document, folded. The hanging number stays, because it is what makes
 * a numbered document read as one at any width.
 */

@media screen and (max-width: 991px) {
  [data-page='legal'] {
    --lg-hang: 2rem;
  }
  [data-page='legal'] .lg-doc::before {
    display: none;
  }
  [data-page='legal'] .lg-sec {
    grid-template-columns: minmax(0, 1fr);
    padding-block: var(--sp-8) 0;
    border-top: 1px solid var(--lg-line);
    margin-top: var(--sp-8);
  }
  [data-page='legal'] .lg-sec:first-child {
    margin-top: 0;
    border-top: 0;
    padding-top: 0;
  }
  [data-page='legal'] .lg-cite {
    padding-top: 0;
    margin-bottom: var(--sp-4);
    text-align: left;
  }
  [data-page='legal'] .lg-cite br {
    display: none;
  }
  [data-page='legal'] .lg-body {
    max-width: none;
  }
  [data-page='legal'] .lg-body::before {
    display: none;
  }
  [data-page='legal'] .lg-dl-i {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-2);
  }
}

/* ── 767 and down ───────────────────────────────────────────────────────── */

@media screen and (max-width: 767px) {
  [data-page='legal'] {
    --lg-hang: 1.875rem;
  }
  [data-page='legal'] .lg-mast {
    padding-block: calc(var(--on-t-nav-spacer, 5.375rem) + 3rem) 0;
  }
  [data-page='legal'] .lg-end-links {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ── 992 and up ─────────────────────────────────────────────────────────── */

@media screen and (min-width: 992px) {
  /* Ten clauses is a long list; two columns keep it inside one screen without
     shrinking a single row. The order stays document order, column by column. */
  [data-page='legal'] .lg-toc.is-two {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-auto-flow: column;
    grid-template-rows: repeat(5, auto);
  }
}

/* ── reduced motion ─────────────────────────────────────────────────────────
 *
 * `base/reset.css` already flattens every transition on the site, and it does
 * it from an EARLIER layer, where an `!important` outranks one written here.
 * The single thing it cannot do from there is undo a layout shift that is not
 * itself a transition: the contents rows move their padding on hover, and under
 * reduce that becomes an instant jump rather than a slide. So the offset is
 * removed instead.
 */

@media (prefers-reduced-motion: reduce) {
  [data-page='legal'] .lg-toc-a:hover {
    padding-left: 0.875rem;
  }
}
}

/* ---- sections · sections/not-found.css ---- */
@layer sections {
/**
 * not-found.css — the 404.
 *
 * Registered in src/styles/order.json before it had content, so that the
 * cascade order is a reviewed decision rather than a side effect of which file
 * was written first.
 *
 * THE WHOLE PAGE IS A HEADING, A SENTENCE AND FOUR LINKS. The Golden Master's
 * 404 spends a Rive canvas, a WebGL dotfield and four unreachable scene
 * switchers on the same message; this one spends a stylesheet. An error page is
 * served to somebody who is already lost, frequently on a poor connection, and
 * frequently to a crawler forming a first impression of the site — it is the
 * one page where weight is least affordable.
 *
 * THE NUMBER IS DECORATION AND IS MARKED AS ONE. `404` is `aria-hidden`,
 * because a screen-reader user is told what happened by the heading in their own
 * language, and three digits announced ahead of it are noise. It is set large
 * because a sighted reader recognises the shape of an error page before they
 * read a word of it.
 *
 * NOTHING HERE MOVES. No reveal, no scroll effect, no transition beyond the
 * link hover — so there is nothing for `prefers-reduced-motion` to switch off
 * that `base/reset.css` does not already own from an earlier layer.
 */

[data-page='not-found'] {
  --nf-line: color-mix(in srgb, var(--color--dark-green) 10%, transparent);
  --nf-rim: color-mix(in srgb, var(--color--dark-green) 20%, transparent);
  --nf-card: color-mix(in srgb, var(--color--dark-green) 5%, var(--color--white));
  /* MEASURED on this page's paper: 6.89:1, past the 4.5:1 body floor. */
  --nf-grey: var(--color--dark-green-tint-2);
  --nf-ease: cubic-bezier(0.215, 0.61, 0.355, 1);
  --nf-measure: 54ch;

  background-color: var(--page-bg);
  color: var(--page-ink);
}

[data-page='not-found'] .nf-s {
  /* The nav is fixed, so the page starts below it and not underneath it. */
  padding-block: calc(var(--on-t-nav-spacer, 5.375rem) + 4.5rem) var(--section-y);
  min-height: 60vh;
}
[data-page='not-found'] .nf-wrap {
  max-width: 68rem;
}

[data-page='not-found'] .nf-code {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-display);
  line-height: 1;
  letter-spacing: -0.02em;
  /* MEASURED. This is decoration — `aria-hidden`, and the heading beside it says
     the same thing in words — but it is still rendered TEXT, so a scanner judges
     it as text and a low-vision reader still sees it. At `--nf-line` it measured
     1.25:1, which is a watermark nobody can read rather than a quiet one.
     70px is large text, so the bar is 3:1; 44% ink gives 3.12:1 and keeps it
     clearly subordinate to the heading. */
  color: color-mix(in srgb, var(--color--dark-green) 44%, var(--color--white));
  /* Tabular figures so the three digits sit on an even rhythm rather than on
     whatever proportional widths the face happens to give 4, 0 and 4. */
  font-variant-numeric: tabular-nums;
}

[data-page='not-found'] .nf-h1 {
  margin: var(--sp-5) 0 0;
  font-family: var(--font-display);
  font-size: var(--fs-h1);
  line-height: var(--lh-display);
  letter-spacing: -0.0625rem;
  text-transform: none;
  font-weight: normal;
  color: var(--page-ink);
  max-width: var(--nf-measure);
}

[data-page='not-found'] .nf-lede {
  margin: var(--sp-5) 0 0;
  max-width: var(--nf-measure);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--nf-grey);
}

/* The links have a real heading rather than a bare list, because the nav that
   labels them needs an accessible name and an invisible one would be a name
   only some readers get. */
[data-page='not-found'] .nf-help {
  margin: var(--sp-8) 0 0;
  padding-top: var(--sp-5);
  border-top: 1px solid var(--nf-line);
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  line-height: var(--lh-flat);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: normal;
  color: var(--nf-grey);
}

[data-page='not-found'] .nf-links {
  margin-top: var(--sp-5);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* The gap IS the rule: cells sit on the page's own line colour, so two
     adjacent borders can never double into a two-pixel seam. */
  gap: 1px;
  background-color: var(--nf-line);
  border: 1px solid var(--nf-line);
  border-radius: 0.625rem;
  overflow: hidden;
}
[data-page='not-found'] .nf-link {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  height: 100%;
  /* A generous target: these four are the only controls on the page, and the
     reader arriving here has already had one address fail them. */
  padding: 1.375rem 1.375rem 1.5rem;
  background-color: var(--page-bg);
  color: inherit;
  text-decoration: none;
  transition: background-color 0.3s var(--nf-ease);
}
[data-page='not-found'] .nf-link:hover {
  background-color: var(--nf-card);
}
[data-page='not-found'] .nf-link:focus-visible {
  /* Drawn INSIDE the cell: the grid clips its overflow, so an offset ring here
     would be cut off — a focus indicator that only looks present. */
  outline: 2px solid var(--page-ink);
  outline-offset: -3px;
}
[data-page='not-found'] .nf-link-t {
  font-size: var(--fs-lead);
  line-height: var(--lh-snug);
  font-weight: 620;
}
[data-page='not-found'] .nf-link-n {
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--nf-grey);
}

@media screen and (max-width: 767px) {
  [data-page='not-found'] .nf-s {
    padding-block: calc(var(--on-t-nav-spacer, 5.375rem) + 3rem) var(--section-y);
  }
  [data-page='not-found'] .nf-links {
    grid-template-columns: minmax(0, 1fr);
  }
}
}

/* ---- utilities · utilities/visually-hidden.css ---- */
@layer utilities {
/* Content available to assistive technology but not painted.
   Accessibility is architectural (ADR-005): this ships before any section does. */
.visually-hidden:not(:focus):not(:active) {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
}

/* ---- utilities · utilities/spacer.css ---- */
@layer utilities {
/* Vertical rhythm as explicit boxes.

   These are rem-based ON PURPOSE, unlike `tokens/type.css`'s `--sp-*` scale.
   A spacer is LAYOUT: it separates two blocks, and on this site layout is
   measured in the root ladder's unit so that a section's air scales with the
   boxes around it rather than against them. The `--sp-*` scale exists for gaps
   INSIDE a component, where the type scale is the right reference.

   The names encode the value because that is what makes a spacer greppable from
   the markup — `.spacer._8rem` in a document is self-describing in a way that
   `.spacer.is-large` is not. */
.spacer {
  width: 100%;
  flex: none;
}

.spacer._2rem {
  height: 2rem;
}
.spacer._4rem {
  height: 4rem;
}
.spacer._5rem {
  height: 5rem;
}
.spacer._6rem {
  height: 6rem;
}
.spacer._8rem {
  height: 8rem;
}
.spacer._8-75rem {
  height: 8.75rem;
}
.spacer._10rem {
  height: 10rem;
}
.spacer._12rem {
  height: 12rem;
}
.spacer._14rem {
  height: 14rem;
}
.spacer._20rem {
  height: 20rem;
}

@media screen and (max-width: 991px) {
  .spacer._20rem.tablet-reduce {
    height: 14rem;
  }
}

@media screen and (max-width: 479px) {
  .spacer._8-75rem.m-6rem {
    height: 6rem;
  }
  .spacer._8-75rem.mobile-shrink {
    height: 4rem;
  }
  .spacer._20rem.tablet-reduce {
    height: 8rem;
  }
}
}

/* ---- utilities · locale/arabic.css ---- */
@layer utilities {
/* ============================================================================
   THE ARABIC LOCALE SHEET.

   Transcribed from the Golden Master's own `site/assets/css/ar.css`, which is
   the most carefully reasoned file in that project: every rule in it carries a
   measurement and several of them record a misrendering that was found on the
   page and fixed. It is the specification for RTL correctness here, and this
   file keeps its reasoning rather than only its declarations.

   SCOPED ON `html[lang='ar']` / `html[dir='rtl']`, WITH ONE DELIBERATE
   EXCEPTION at the foot of this file. Everything else here is provably inert on
   the English and German documents; the exception is the `[lang='ar']` rule that
   sets the Arabic face for an Arabic RUN inside a Latin document, which is a
   thing those two documents genuinely contain — the language switcher's own
   `العربية`. It ships in the one stylesheet every page loads (ADR-006) and
   costs the Latin documents nothing but the bytes, because the Zain faces below
   declare a `unicode-range` that no Latin codepoint falls in and the menu panel
   is `display: none` until it is opened.

   ── P10: ONE TYPEFACE, ZAIN ────────────────────────────────────────────────

   Cairo and Noto Kufi Arabic are gone, at the owner's instruction: all Arabic
   text is Zain (SIL OFL 1.1, `src/assets/fonts/zain/ofl.txt`). Three cuts,
   161,096 B against the 159,900 B the two families it replaces cost — so the
   change is byte-neutral to within a kilobyte, and the Arabic document loads
   three files where it used to load three.

   WHY THREE, AND NOT EIGHT. The family ships ExtraLight through Black plus two
   italics. The promotions below ask for exactly two weights, 600 and 700, and
   nothing on this site declares `font-style: italic`; Arabic has no italic at
   all. Shipping the other five cuts would be 158 KB for weights no rule selects.

   TWO SYNTHETIC FAMILIES FROM THREE FILES, which is the device `base/fonts.css`
   uses for the Latin type and which a single-family Arabic typeface now forces.
   Cairo and Kufi were two DESIGNS, so a display heading and the paragraph under
   it could differ by FACE. Zain is one design, so the only levers left are
   weight and metrics — and one `size-adjust` cannot serve both roles, because
   Zain's tooth-to-alef ratio is not Cairo's. Measured from the outlines with
   fontTools, as em fractions:

                        tooth (ب)   alef (ا)   ratio
       Cairo Regular      50.000%    71.700%    0.697
       Cairo SemiBold     50.100%    71.400%    0.702
       Zain  Regular      44.750%    69.500%    0.644
       Zain  Bold         45.750%    69.500%    0.658
       Noto Kufi Bold     49.500%    76.000%    0.651

   PROSE is read at tooth height, the way Latin prose is read at x-height, so
   `Zain AB` matches Zain's tooth to Cairo's: 0.500 / 0.4475 = 1.117 -> 112%.
   Every Arabic paragraph keeps the measure, the wrap points and the line count
   it has today.

   DISPLAY is read at alef height — every impact word on this site is one word
   of verticals — so `Zain AB Display` matches Zain's alef to Cairo SemiBold's:
   0.714 / 0.695 = 1.027 -> 103%.

   THE DISPLAY FAMILY IS MATCHED TO CAIRO AND NOT TO KUFI, AND THAT IS A VISIBLE
   CHANGE. Kufi's alef is 76.0% and was the largest thing on the Arabic homepage;
   matching it would need 109% and would make every Arabic heading 6% BIGGER,
   which is the opposite of what was reported. Matching Cairo makes them 5.8%
   smaller than the Kufi they replace. It also collapses the size gap `-brier`
   used to have over `-mona`; that gap was a side effect of using two typefaces
   rather than a decision, and the two roles are told apart by WEIGHT — 700
   against 600 — which is what the design actually meant.

   NO `ascent-override` / `descent-override`, AND THAT IS THE ONE PLACE THIS
   FILE DEPARTS FROM `base/fonts.css`. The Latin overrides exist so that a
   Quicksand swap does not reflow against Arial. Arabic cannot do that: Quicksand
   and Arial carry no Arabic at all, so the real fallback is whatever naskh the
   system supplies and there is nothing stable to match. What the overrides would
   otherwise buy — a line box the existing line-heights were tuned against — the
   `size-adjust` already delivers, because `size-adjust` scales the font's own
   metrics too:

       Cairo content box      130.3% + 57.1%           = 1.874em
       Zain at size-adjust    (108.625% + 57.375%) x 1.12 = 1.859em

   0.8% apart, so every Arabic paragraph's line box is the one the numbers below
   were measured against. An override stated as a raw ink percentage would NOT
   have been scaled by `size-adjust` — the spec applies the adjustment first and
   the override replaces the result — and would have been wrong by 12%.

   THE FILES ARE FULL FONTS, DELIBERATELY NOT SUBSET. Subsetting was measured to
   break mark composition: the dots are separate glyphs composed at render time,
   and a subsetter that drops what it thinks is unreachable drops them. That
   decision was made for Cairo and it carries to Zain unchanged.

   `unicode-range` IS NOT SUBSETTING, AND IS NOT THE MECHANISM THAT FAILED. The
   Golden Master hung `unicode-range` faces under the SITE'S OWN family names and
   measured Chromium resolving Arabic runs through full-range Quicksand anyway.
   This is the opposite direction: the ARABIC family is restricted, Quicksand is
   the fallback for everything outside the range, and no font is asked to
   arbitrate a codepoint it does not cover.

   It exists because ZAIN COVERS LATIN — 166 glyphs in U+0041-U+024F. So does
   Cairo, with 291, which means the claim this header used to make (that these
   files "carry only Arabic glyphs" and "Latin falls through to Quicksand") has
   never been true: every project name, every HTML/CSS chip and `Google AI
   Essentials` on `/ar/` has been rendering in Cairo. Zain's Latin is a different
   voice again — cap 69.5%, x-height 46.0%, against Cairo's 69.1% / 50.0% — so
   the drift would have got worse. This is the rule that makes the claim true.
   ============================================================================ */

@font-face {
  font-family: 'Zain AB';
  src: url('/assets/fonts/zain/regular.woff2') format('woff2');
  font-weight: 100 500;
  font-display: swap;
  size-adjust: 112%;
  unicode-range:
    U+0600-06FF, U+0750-077F, U+0870-088E, U+08A0-08FF, U+200F, U+FB50-FDFF, U+FE70-FEFF;
}

@font-face {
  font-family: 'Zain AB';
  src: url('/assets/fonts/zain/bold.woff2') format('woff2');
  font-weight: 501 900;
  font-display: swap;
  size-adjust: 112%;
  unicode-range:
    U+0600-06FF, U+0750-077F, U+0870-088E, U+08A0-08FF, U+200F, U+FB50-FDFF, U+FE70-FEFF;
}

/* THE WEIGHT SPLIT AT 650 IS WHAT KEEPS THE PROMOTIONS BELOW UNCHANGED. This
   sheet already writes 600 for the `-mona` roles and 700 for `-brier`, so a
   display family split there selects Bold for one and ExtraBold for the other
   with no edit to either rule. */
@font-face {
  font-family: 'Zain AB Display';
  src: url('/assets/fonts/zain/bold.woff2') format('woff2');
  font-weight: 100 650;
  font-display: swap;
  size-adjust: 103%;
  unicode-range:
    U+0600-06FF, U+0750-077F, U+0870-088E, U+08A0-08FF, U+200F, U+FB50-FDFF, U+FE70-FEFF;
}

@font-face {
  font-family: 'Zain AB Display';
  src: url('/assets/fonts/zain/extrabold.woff2') format('woff2');
  font-weight: 651 900;
  font-display: swap;
  size-adjust: 103%;
  unicode-range:
    U+0600-06FF, U+0750-077F, U+0870-088E, U+08A0-08FF, U+200F, U+FB50-FDFF, U+FE70-FEFF;
}

/* The blanket, at (0,1,2): it outweighs every single-class `font-family` rule
   in the section sheets without `!important`, and loses on purpose to the two
   promotions below. Quicksand sits behind it and now genuinely renders the
   Latin runs, because the faces above declare a `unicode-range` that stops at
   the Arabic blocks. */
html[lang='ar'] body,
html[lang='ar'] body * {
  font-family: 'Zain AB', Quicksand, Arial, sans-serif;
}

/* Where the site reaches for its Latin display face, Arabic reaches for the
   display cut of its own. `h1/h2/h3` are deliberately not listed: every display
   element on this site carries a `.text-*` role class, and an element selector
   here would also catch the zero-height screen-reader headings.

   QUICKSAND SITS IN THIS STACK TOO, AND LEAVING IT OUT WAS A MEASURED DEFECT.
   The Zain faces stop at the Arabic blocks, so every OTHER codepoint in a
   display heading — the space in `شهادة واحدة`, the `&` in `لغات & تقنيات`,
   any digit — resolves down the stack. Without Quicksand that landed on Arial:
   verified with `CSS.getPlatformFontsForNode`, two Arial glyphs inside the
   certificate heading on `/ar/`. The blanket above already had it; this rule
   did not, and a display heading is the most visible place on the page to be
   quietly setting part of a line in the system's fallback. */
html[lang='ar']
  :is(.text-title-reg-brier, .text-impact-reg-brier, .text-impact-sm-brier, .font-brier) {
  font-family: 'Zain AB Display', 'Zain AB', Quicksand, Arial, sans-serif;
}

/* The "this is measured" mono signal keeps its mono for Latin and figures and
   borrows Zain only for Arabic glyphs inside it. */
html[lang='ar'] :is(code, pre, .cdx-code, .cdx-file, .scx-x) {
  font-family: ui-monospace, sfmono-regular, menlo, 'Zain AB', 'Courier New', monospace;
}

/* ARABIC NEVER LETTER-SPACES. Tracking breaks the joined script — the letters
   stop touching and the word stops being a word. The shared sheets set
   em-tracking on eyebrows, buttons, nav links and display type, so this is the
   one justified blanket `!important` in the project: it is not overriding a
   decision, it is overriding a decision that was only ever about Latin. */
html[lang='ar'] body,
html[lang='ar'] body * {
  letter-spacing: 0 !important;
}

/* The site varies weight through `font-variation-settings`, which is a no-op on
   these static instances — so promote real weights and let the cascade select
   the SemiBold and Kufi faces. */
html[lang='ar']
  :is(
    h1,
    h2,
    h3,
    .text-nav-link,
    .btn-text,
    .text-eyebrow,
    .text-title-lg-mona,
    .text-title-reg-mona,
    .text-impact-reg-mona,
    .text-impact-sm-mona,
    .text-cta-short-intro
  ) {
  font-weight: 600;
}

html[lang='ar'] :is(.text-title-reg-brier, .text-impact-reg-brier, .text-impact-sm-brier) {
  font-weight: 700;
}

/* Latin display sizes assume uppercase ascent only. Arabic carries dots and
   marks ABOVE AND BELOW — a final yeh's dots hang nearly half an em under the
   baseline — so the sub-1 line-heights this site uses collide. Open them.

   1.45, AND IT IS MEASURED RATHER THAN ROUND. Two stacked lines collide INK TO
   INK, never box to box, so the floor is the deepest descender on one line plus
   the tallest ascender on the next — over the strings that actually ship, at
   every WORD BOUNDARY they can wrap at, times the family's own `size-adjust`.
   Computed with fontTools over the Zain outlines:

       footer statement   دائمًا | أستكشف الفكرة التالية      1.3944  <- the floor
       projects heading   مشاريع | مختارة                     1.2952
       certificate        خمس | دورات شهادة واحدة             1.2412
       stack              لغات | & تقنيات                     1.1549

   The binding case is the hamza-bearing alef `أ`, which reaches 1.024em above
   the baseline where a plain alef reaches 0.716 — so a display SENTENCE that
   can put one on a second line is what sets this number, and 1.45 clears it by
   5.6% of an em. This is why the diptych, whose headings are single words that
   cannot wrap, gets its own tighter number in `sections/home-otot.css` rather
   than a smaller value here. */
html[lang='ar']
  :is(
    h1,
    h2,
    h3,
    .text-nav-link,
    .text-title-lg-mona,
    .text-title-reg-mona,
    .text-title-reg-brier,
    .text-impact-reg-mona,
    .text-impact-reg-brier,
    .text-impact-sm-mona,
    .text-impact-sm-brier
  ) {
  line-height: 1.45;
}

/* THE ONE PLACE ON THE SITE WHERE AN ARABIC DISPLAY LINE CANNOT WRAP.

   The rule above has to clear a display SENTENCE that can put a hamza-bearing
   alef on a second line. The code/design diptych's four headings are ONE WORD
   each — `الجانب` over `البرمجي`, `الجانب` over `التصميمي` — so the only
   collision available is the first element's descenders against the second's
   ascenders, and both are known glyphs. Measured with fontTools over the Zain
   cuts these select (ExtraBold at `size-adjust: 103%` over Bold at the same):

       descender of الجانب   0.4262em
       ascender  of البرمجي  0.7158em
       floor                 1.1420em

   1.25 clears that by 10.8% of an em, and takes the Arabic column block from
   391px to 337px at 1440 against the Latin's 247.

   IT LIVES HERE, NOT IN `sections/home-otot.css`, AND THAT IS THE CASCADE'S
   DOING RATHER THAN A PREFERENCE. This sheet is in the `utilities` layer and
   every section sheet is in `sections`, so a LAYER beats specificity: the rule
   above wins over anything a section file can write, however tightly scoped.
   `sections/projects-index.css` gets to keep its own `html[lang='ar']`
   line-height only where the selector it uses is not one this sheet targets.
   Shipped in the section file first and measured at 130.5px where 112.5 was
   intended, which is how the rule was found. */
html[lang='ar'] .otot-home-text-w :is(h1, h2, h3) {
  line-height: 1.25;
}

html[lang='ar'] :is(p, li, .text-body-reg-mona) {
  line-height: 1.8;
}

/* ────────────────────────────────────────────────────────────────────────────
   THE ROLL-UP TWIN, RE-TUNED — three numbers that move together.

   `components/button.css` paints a duplicate of each label one line-box below
   as a `text-shadow`, and a clip window hides it until hover rolls it up. Every
   `--text-offset` equals the element's own font size, i.e. the height of a
   `line-height: 1` box.

   Arabic breaks that in both directions at once. Opening the line box (above)
   without moving the twin lands the twin INSIDE the window — a ghost row. And
   the window itself cuts at 94% of the box, which at any line-height eats the
   dots under a final ي: the Golden Master measured this on its own footer link
   `راسلني`, where the ink ran 5px past the cut and the word rendered as
   `راسلنى` — a different letter, and a misspelling of the owner's own words.

   So all three move together, and the order is the invariant:
   TWIN BELOW WINDOW, WINDOW BELOW INK. */
html[lang='ar'] :is(.btn-text, .text-eyebrow, .text-title-small-label) {
  --ar-label-lh: 1.55;

  line-height: var(--ar-label-lh);
}

html[lang='ar'] .text-clip-w {
  clip-path: polygon(0 -2%, 0 114%, 100% 114%, 100% -2%);
}

html[lang='ar'] [split-text].btn-text {
  --text-offset: 2em;
}

/* The fullscreen menu's link keeps its offset EQUAL to its line box, in em, so
   the two can never drift apart again. `components/site-nav.css` writes the
   Latin `5.25rem`; this is the same quantity expressed as the ratio it has to
   preserve. */
html[lang='ar'] .text-nav-link {
  --ar-nav-lh: 1.55;

  line-height: var(--ar-nav-lh);
  text-shadow: 0 calc(var(--ar-nav-lh) * 1em);
}

/* ────────────────────────────────────────────────────────────────────────────
   LATIN RUNS INSIDE AN RTL DOCUMENT.

   Character-splitting wraps every letter in its own inline box, and inline
   boxes lay out in the CONTAINER's direction — so a Latin word split into
   characters renders REVERSED in RTL. The Golden Master measured `dobbAdaoL`
   on its own `/ar/` where `/` and `/de/` showed `LoadAbbod`. The bidi algorithm
   cannot help: each letter is its own run.

   Every split Latin label needs its own LTR inline context. The language
   switcher's own names are the case that ships on all three documents. */
html[dir='rtl'] :is(.nav-lang-links .btn-text, .btn-text.is-latin) {
  direction: ltr;
}

/* ────────────────────────────────────────────────────────────────────────────
   AN ARABIC RUN INSIDE A LATIN DOCUMENT — the one rule in this file that is not
   scoped to `/ar/`, and the fix for defect P11-03.

   The language switcher prints its three names in their own languages on ALL
   THREE documents, which is what makes it usable: a reader who cannot read the
   current page still recognises `العربية`. `src/content/<locale>/site.json` therefore
   lists `nav.langAr` under `$shared`, and the anchor already carries `lang="ar"`
   and `dir="rtl"` in the markup, so the document says the truth about that run.

   What it did NOT have was a face. Every rule above is scoped on
   `html[lang='ar']`, so on `/` and `/de/` the word inherited `--font-label`
   (Quicksand) — which carries no Arabic at all — and the browser fell through to
   whatever naskh the platform happened to supply. Measured on macOS WebKit and
   Chromium: the label rendered in the system fallback beside four Quicksand
   labels, at a different weight, a different colour density and a different
   optical size. On a device with no Arabic system face it renders as tofu.

   SO THE SELECTOR IS THE LANGUAGE, NOT THE DOCUMENT. `[lang='ar']` matches the
   anchor on every document, including `/ar/` where the blanket already agrees
   with it. The descendant half is required and not belt-and-braces:
   `components/button.css` sets `font-family: var(--font-label)` on `.btn-text`,
   which is a CHILD of the anchor, so a rule on the anchor alone would be
   overridden on the only element that holds the ink. This file sits in the
   `utilities` layer and `button.css` in `components`, so the layer order settles
   it without a specificity fight and without `!important` (INV-10).

   IT DOES NOT TOUCH THE REST OF THE MENU. `English` and `Deutsch` carry
   `lang="en"` and `lang="de"`; nothing else in the panel is marked Arabic. The
   menu's own typography — size, weight, tracking, the roll — is unchanged. */
[lang='ar'],
[lang='ar'] * {
  font-family: 'Zain AB', Quicksand, Arial, sans-serif;
}

/* THE FACE ALONE IS NOT THE FIX. Three more properties were inherited from the
   Latin document and are wrong for this run, and the last two are the defect
   this sheet already records having found on the Golden Master's own footer.

   TRACKING. `base/document.css` sets `letter-spacing: var(--track-body)` on
   `body` and it inherits into every label. Arabic is joined; tracking pulls the
   letters apart and the word stops being a word — the rule this sheet states at
   length above. No `!important` is needed here and none is used: an inherited
   value always loses to a declaration on the element itself, so the blanket that
   needs `!important` on `/ar/` (where it is overriding real declarations in the
   section sheets) does not need it here.

   THE LINE BOX AND THE WINDOW, TOGETHER. `.btn-text` is `line-height: 1` and
   `.text-clip-w` clips at 94% of that box. Zain's content box at `size-adjust:
   112%` is 1.859em, so a 1em box has negative half-leading and the descenders of
   ر and ع run past a window cut at 94%. That is exactly the failure this sheet
   documents at the roll-up twin: the Golden Master's `راسلني` rendered as
   `راسلنى`, a different letter and a misspelling of the owner's own words. It
   was fixed on `/ar/` and has been live on `/` and `/de/` ever since, on the one
   Arabic word those documents contain.

   AND THE TWIN MOVES WITH THEM, because the invariant is the one stated above:
   TWIN BELOW WINDOW, WINDOW BELOW INK. 2em, the same quantity the Arabic
   document uses, and for the same arithmetic — the window at 114% of a 1.55em
   box reaches 1.767em, and the twin's ink begins at 1.845em. It is also, since
   P11-07, the distance `animation/text-hover.js` rolls by, because that module
   now reads the twin's own computed offset instead of assuming a line box.

   SCOPED TO THE SWITCHER'S LABEL rather than to `[lang='ar']` at large: these
   are the metrics of a small uppercase label in a menu, not of Arabic in
   general, and the only Arabic run on a Latin document is this one. */
.nav-lang-links :is(.btn-text, .text-nav-link):lang(ar) {
  --ar-label-lh: 1.55;
  --text-offset: 2em;

  line-height: var(--ar-label-lh);
  letter-spacing: 0;
}

.nav-lang-links .text-clip-w:lang(ar) {
  clip-path: polygon(0 -2%, 0 114%, 100% 114%, 100% -2%);
}
}
