/* ==========================================================================
   TOKENS — HigherEd Collective Summit
   Every value in the system. Nothing outside this file is a legal value.
   Token names describe ROLE, not appearance.
   Source: Documents/design-rules.md · CLAUDE.md §3–4
   ========================================================================== */

:root {

  /* --------------------------------------------------------------------
     COLOUR — GROUND
     -------------------------------------------------------------------- */
  --color-bg-base:            #000000;  /* page background. always. */
  --color-surface-raised:     #0A0A0A;  /* raised ground. ONE use page-wide (footer). */
  --color-surface-card:       #121212;  /* cards, modal panel, inputs' parent */
  --color-surface-hover:      #1A1A1A;  /* hover / raised state */

  /* A FOURTH GREY — R7g, by direction, for the §4 answer panel.
     It sits BETWEEN surface-card and surface-hover, and the ladder is now
     tighter than it can usefully be:

       surface-raised  #0A0A0A   luminance 0.0030
       surface-card    #121212   luminance 0.0060
       surface-panel   #171717   luminance 0.0086   ← this
       surface-hover   #1A1A1A   luminance 0.0103

     #171717 against #1A1A1A is 1.03:1 — three values per channel, and a
     contrast ratio a hair above pure identity. On most displays, at most
     brightness settings, THE TWO ARE THE SAME COLOUR. If this panel is ever
     hovered, or sits against a surface-hover element, the boundary will not
     read at all. --color-surface-hover would have been indistinguishable and
     cost no new token; it is kept separate only because the value was
     specified exactly.

     Everything on it still measures fine: white 17.9:1, body 8.6:1,
     muted 5.2:1, and the violet badge 3.1:1 as a non-text fill. */
  --color-surface-panel:      #171717;  /* §4 — the section box (R8h). It held the ✗ rows
                                          from R7w to R8h; those are black now. */

  /* ⚠ NO CONSUMER as of R8y. It existed for one thing — R6k's light duplicate
     of the §7 plate — and that section was removed by direction. R7x briefly
     gave §4's answer panel the same white and R7z moved it back off again.

     WHITE IS A TEXT COLOUR AGAIN AND NOTHING ELSE, which is what this system
     was before R6k: see --color-text-heading. Kept, not deleted, only because
     a light section is a decision that has been made and unmade three times
     in this build; if the page stays dark, delete it.

     White has never been a surface in this system; it exists only as
     --color-text-heading. It is a SEPARATE token rather than a reuse of that
     one, because a text colour standing in as a page-wide ground is how a
     palette stops meaning anything — and because the two must be able to move
     independently.

     CONSEQUENCE, MEASURED: this is now the brightest ground on the page,
     brighter than §11's 90%-white cells, and the fixed nav crosses it. The
     glass bar's white logo measures 3.35:1 over it at the current 0.45 scrim
     — legal for a graphic (3:1) and the thinnest margin anywhere on the page.
     If the header ever carries text again, or the scrim opens further, this
     section is the ground to measure against. */
  --color-surface-invert:     #FFFFFF;  /* no consumer — R8y */

  /* THE LIGHT VALUE — R7o, by direction, originally as §3's ground. Both
     grounds that used it have since gone back to black (§3 at R8f, §4 at
     R8g), so ⚠ it has exactly ONE consumer left: §4's answer panel. It is no
     longer a "ground" in any section; #FFFFFF stays reserved to §2a.

     ⚠ THE COMMENT ABOVE SAID "THE ONE WHITE GROUND". It is no longer one.
     Two light grounds now sit three sections apart (built 3 and built 6), and
     the pair is close enough that the second reads as a reprise of the first
     rather than as its own event. That is a composition question, recorded
     here because this token is where it becomes visible.

     #F5F5F5 rather than #FFFFFF, and the distinction is doing work: it keeps
     §2a's plate the brightest thing on the page, so the two light sections
     are not literally the same surface.

     MEASURED against the fixed nav, which crosses this the way it crosses
     §2a: white logo over the 0.45 scrim = 3.59:1, better than §2a's 3.36:1
     because the ground is darker. Both clear the 3:1 a graphic needs. */
  --color-surface-light:      #F5F5F5;  /* §4 answer panel only — §4's ground went black at R8g */

  /* THE PLACEHOLDER BLOCK ON A LIGHT GROUND — R7u, re-derived at R8r.

     THE RULE IT ENCODES IS A STEP, NOT A VALUE: a placeholder sits 1.27:1 off
     whatever it sits on. That figure came from mirroring the dark side, where
     a placeholder is #1A1A1A on #000000 — a 1.21:1 lift, enough to say "a box
     is here" without the box competing with the copy. Below roughly 1.2 the
     block stops holding the mass the image will occupy, which is the entire
     job of a placeholder (§5 — a filled box reads as mass, an outline reads
     as absence).

     THE GROUND UNDER IT CHANGED, SO THE VALUE HAD TO. R7u set #DBDBDB for
     §3's montage against the #F5F5F5 panel: 1.27:1, correct. §3 went back to
     black at R8f, leaving §4's face circles as the only consumer — and those
     sit on the #E5E5E5 tile, not the panel. Against the tile #DBDBDB is only
     1.10:1, which is why the circles read as barely there.

     #CCCCCC restores the step exactly: 1.27:1 against #E5E5E5, the same
     figure #DBDBDB gave against #F5F5F5. The rule is unchanged; only the
     ground moved. */
  --color-surface-light-block: #CCCCCC;  /* §4 face circles */


  /* THE MIDDLE RUNG — R8a. §4's brief tile.

     The two light values above now form a ladder, and it mirrors the dark
     one almost exactly:

       dark    #000000 -> #121212  1.12:1   #121212 -> #1A1A1A  1.08:1
       light   #F5F5F5 -> #E5E5E5  1.16:1   #E5E5E5 -> #DBDBDB  1.10:1

     §9a's rule holds on both sides: ground -> tile -> element, three steps,
     and no deeper. */
  --color-surface-light-tile:  #E5E5E5;  /* §4 only */

  /* TYPE ON THE LIGHT GROUND — R7o.

     HEADING is #000000 by direction. Note it is NOT --color-on-plate-invert
     (#0F0F0F), which is what the §7 and §2a plates use; this section was
     specified as pure black and takes its own token rather than bending that
     one.

     BODY AND THE HAIRLINE COULD NOT BE LEFT AS THEY WERE. The instruction was
     "everything else remains the same", which holds for the violet, the dark
     blocks and the layout — but #B4B4B4 body copy on this ground measures
     1.90:1 and rgba(255,255,255,0.20) hairlines are simply invisible on it.
     Leaving them "the same" would not preserve the design; it would delete
     two of its elements.

     The hairline is mirrored to hold its EXISTING RELATIONSHIP rather than
     its existing hex: rgba(255,255,255,.20) on black is 1.66:1 against its
     ground, rgba(0,0,0,.20) here is 1.60:1.

     BODY IS #5F5F5F BY DIRECTION — R7s, arrived at in two moves: derived as
     #3B3B3B (R7o), set to #2F2F2F (R7q), then to #5F5F5F.

     ⚠ IT NO LONGER MIRRORS --color-text-body. At 5.86:1 it lands on the
     MUTED role's mirror instead — --color-text-muted #8A8A8A is 6.08:1 on
     black, whose light-ground equivalent is #5C5C5C at 6.13:1, three units
     away. So this token is named for body and toned like muted. The name is
     kept because it is what the section's prose uses; the discrepancy is
     recorded here rather than papered over by renaming a token mid-build.

     ✅ IT RECOVERS THE HIERARCHY R7q SPENT. Heading-to-body separation:

       #000000 vs #2F2F2F   1.57:1   (R7q — flatter than anywhere on the page)
       #000000 vs #3B3B3B   1.87:1   (the derived value)
       #000000 vs #5F5F5F   3.29:1   (now)
       #FFFFFF vs #B4B4B4   2.07:1   (the dark ground, for comparison)

     The light ground now separates heading from body MORE than the dark one
     does. Still comfortably past AA at 5.86:1 against the 4.5 normal text
     needs. */
  --color-text-heading-on-light: #000000;             /* 19.26:1 */
  --color-text-body-on-light:    #5F5F5F;             /*  5.86:1 — R7s, by direction */
  /* ⚠ NO CONSUMER as of R8f. Its only job was §3's item separators, and §3
     is dark again. Kept because §4 is still light and any hairline added
     there needs this rather than --color-border-strong, which is invisible on
     a light ground. Delete it if §4 goes dark too. */

  /* --------------------------------------------------------------------
     COLOUR — BORDER
     -------------------------------------------------------------------- */
  --color-border-default:     rgba(255, 255, 255, 0.10);
  --color-border-strong:      rgba(255, 255, 255, 0.20);
  --color-border-interactive: rgba(255, 255, 255, 0.16); /* secondary button at rest */

  /* THE ONE GOLD BORDER — R6w. Sand at 0.20, for the Hero eyebrow's frame.
     Every other border in the system is white at an alpha; this is the same
     idea in the emphasis hue, which is why it lives here beside them rather
     than among the accents. It is a BORDER token: never text, never a fill.

     WHY AN ALPHA AND NOT ONE OF THE TWO GOLDS. Both are too present as a
     twelve-segment outline — measured on the #1A1A1A eyebrow fill:
       #F2D98A solid                12.5:1   R6t, too bright
       #BE9D4A solid                 6.7:1   R6v, still an outline shouting
       sand @ 0.20                   1.7:1   this — a gold edge
       white @ 0.10 (border-default) 1.3:1   the quiet reference
     The ladder is there if it needs nudging: 0.16 → 1.50, 0.24 → 1.90,
     0.30 → 2.28. Past roughly 0.30 it stops being light.

     It is decorative chrome and carries no meaning, so it answers to no
     contrast minimum — the label does all the reading. */
  --color-border-emphasis:    rgba(242, 217, 138, 0.20);

  /* --------------------------------------------------------------------
     COLOUR — TEXT
     Ratios measured against --color-bg-base.
     -------------------------------------------------------------------- */
  --color-text-heading:       #FFFFFF;  /* 21.0:1 */
  --color-text-body:          #B4B4B4;  /* 10.1:1 */
  --color-text-muted:         #8A8A8A;  /*  6.1:1 */

  /* --------------------------------------------------------------------
     COLOUR — ACCENT (role-locked; see CLAUDE.md §3)
     -------------------------------------------------------------------- */
  /* R2 rebalance. One dominant accent per section; the CTA's orange exempt. */
  --color-accent-identity:      #865BFF; /* identity, logo, links, G1. NEVER a control. 4.95:1 */

  /* A SECOND VIOLET — the HERO CTA GROUND. R5p.
     Its own token, not a variant of the one above: they are visibly different
     weights of violet and must never be interchangeable in code.

     ITS ROLE INVERTED BETWEEN R5j AND R5q. It arrived as the Hero H1's colour
     — text, large-text-only, explicitly never a control. It is now the exact
     opposite: a control surface, and NOT a text colour. The headline went
     back to white at R5q. The old caveat is retired; the live constraints are
     the ones below.

     ON THIS GROUND THE LABEL IS WHITE, and that is forced, not chosen:
       #6146ED + #FFFFFF = 5.82:1  ✓ AA normal text
       #6146ED + #0F0F0F = 3.29:1  ✗ the near-black label cannot come here
       #6146ED on #000000 = 3.61:1 ✓ past the 3:1 a control boundary needs
     So "the label is never white on violet" holds for #865BFF ONLY. On this
     violet it reverses. Never swap one for the other. See design-rules R5p.

     AS TEXT it is 3.61:1 — AA for LARGE TEXT (3:1), a FAILURE for normal text
     (4.5:1). It is used on type in exactly ONE place: §3's closing line
     (R6q), which is H3 — 26 / 30 / 34px, never below the 24px large-text
     floor at any breakpoint. It was also tried on the Hero's 20px lead at
     R5y and reverted at R5z for precisely that reason.
     THE SIZE IS THE PERMISSION. Below 24px this colour is illegal on text.

     It also draws §3's pull rule (R6s) — 3.61:1 clears the 3:1 a non-text UI
     boundary needs. */
  --color-accent-identity-deep: #6146ED; /* CTA ground · §3 close + rule */
  --color-accent-action:        #FF5B1A; /* primary CTA · scarcity · the ✗ side · error. 6.8:1 */
  --color-accent-confirm:       #3BFF9A; /* the ✓ side · confirm markers · focus ring. 16.0:1 */
  --color-accent-emphasis:      #F2D98A; /* sand — numerals, inline emphasis, phone. 15.1:1 */
  --color-accent-emphasis-dim:  #BE9D4A; /* gold — numerals, inline emphasis, dimmer. 8.1:1 */

  /* --------------------------------------------------------------------
     CONTROL SURFACE — the ground of the page's primary action
     Violet, by direction. Read the ratios before changing anything here:

       #865BFF + #0F0F0F  =  4.52:1   passes AA for normal text by 0.02
       #865BFF + #FFFFFF  =  4.24:1   FAILS. The label is NEVER white.
       #865BFF on #000000 =  4.95:1   fine as a stroke (needs 3:1)
       #865BFF on #FF537E =  1.37:1   the §7 plate button CANNOT take violet

     There is no headroom. Darkening the violet makes it worse, not better —
     a darker ground under a dark label. Any change to either value has to be
     re-measured in both directions before it ships.
     -------------------------------------------------------------------- */
  --color-control-primary: #865BFF;
  --color-control-stroke:  #865BFF;

  /* Labels that sit ON an accent surface */
  --color-on-action:            #0F0F0F; /* 6.2:1 on --color-accent-action */
  --color-on-plate-invert:      #0F0F0F; /* section-7 plate button ground */

  /* THE GLASS MATERIAL — R6d, opened at R6g, generalised at R6o.
     Named for the material, not the nav: the §3 play button asks for "the
     same glass as the header", so the two share one definition rather than
     two that drift. Changing it changes both, which is the intent.
     Renamed from --color-surface-nav-scrolled: the bar carries this at all
     times now (R6c), so "scrolled" described a state it no longer has.

     0.72 → 0.60 (R6d) → 0.45 (R6g). More of the page shows through and the
     blur behind it reads as blur rather than as a slightly dirty panel.

     WHAT THE FLOOR IS MEASURED AGAINST CHANGED, and that is why it could move.
     At R6d the binding element was the SAND PHONE LINK over §11 — a full-bleed
     field of cells filled at 90% white, the brightest ground the bar crosses.
     Sand failed first and capped the scrim at 0.60. R6e deleted that element.
     What is left over the glass is the logo (a graphic — 3:1, not 4.5:1) and
     the CTA, which carries its own violet ground and is unaffected.

       alpha   white over #E6E6E6
       0.60        6.69
       0.50        4.74
       0.45        4.03   ← still past 4.5:1 for TEXT, let alone the 3:1 a
       0.40        3.45      graphic needs. Chosen with that headroom on purpose.

     THE LOGO IS PART WHITE, PART NEAR-BLACK (a violet-to-black gradient mark
     with a white stroke). Only the white carries the shape against a dark
     ground, which is why white is the value measured here. The dark parts gain
     contrast over a light backdrop, not lose it.

     Re-measure against the LOGO over §11 before opening this further, not
     against the black the Hero happens to sit on. */
  --color-surface-glass: rgba(0, 0, 0, 0.45);

  /* R14q — A DEEPER GLASS, for a glass disc that sits ON A PHOTOGRAPH.

     0.45 was set for the navigation, where what is behind it is the page's
     own dark ground. The play control borrows the same material but sits over
     a video still, and that is a different problem: MEASURED against the
     brightest fifth of the frame behind §6's first card, a violet glyph on
     0.45 glass comes to 1.44:1, against the 3:1 a non-text control needs.

     0.82 IS THE MEASURED FLOOR PLUS HEADROOM, and it has moved TWICE — which
     is the point of the warning below. 0.75 at R14q, computed against a card
     scrim of 0.30 under the disc. 0.80 at R14t, when that scrim lightened to
     0.11. 0.82 now, at R14u: the ≥1200 band starts at 52%, so the scrim under
     the disc is ZERO and the glass is the only thing between the glyph and
     the frame. Measured on the brightest fifth behind each disc, glass alone:
     0.80 gives Philip 2.98:1 — a hair under — and 0.82 gives 3.21:1.

     ⚠ THE TWO VALUES ARE COUPLED AND THE COUPLING IS NOT OBVIOUS. Every time
     the video card's scrim lightens, this has to darken to compensate, because
     both sit between the glyph and the same photograph. Change one and
     re-measure the other — the numbers are in members.css.

     IT IS STILL GLASS. backdrop-filter still blurs and saturates what is
     behind it; only the tint is heavier. A dark blurred disc over footage is
     what every video player uses, and for the same reason. */
  --color-surface-glass-deep: rgba(0, 0, 0, 0.82);
  --color-scrim-modal:          rgba(0, 0, 0, 0.70);

  /* RETIRED at R5i — --color-overlay-hover, rgba(255,255,255,0.14).
     A hover tint that lightened an accent without introducing a new hue. Its
     only consumers were the button's two blocks, and buttons no longer change
     colour on hover, so it is removed rather than left standing: a token
     nothing spends is debt, not vocabulary. Restoring it means restoring the
     two ::after overlays in button.css with it. */

  /* Focus */
  --color-focus-ring:           var(--color-accent-confirm);

  /* --------------------------------------------------------------------
     GRADIENT — all five, all in active use (design-rules §6)
     Stops appearing only inside a gradient — #A45DFF #FFE35A #37DFFF
     #35FFE5 #B05EFF #201020 — are gradient-only. Never standalone colours.
     Assignment: G1 hero fill + hosted-by glow · G2 why-now · G3 who's-in-
     the-room + final-CTA shape · G4 hero shape + speakers · G5 final-CTA fill.
     -------------------------------------------------------------------- */
  /* ⚠ GRADIENT SURVIVES IN ONE PLACE ONLY — the portrait ground.

     CORRECTION TO THE R9h NOTE THAT STOOD HERE: it claimed all five had no
     consumer. That was wrong. R9h removed every BLEND SHAPE and the one flat
     fill, which is what left the .blend and .gradient-fill components dead —
     but §11a's portrait ground is a separate, capped exemption and it is
     live. G1 backs the wall portraits and G4 backed the speakers' until R10e.

     Still with no consumer: G2, G3, G5, and both components. Kept rather than
     deleted — that is a whole visual system, and removing it is a bigger
     decision than any single section. */
  --gradient-g1: linear-gradient(135deg, #865BFF 0%, #A45DFF 50%, #FF537E 100%);

  /* THE SPEAKERS' PORTRAIT GROUND — R10e, by direction, from a supplied
     swatch: #0E0A1E to #6146ED.

     ⚠⚠ THIS IS A SIXTH GRADIENT, and §13 prohibits "any gradient other than
     the five in §3" outright. It is also the first to be built from ROLE
     tokens rather than gradient-only stops: #6146ED is --color-accent-
     identity-deep, the page's own violet, where every other gradient is made
     of values that exist nowhere else. #0E0A1E is new and is gradient-only.

     ⚠ G4 LOSES ITS ONLY CONSUMER. It was the speakers' ground and nothing
     else; G2, G3 and G5 were already unused. G1 still backs the wall
     portraits. The palette is now one live gradient plus this one.

     135deg, matching the family's convention, so the dark stop sits top-left
     and the violet runs to bottom-right.

     ⚠ MEASURED, AND THE DARK END NEARLY DISAPPEARS: #0E0A1E against the
     speaker card's #171717 is 1.08:1. The staircase corners on that side of
     the frame will read as almost nothing — the frame's SHAPE is carried by
     the violet end alone. G4's darkest stop was 4.23:1 against the same card.
     That is the cost of a dark gradient on a dark panel, and it is the thing
     to look at rather than a number to fix. */
  --gradient-portrait: linear-gradient(135deg, #0E0A1E 0%, #6146ED 100%);

  /* R11e — THE §11 GLOW. ⚠ A SEVENTH GRADIENT, against §9's five-only rule
     and the second exception after --gradient-portrait. Added by direction:
     "a subtle dark purple glow (radial gradient) from behind the cards
     strip".

     RADIAL, AND THE ONLY RADIAL ON THE PAGE. The other six are 135deg
     linears. A linear cannot do this job — a glow has a source and falls off
     in every direction from it, and a linear has a direction instead of a
     centre.

     THE HEX IS NOT REPEATED. rgba() would mean writing #6146ED's channels a
     third time, and a value written three times is a value that will
     disagree with itself. color-mix keeps the stops tied to the token they
     come from, so re-tinting the glow is a one-token edit.

     ⚠ color-mix IS THIS STYLESHEET'S FIRST. Where it is unsupported the whole
     background-image is invalid and the glow simply does not render — which
     is the correct failure for decoration, but it is a new floor for the
     page (Safari 16.2 / Chrome 111 / Firefox 113) and it is now load-bearing
     for nothing else. Keep it that way.

     THREE STOPS, NOT TWO. A two-stop radial to transparent has a visible
     edge where the falloff ends; the middle stop is what makes it read as
     light rather than as a soft-edged shape.

     R11f — RADIUS 62/64% -> 80/82%, by direction. The stops are untouched:
     they are proportions OF the radius, so growing the ellipse carries the
     falloff out with it and the glow keeps its shape while getting larger.
     Moving the stops instead would have changed how hard it falls off, which
     is a different adjustment wearing the same name.

     ⚠ THE PERCENTAGES ARE OF THE ELEMENT, AND THE ELEMENT OVERHANGS. The box
     is 1.3x the container wide (inset -15% each side), so an 80% radius is
     1.04 containers and the transparent stop lands at about 1.5 containers —
     comfortably off-screen. That headroom is what keeps the falloff's outer
     edge from ever becoming a visible ring, and it is spent, not free: much
     past this the edge starts arriving inside the frame and the inset has to
     grow with the radius. */
  --gradient-glow-violet: radial-gradient(
    ellipse 80% 82% at 50% 50%,
    color-mix(in srgb, var(--color-accent-identity-deep) 30%, transparent) 0%,
    color-mix(in srgb, var(--color-accent-identity-deep) 12%, transparent) 40%,
    color-mix(in srgb, var(--color-accent-identity-deep)  0%, transparent) 72%
  );

  /* R23b — §12's VIOLET PLATE. ⚠ AN EIGHTH GRADIENT, against the five-only
     rule, and the third exception after --gradient-portrait and
     --gradient-glow-violet. Added by direction: a full-width, edge-to-edge
     violet ground for the Final CTA — "a refined version of #6146ED into
     deeper violet/indigo, with subtle radial lighting rather than a simple
     two-colour gradient." Amended in design-rules.md §6 first.

     ⚠⚠ NO NEW COLOUR VALUE. Four layers, every stop mixed from three tokens
     that already exist. The supplied CSS carried #8269FF, #5B42DF, #4D36C4
     and #4B34BE as literals; none are used. Its bright stop rgba(130,105,255)
     is within three points of --color-accent-identity (#865BFF) on every
     channel, so the page's own second violet does that job and the plate
     re-tints from tokens instead of from four hexes that answer to nothing.

     ⚠⚠ THE ARITHMETIC TRAP, and the reason layer 1 is capped at 30%:
     WHITE ON #865BFF IS 4.24:1 AND IS PROHIBITED PAGE-WIDE. It is legal here
     only because the stop is a wash that never resolves to the flat colour —
     30% of #865BFF over the darkened ground composites to #6C4CE8 under the
     heading, and white on that is 5.47:1. Raise this radial toward opaque and
     the page's single hardest prohibition is rendered full-bleed behind a
     headline. Re-measure before touching it; do not eyeball it.

     ⚠⚠ AND THE GOLD BINDS BEFORE THE WHITE DOES. #F2D98A on the XXX measures
     4.90:1 at 1440 and 4.63:1 at 390 — but on FLAT #6146ED it is 4.18:1, i.e.
     the brand violet at full strength already fails the body floor for the
     gold. That is layer 4's job, not layer 1's: dimming the glow was tried
     first and stalled at 4.36 on mobile. Measure at the XXX at 390.

     FOUR LAYERS, each with one job — painted top to bottom:
       1  the content glow. Off-centre at 20% 32%, where the heading, the
          numeral and the button actually sit, so the light is behind the
          content rather than behind the middle of the box.
       2  the vignette. Centred on the same point but far larger than the
          box, so it darkens the far edges — including the top-right corner
          the content never reaches — without ever showing its own edge.
       3  the bottom settle. Flat across the width, because a radial cannot
          darken a bottom EDGE evenly; it would sag in the middle.
       4  the ground. 135deg, the family convention, #6146ED to 72% of it.
          ⚠ IT RAMPS EARLY AND ON PURPOSE — 88% by 42%, not a lazy fade to
          the far corner. That is the contrast fix for the gold, not a
          styling preference. See the trap note above.

     THREE STOPS ON EVERY RADIAL, never two — a two-stop radial to transparent
     has a visible edge where the falloff ends. Same finding as
     --gradient-glow-violet, and the same reason the middle stop is there.

     ⚠ IT BANDS WITHOUT GRAIN, AND THAT WAS MEASURED, NOT ASSUMED. Layer 4
     moves #6146ED's blue channel from 237 to 185 across the plate — one
     8-bit step every ~20px at desktop width, on a flat unbroken surface,
     which is the textbook case for banding. Rendered both ways and counted:

       without .u-grain   longest flat run 60px   50 runs over 12px
       with    .u-grain   longest flat run 11px    0 runs over 12px

     .u-grain is not decoration here — it is the dither, and removing it
     brings fifty bands back. */
  --gradient-final-cta:
    radial-gradient(
      ellipse 64% 78% at 20% 32%,
      color-mix(in srgb, var(--color-accent-identity) 30%, transparent) 0%,
      color-mix(in srgb, var(--color-accent-identity) 12%, transparent) 42%,
      color-mix(in srgb, var(--color-accent-identity)  0%, transparent) 74%
    ),
    radial-gradient(
      ellipse 118% 128% at 26% 28%,
      color-mix(in srgb, var(--color-bg-base)  0%, transparent) 38%,
      color-mix(in srgb, var(--color-bg-base) 14%, transparent) 72%,
      color-mix(in srgb, var(--color-bg-base) 30%, transparent) 100%
    ),
    linear-gradient(
      to bottom,
      color-mix(in srgb, var(--color-bg-base)  0%, transparent) 42%,
      color-mix(in srgb, var(--color-bg-base) 18%, transparent) 100%
    ),
    linear-gradient(
      135deg,
      var(--color-accent-identity-deep) 0%,
      color-mix(in srgb, var(--color-accent-identity-deep) 88%, var(--color-bg-base)) 42%,
      color-mix(in srgb, var(--color-accent-identity-deep) 72%, var(--color-bg-base)) 100%
    );

  /* R25g — THE STICKY CTA's SHEEN, by direction: "every 3 seconds the sticky
     'Request For Your Seat' CTA button will have a shimmering effect."

     ⚠⚠ A NEW GRADIENT, AGAINST §3's LIST. It is declared here rather than
     written into button.css because §9 prohibits "any gradient not in §3" —
     a literal in a component would be the breach the rule names. The list is
     now nine declared, four rendering. design-rules.md §6 has NOT been
     amended for it; rule 6 says it should be, and that is outstanding.

     NO NEW COLOUR: white, from --color-text-heading, at two alphas. It is a
     highlight passing over a violet control, so it has to be white — the
     accent it sits on is the ground.

     ⚠⚠ ANGLED, BY DIRECTION — R25i: "make the sweep gradient for the shimmer
     slightly angled and not straight." The note here previously said the
     opposite, and said it for a reason: §10a keeps this page's geometry
     rectangular and axis-aligned — the cell, the pixel glyph, the ticket
     notch and the eyebrow frame are all square to the page. A leaning band
     is the first diagonal on it. The lean lives in --shimmer-skew and is
     applied as a transform, so the GRADIENT itself is still a straight
     90deg ramp; nothing about this token changed. */
  --gradient-shimmer: linear-gradient(
    90deg,
    color-mix(in srgb, var(--color-text-heading)  0%, transparent) 0%,
    color-mix(in srgb, var(--color-text-heading) 28%, transparent) 50%,
    color-mix(in srgb, var(--color-text-heading)  0%, transparent) 100%
  );

  /* Grain accompanies gradient ONLY. */
  --texture-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  --texture-grain-opacity: 0.06;

  /* --------------------------------------------------------------------
     TYPE — family & weight
     Mundial via Adobe Fonts kit whs6yen. Domain-locked.
     FOUR weights, no others. With one family, weight is one of only three
     hierarchy tools — the previous three-weight system had it switched off.
     -------------------------------------------------------------------- */
  --font-family-base: "mundial", "Mundial", ui-sans-serif, system-ui,
                      -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;

  /* HEADINGS — Loos Extrawide Medium, from the same kit.

     READ THIS BEFORE CHANGING THE WEIGHT. Kit whs6yen publishes
     loos-extrawide at 400 and 700 ONLY — there is no 500 in it. Verified by
     fetching the kit, not assumed.

     --font-weight-heading is 500 anyway, and that is deliberate:
       · CSS weight matching for a desired 500 checks 500, then DESCENDS to
         400 before it ever looks above. So 500 renders the published 400
         face with NO synthesis. Nothing is smeared.
       · If Medium is later added to the kit, this picks it up with no code
         change. A hard-coded 400 would not.

     If the headings must be heavier, 700 is the only other real weight here.
     Anything between 500 and 700 synthesises.

     NUMERALS STAY IN MUNDIAL. Counters must not reflow while counting (§18),
     which needs tabular figures; Mundial's are confirmed, Loos's are not.
     Chrome stays Mundial too — eyebrows, labels, buttons, form text are
     controls and metadata, not headings. */
  --font-family-heading: "loos-extrawide", "mundial", "Mundial", ui-sans-serif,
                         system-ui, -apple-system, "Segoe UI", Helvetica,
                         Arial, sans-serif;

  --font-weight-heading: 500;

  --font-weight-light:    300; /* ⚠ The 22px floor is RETIRED (R9c, after three overrides).
                                 What governs now: contrast >= 5:1 measured per use, since the
                                 ratio carries alone; never in text-muted; and on dark grounds
                                 halation checked BY EYE — it cannot be derived from values. */
  --font-weight-regular:  400; /* body default, 16–20px */
  --font-weight-semibold: 600; /* H2 · H3 · H4 · eyebrows · labels · secondary button */
  --font-weight-bold:     700; /* display / H1 · numerals · primary CTA label */

  /* --------------------------------------------------------------------
     TYPE — scale (mobile first; stepped at 768 and 992)
     Headings compress 50% desktop → mobile; body compresses 6%.
     -------------------------------------------------------------------- */
  /* Display and H1 are SEPARATE from ≥992. The scale's top row reads
     "Display / H1 — 76px", and that still holds for display type: the §7
     plate title and the display numerals. The H1 alone steps down to 52px:
     it is four lines of a long headline, and at 76px it pushed the Hero's
     CTA below the fold.

     R5b: 52 → 48. Loos Extrawide is the widest cut in the family, so the
     eight-word headline sets long even in a 5/12 column.

     ⚠ 48 IS ALSO H2. The H1 and H2 are now the same face, size and weight,
     so the page's headline is distinguished by POSITION and MEASURE only —
     it is the one heading at the top of the page in its own column. That is
     workable but it is not a type hierarchy, and it should not be read as
     the intended end state. Two ways out, both one value:
       · drop H2 to 42px and restore a real step, or
       · widen the Hero column back to 7/12 and take H1 back up.
     Do NOT take H1 below 48 — under H2 the hierarchy inverts.

     Mobile and tablet are unchanged; they were never the problem. */
  --font-size-display:  38px;
  --font-size-h1:       38px;
  --font-size-h2:       30px;
  --font-size-h3:       26px;
  --font-size-h4:       22px;
  --font-size-lead:     18px;
  --font-size-body-lg:  17px;
  --font-size-body:     17px;
  --font-size-small:    16px;
  --font-size-meta:     14px;

  /* --------------------------------------------------------------------
     TYPE — line height
     -------------------------------------------------------------------- */
  /* 1.02 → 1.08 at R5d. design-rules specified 100–104% for display, and
     that range was written for a 76px line of four words or fewer — where
     near-solid leading is what makes a short display line read as one shape.

     The H1 is no longer that. It is 48px of Loos Extrawide, eight words, and
     it wraps to five or six lines in the Hero's narrow column. At 102% the
     ascenders of one line nearly touch the descenders of the line above, and
     a wide face makes that collision more visible, not less. 108% opens it
     without loosening it into body leading.

     The single-line consumers — the display numerals in §5, §8, §12, §13 —
     are unaffected in appearance; only their line box grows, and the counter
     box is sized by width, so nothing reflows. */
  --line-height-display: 1.08;
  --line-height-heading: 1.12;
  --line-height-lead:    1.30;
  --line-height-body:    1.48;
  --line-height-flat:    1;

  /* --------------------------------------------------------------------
     TYPE — tracking
     -------------------------------------------------------------------- */
  --letter-spacing-display: -0.025em;
  --letter-spacing-heading: -0.02em;
  --letter-spacing-lead:    -0.005em;
  --letter-spacing-body:    0em;
  --letter-spacing-caps:     0.06em;  /* all uppercase carries positive tracking */
  --letter-spacing-caps-wide: 0.08em;

  /* --------------------------------------------------------------------
     SPACE — the only permitted spacing values
     -------------------------------------------------------------------- */
  --space-2xs:  8px;
  --space-xs:  12px;
  --space-sm:  16px;
  --space-md:  24px;
  --space-lg:  32px;
  --space-xl:  40px;
  --space-2xl: 48px;
  --space-3xl: 64px;
  --space-4xl: 80px;
  --space-5xl: 120px;
  --space-6xl: 160px;

  /* SPACE — fixed relationships (design-rules §4) */
  --gap-heading-to-content:   var(--space-lg);  /* 32 mobile → 48 desktop */
  --gap-heading-to-paragraph: var(--space-sm);  /* 16, all breakpoints */
  --gap-paragraph-to-cta:     var(--space-lg);  /* 32, all breakpoints */
  /* R19p — 32 -> 24, by direction: "reduce the space between the eyebrow and
     the headline here and everywhere. and keep the spacing uniform across the
     page." 24 is one step down §4's scale and is the value the Hero's eyebrow
     had been using alone since the foundation build, so the page converges on
     a number it already contained rather than a new one.

     ⚠ THIS TOKEN IS THE EYEBROW-TO-HEADING INTERVAL PAGE-WIDE. Four rules read
     it — .section__title's flex gap (§5, §6, §10, §11, §14), .venue__title,
     .plate__eyebrow and .hero__eyebrow — and after R19p there is no fifth
     value anywhere. Change it here, not in a section. */
  --gap-title-group:          var(--space-md);  /* 24, all breakpoints */
  --gap-card-padding:         var(--space-md);  /* 24 mobile → 32 desktop */
  --gap-grid:                 var(--space-sm);  /* 16 mobile → 24 desktop */

  /* --------------------------------------------------------------------
     LAYOUT — container & section
     -------------------------------------------------------------------- */
  --container-max:            1280px;
  --measure-prose:             720px;
  --measure-prose-wide:        820px;
  --container-padding-inline: var(--space-md);  /* 24 mobile → 32 → 48 */

  --section-padding-block:       var(--space-3xl); /* 64 mobile → 80 → 120 */

  /* --------------------------------------------------------------------
     RADIUS — five values, nothing else
     -------------------------------------------------------------------- */
  --radius-none:    0;
  /* SHARP — R6x, by direction. 8px → 0 for every button, box, card, image,
     input, panel and media block on the page: 24 consumers, all of them
     following this one value.

     THE PAGE IS NOW SQUARE except for two shapes that are not "curved
     corners" and were deliberately left:
       --radius-module 32px  the blend shapes. Blurred gradient atmosphere,
                             not a box; its corners are invisible under a
                             40px blur.
       --radius-avatar 50%   the §3 play control. A circle has no corners to
                             sharpen — squaring it would make it a different
                             component, not a sharper one.
     Say the word on either and they follow.

     COUPLING PAID AT THE SAME TIME: the secondary button's outline is a
     stroked SVG path that hard-codes its geometry, so it was redrawn square.
     See button.css. */
  --radius-control: 0;      /* cards, buttons, images, inputs */
  --radius-avatar:  50%;

  /* --------------------------------------------------------------------
     CONTROL SIZING
     -------------------------------------------------------------------- */
  /* 56 → 52 at R5h, by direction. design-rules §9 gives the button a range of
     52–56px, so this is the floor of the existing spec and not an amendment.
     Still 4px clear of the 48px tap-target minimum.

     The icon box moves with it and stays SQUARE. It is a square by
     construction, not a coincidence — the notch is cut into a square block,
     and a 56-wide box on a 52-tall button would put a wide rectangle next to
     the label and read as a misfit rather than a smaller button.

     Buttons and inputs now share one height. That is a gain, not a
     collision: the modal's full-width CTA sits directly beneath the fields
     and they should agree. */
  --control-height:       52px;
  --control-height-input: 52px;
  --control-padding-inline: var(--space-md); /* 24 mobile → 28 desktop via override */
  /* ⚠ NO CONSUMER SINCE R15q. It sized §5's violet icon disc, which was
     removed by direction. Kept rather than deleted, following the same
     convention as --color-border-emphasis and gradients G2/G4: a token with a
     stated role and no user is cheaper to keep than to re-derive, and its
     value is the button's height, which is not a free choice. */
  --icon-box-size:        52px;

  /* THE SMALL ICON BOX — R7p. §3's three list marks and nothing else.

     52px is a CONTROL dimension: it is the button's height, and a 52px block
     beside 18px body copy reads as something you press. These marks are not
     pressable and never will be, so they take their own value rather than
     borrowing the control's.

     40px is the largest step down that still gives the 18px glyph room to
     breathe — an 11px surround on each side against the control's 17px. It is
     on the spacing scale (--space-xl) but is NOT spacing; like every other
     value in this block it is a box dimension and answers to no scale. */
  --icon-box-size-sm:     40px;  /* §3 list marks · §4 face circles (R8a) */

  /* THE SMALLEST MARK BOX — R8c. §4's ✓ badge and nothing else.

     24px at R8d, from 20 at R8c, by direction. The GLYPH sets the floor, not
     taste: the pixel tick is --icon-size-pixel-sm 15px, which §13 fixes as
     the smallest legal size in the set — 1:1 with the 15-unit grid, nothing
     below it. Measured against the circle it sits in:

       badge  radius  worst drawn corner  clearance
       24px    12.0        7.78px          +4.22px   ← here
       20px    10.0        7.78px          +2.22px
       16px     8.0        7.78px          +0.22px   effectively touching

     It is a box dimension, not spacing. It began life as --space-md, which
     was the wrong family for it; the value has come back round to 24 but it
     now lives where box dimensions live. */
  /* ⚠ NO CONSUMER as of R8p — the ✓ badge moved to --icon-box-size-badge to
     match the ✗ badge. Kept only because 24px is what both return to if the
     pair is ever taken back down; delete it if that stops being likely. */

  /* THE ✗ BADGE — R8n. 28px, larger than the ✓ badge's 24 by direction.

     IT ALSO CLOSES A FAMILY ERROR. This badge had been sized with --space-md
     since it was built: a spacing token doing a box dimension's job, which
     R8c called out and fixed only on the ✓ side. Both are in the right family
     now.

     28 AND NOT 32, because of what it does to the mark inside it. The cross
     draws 11.78px whatever the badge, since 18px is the largest legal icon
     size below 30 (§13 fixes the set at 15 · 18 · 30 and there is nothing
     between):

       badge 24  fill 49.1%      badge 28  fill 42.1%      ✓ badge is 45.8%

     28 keeps the ✗ mark reading at roughly the ✓ mark's weight. At 32 the
     fill drops to 36.8% and the cross starts to look lost in its own circle
     — the glyph cannot grow to meet it. */
  --icon-box-size-badge:  28px;  /* §4 ✗ and ✓ badges — matched at R8p */


  /* PLAY BUTTON — R6o. Its own value: it is not a text control and answers to
     neither --control-height nor the spacing scale. 80px is large enough to
     read as the video's primary affordance from across the section and still
     leaves the 30px glyph inside it at a legible proportion. */
  --control-play-size:    80px;
  --tap-target-min:       48px;

  --nav-height:           64px;   /* → 80 at ≥992 */

  /* R12z — 34/42 → 40/48, by direction. R6e's 28/34 → 34/42 came before it.
     Both values are on §4's spacing scale now, where 34 and 42 were not.

     THE BAR DOES NOT NEED TO GROW, and it was checked rather than assumed.
     .nav__inner has no block padding: its height is max(min-block-size,
     tallest child), and the tallest child is the 52px CTA, not the logo. At
     48 the logo is still 4px SHORTER than the control beside it, so the 80px
     bar keeps 16px of clearance top and bottom — the same air it had at 42.
     Mobile: a 40px logo in a 64px bar, where the 52px control already sets
     the height.

     THE CEILING IS 52px, not 80. Past the control's height the logo starts
     driving the bar instead of sitting inside it, and the two stop reading as
     a pair. If it needs to be bigger than that, the bar grows with it. */
  /* R17 — 40/48 -> 32/40 -> 36/44, in three steps, all by direction: down
     one scale value at each breakpoint, then each bar nudged back up.

     NEITHER 36 NOR 44 IS ON §4's SPACING SCALE, AND NEITHER NEEDS TO BE. This
     is a box dimension, like --nav-height (64/80), --icon-box-size (52) and
     --control-play-size (80); the note at the head of this block says so, and
     this token itself held 34 and 42 for several revisions. "Slightly bigger"
     has no answer on the scale at either end — the next values up are 40 and
     48, and both are where these came from and were called too large.

     BOTH BARS KEEP THEIR HEIGHT AND GAIN AIR. .nav__inner has no block
     padding, so its height is max(--nav-height, tallest child). Below 992: a
     36px logo in a 64px bar leaves 14px top and bottom, where 40 left 12.
     Desktop: 44 in 80 leaves 18 a side, where 48 left 16 — and at 44 the logo
     is still 8px SHORTER than the 52px CTA beside it, so the control goes on
     setting that bar's height rather than the logo.

     44 IS 8px UNDER THE CEILING. Past 52 the logo would start driving the bar
     instead of sitting inside it, and there is one step left (48) before that
     line is reached.

     THE 36 APPLIES EVERYWHERE BELOW 992, tablet included. That is the same
     line the logo-only header rule uses, so the two agree: below desktop the
     bar holds one thing and it is sized once.

     ⚠⚠ THE REAL PROBLEM IS THE ASSET, AND SHRINKING MAKES IT WORSE. The only
     logo that exists is the full horizontal lockup at 767x264 — three stacked
     lines: HECS, HIGHERED COLLECTIVE, SUMMIT - 2026. The third line is about
     15% of the artwork's height, so it renders at roughly

        5.5px  at the 36px lockup
        6.7px  at 44px (desktop)

     — under half the 14px this system's smallest legible type uses, at every
     nav height this token can legally take. It is not text a reader can read;
     it is texture. A NAV NEEDS ITS OWN COMPACT VARIANT — the HECS mark alone,
     or the mark plus one line — and that has been on the blocker list since
     the foundation build as "logo nav variant: only a 2.9:1 horizontal
     lockup exists". This token cannot solve it: every value that makes the
     lockup small enough for the bar makes its sub-lines smaller still.

     THE CEILING IS 52px. Past the control's height the logo drives the bar
     instead of sitting inside it, and the two stop reading as a pair. */
  --nav-logo-height:      36px;   /* → 40 at ≥992 */

  --modal-panel-max:     560px;

  /* Sticky column — design-rules §16, extracted from Config and kept.
     Not spacing-scale values: an offset from the viewport edge and a measure
     are neither margins nor padding, and answer to neither scale. */

  /* Sticky-stack offset — §10 only. R9z: ONE VALUE, not six.

     The stack used to stagger — each card pinned 20px lower than the last so
     the previous card's top edge stayed visible, and that visible edge was
     what made the pile read as depth. By direction each card now covers the
     one before it COMPLETELY, which means they all pin at the same line and
     only z-index separates them. A stagger and full coverage are mutually
     exclusive: the offset difference IS the sliver.

     --sticky-stack-2 through -6 are deleted rather than left orphaned; they
     described a behaviour the section no longer has. */
  --checkbox-size:        20px;
  --checkbox-mark-size:    8px;

  /* --------------------------------------------------------------------
     ICON (design-rules §13)
     Stroke-based, geometric, squared terminals. One set page-wide.
     -------------------------------------------------------------------- */
  /* Pixel-glyph family. Drawn on a 15-unit grid.
     18px is the default: large enough for the stepped construction to read,
     small enough to sit correctly in a 52px icon box. It is NOT a whole
     multiple of 15, so `shape-rendering: crispEdges` is deliberately NOT
     applied — see button.css. 15px and 30px are the exact-multiple sizes. */
  --icon-size-pixel:    18px; /* default — button and inline */
  --icon-size-pixel-sm: 15px; /* 1:1 with the grid — exact, hard edges */
  --icon-size-pixel-lg: 30px; /* 2:1 with the grid — exact, hard edges */
  --icon-size-inline:  16px;  /* stroke glyphs — interim, see design-rules §13 */
  --icon-size-list:    20px;  /* list markers */
  --icon-size-feature: 24px;  /* feature */
  --icon-stroke:          2;

  /* --------------------------------------------------------------------
     THE CELL — signature motif module
     3:2, rotating to 2:3 when it holds a person.
     -------------------------------------------------------------------- */

  /* Panel-corner notch. design-rules §12.1: "20×20px notches at panel
     corners". Notch geometry is its own family and does not answer to the
     spacing scale, exactly as icon sizes do not. */

  /* Ticket-notch geometry. Every button, trailing edge of the icon block. */
  --notch-depth: 6px;
  --notch-span: 10px;

  /* The notch chevron, as a MASK. Used to re-stroke the focus ring across the
     notch, where clip-path has cut the border away.
     The `%23000` is mask alpha, not a design colour — a mask reads only the
     alpha channel, so the fill is meaningless. Do not flag it as an untokened
     colour. Delivered as a mask (not background-image) so the stroke takes
     `background-color` and can follow the focus token — see design-rules §13.
     viewBox is depth+2 × (span×2)+2, so it maps 1:1 onto the box the ring
     sizes from the same two tokens. */
  --mask-notch-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='22' viewBox='0 0 8 22'%3E%3Cpath d='M8 1L2 11L8 21' fill='none' stroke='%23000' stroke-width='2'/%3E%3C/svg%3E");

  /* --------------------------------------------------------------------
     PORTRAIT FRAME — the module that holds a person.
     Two-step staircase at all four corners. Steps are square, so the step
     is a length, not a percentage: a percentage would go oblong the moment
     the frame is not square.
     -------------------------------------------------------------------- */
  --portrait-step:  var(--space-md);  /* 24 mobile → 40 desktop */
  --portrait-ratio: 250 / 233;        /* the reference frame, 853:795 */

  /* --------------------------------------------------------------------
     STROKE & BLUR
     -------------------------------------------------------------------- */
  --border-hairline:   1px;

  /* A SECOND BORDER WIDTH — R6s. The system had exactly one, and one is the
     right number for hairlines on panels, cards and dividers. This exists for
     a single job: the §3 pull rule beside the closing line. At 1px, a rule
     standing next to 34px display type does not read as a mark — it reads as
     a rendering artefact. 2px is the smallest width that holds its own there.
     If it ends up used anywhere a hairline would do, it should be deleted.

     R7t took its original consumer away — the §3 pull rule is stem-matched in
     em now so it can track the 26/30/34px type it sits beside. R8a gave it a
     new one: the ring separating §4's overlapped face circles. Different job,
     same reasoning — 1px is not enough to read as a deliberate edge. */
  --border-emphasis:   2px;
  /* 20 → 28 at R6d. The glass reads as glass by SMEARING what passes behind
     it; opacity alone just makes a dark panel look thin. The saturation is
     the other half — it keeps colour in the blurred content instead of
     letting the black scrim grey it out. Neither costs contrast: both act on
     the backdrop, and the scrim above still governs luminance. */
  --blur-glass:       40px;
  --saturate-glass:    200%;
  --blur-scrim:        8px;
  --focus-ring-width:  2px;
  --focus-ring-offset: 2px;

  /* --------------------------------------------------------------------
     MOTION
     -------------------------------------------------------------------- */
  --duration-fast:      200ms;
  --duration-base:      300ms;
  --duration-modal-in:  300ms;
  --duration-modal-out: 240ms;
  --duration-reveal:    560ms;

  --ease-standard: ease-in-out;
  --ease-reveal:   cubic-bezier(0.16, 1, 0.3, 1);

  /* HERO SLIDER — three values, none of them on the scales above, all three
     by direction. They are quarantined here rather than folded into the
     motion scale because the slider is the page's one continuously-moving
     element and nothing else may borrow its timing.

     HOLD is the dwell between advances. It is not on the duration scale
     because the scale covers state changes and entrances, and this is an
     interval between them — a different kind of value. 1500 → 2500 at R5g:
     at 1500 the card arrived and left almost together, which reads as a
     flicker rather than as a set being shown one at a time.
     ⚠ 2500 → 1000 → 1500 at R24y, by direction — BACK TO the exact value
     R5g rejected as a flicker. It is the setInterval period, so it CONTAINS
     the 900ms travel: a card rests ~600ms before it moves again. Directed,
     not drifted; R5g's reasoning is kept so the trade is visible.

     TRAVEL is how long a card takes to cross. --duration-reveal (560ms) was
     borrowed at first and is wrong for this: a reveal is a short arrival,
     this is a long traverse.

     EASE is symmetric, and that is the point. --ease-reveal is a hard
     deceleration — it launches and coasts, which is right for something
     ARRIVING and wrong for something PASSING THROUGH. A slide has no
     arrival: the card leaving matters as much as the card entering, so
     both ends of the curve are eased equally. */
  --duration-slide-hold:   1500ms;
  --duration-slide-travel:  900ms;
  --ease-slide: cubic-bezier(0.65, 0, 0.35, 1);

  /* §9's photo rail, ≤991 only. R24y — split off --duration-slide-hold, which
     it used to borrow as hold × 2, so the Hero's dwell could change without
     dragging this rail's interval along with it.

     ⚠ R25n — 5000ms -> 500ms, then R25o — 500ms -> 1500ms, both by
     direction. Measured at 500ms the rail did land on every tile, but a full
     pass of six images took ~3s and the rewind to the first image ran
     constantly; 1500ms is the Hero slider's dwell and the pace the note here
     recommended.

     ⚠ IT IS AN INTERVAL, NOT A TRANSITION. advance() calls scrollTo with
     `behavior: 'smooth'` and the browser's own glide across one tile takes
     roughly 300-400ms, so at 1500ms each image SETTLES for about a second
     before the next is fetched. Below ~500ms the glides start overlapping
     and the rail reads as one continuous drift instead. */
  --duration-venue-hold:   1500ms;

  /* R23q — THE COUNTER'S MOTION, shared by the figure and the bar that draws
     the same number.

     ⚠⚠ THE 1200 WAS HARD-CODED IN motion.js AND THAT WAS ALREADY A BREACH.
     The slider's own note in the same file states the rule — "both are read
     from CSS, never hard-coded" — and initCounters was the exception nobody
     had needed to touch. §12's progress bar needs the SAME duration to finish
     with the number it illustrates, so the value had to stop living in one
     language. `readMs('--duration-count')` now reads it.

     ⚠⚠ --ease-count AND THE JS EASING ARE ONE CURVE WRITTEN TWICE, because a
     cubic-bezier string cannot be evaluated by the counter's rAF loop and the
     loop's formula cannot be handed to a CSS transition. motion.js runs
     `1 - Math.pow(1 - p, 3)` — cubic ease-out — and this is its bezier twin.
     CHANGE BOTH OR NEITHER: if they drift, the bar and the figure will
     separate mid-count and land together, which reads as a stutter rather
     than as a difference in timing. */
  --duration-count: 1200ms;
  --ease-count: cubic-bezier(0.33, 1, 0.68, 1);

  /* MARQUEE — R7m. Quarantined the same way the slider tokens are: reserved
     to the Hero's partner strip, borrowed by nothing else.

     40s is a RATE, not a duration anyone waits out — one group's width per
     40s. It is deliberately far slower than anything else on the page: the
     strip is peripheral, and a marquee fast enough to notice is a marquee
     that competes with the headline beside it. LINEAR, always — any easing
     puts an acceleration in the middle of a loop that is supposed to have
     no beginning and no end. */
  --duration-marquee: 40s;

  /* R18e — THE LEAD-IN EVERY ENTRANCE ON THE PAGE WAITS, by direction: "all
     the slide in animation in the page will have a slight delay 300ms".

     An entrance fired on the exact frame its box crosses the trigger line
     reads as a reflex of the scroll. A beat in front of it reads as a
     decision, and it also lets a lazy-loaded image finish decoding before
     anything uncovers it.

     ⚠ IT IS ITS OWN TOKEN THOUGH --duration-base IS ALSO 300ms, and the
     coincidence is the reason to separate them, not to merge them. One is
     how long a hover takes; this is how long an arrival waits. Tune the
     hover speed with them shared and every reveal on the page silently
     moves with it — a coupling nobody would look for.

     ⚠ IT IS ADDED TO --reveal-delay, NEVER SUBSTITUTED FOR IT, so
     .reveal-stagger and .reveal-sequence keep working on top of it.

     ⚠ NOT FOR ANYTHING THAT ANSWERS A PERSON. The modal, the dock and every
     hover are excluded on purpose: a control that hesitates 300ms after a
     click or a pointer reads as broken, not as considered. This is for
     entrances the SCROLL triggers. */
  --delay-reveal-lead: 300ms;

  --stagger-step:    70ms;
  --reveal-distance: 24px;

  /* MICRO-MOVEMENT — extracted from Config, adapted.
     See CLAUDE.md §6 for what was taken, adapted and rejected. */
  --icon-swap-distance: 50px;   /* [Config] .primary-button-icon.hover start X */
  --media-hover-scale:  1.05;   /* [Config] 1.1 → capped at 1.05 by design-rules §16 */
  --drift-distance:     12px;   /* [Config] .registraton-object 8.8–15px diagonal */
  --duration-drift:       9s;   /* ambient loop, desktop only */

  /* R25g — THE SHIMMER'S FULL CYCLE, by direction: "every 3 seconds". It is
     the INTERVAL, not the sweep: --shimmer-travel is how long the band takes
     to cross, and the remainder of the 3s is the rest between passes. Two
     values so the pace and the speed can be tuned independently — at one
     value, slowing the cycle would also slow the band to a crawl. */
  /* R25i — 3s -> 2s, by direction: "the interval will be 2 seconds."

     ⚠ R25j — AND THEN THE SWEEP SLOWED, 840ms -> 1000ms, by direction: "make
     the shimmer slightly slow with the same 2 seconds interval in between on
     full sweep." THE CYCLE IS UNTOUCHED — a pass still begins every 2s — so
     what moved is the split inside it: half the cycle crossing and half at
     rest, where it was 42/58.

     ⚠ NOT 1500ms. That was tried at R25h against a 3s cycle and reverted by
     direction; "slightly" is the word used here, and 1000ms is one step from
     840 rather than two. */
  --duration-shimmer:     2s;
  --duration-shimmer-travel: 1000ms;  /* 50% of the cycle */

  /* R25j — the wait before the FIRST pass only, by direction: "the first
     shimmer comes after 1 second of page load."
     ⚠⚠ ITS OWN TOKEN, WHERE R25h REUSED --duration-shimmer FOR THE DELAY.
     That worked only because the wait and the cycle were both 3s and were
     the same fact; 1s against a 2s cycle are two different facts, and one
     value cannot carry both without the wait moving whenever the cycle does. */
  --delay-shimmer:        1s;

  /* R25i — the band's lean. NEGATIVE leans the top of the band forward, the
     direction the sweep travels, so the sheen reads as raking across the
     control rather than tipping back against its own movement.
     ⚠ 12deg IS SMALL ON PURPOSE — "slightly angled". Over a 52px control the
     top and bottom edges part by 52 × tan(12°) ≈ 11px. */
  --shimmer-skew:       -12deg;

  /* --------------------------------------------------------------------
     LAYER
     -------------------------------------------------------------------- */
  --layer-base:          0;
  --layer-raised:        1;
  --layer-nav:         100;
  --layer-modal-scrim: 900;
  --layer-modal-panel: 910;

  /* --------------------------------------------------------------------
     BREAKPOINTS — reference only.
     Custom properties cannot be used inside @media. These exist so JS and
     future sessions read the same numbers the media queries use.
     -------------------------------------------------------------------- */
}

/* ==========================================================================
   TABLET — ≥768px
   ========================================================================== */
@media (min-width: 768px) {
  :root {
    --font-size-display: 52px;
    --font-size-h1:      52px;
    --font-size-h2:      38px;
    --font-size-h3:      30px;
    --font-size-h4:      24px;
    --font-size-lead:    20px;
    --font-size-body-lg: 18px;
    --font-size-body:    18px;

    --container-padding-inline: var(--space-lg);   /* 32 */
    --section-padding-block:       var(--space-4xl); /* 80 */
  }
}

/* ==========================================================================
   DESKTOP — ≥992px
   Type reaches full scale here, not at 1200. A 76px H1 at a 1100px viewport
   is correct; holding tablet type to 1200 would leave laptops undersized.
   ========================================================================== */
@media (min-width: 992px) {
  :root {
    --font-size-display: 76px;
    --font-size-h1:      48px;
    --font-size-h2:      48px;
    --font-size-h3:      34px;
    --font-size-h4:      26px;
    --font-size-lead:    22px;
    --font-size-body-lg: 20px;
    --font-size-body:    18px;

    --container-padding-inline: var(--space-2xl);  /* 48 */

    --section-padding-block:       var(--space-5xl); /* 120 */

    --gap-heading-to-content: var(--space-2xl);    /* 48 */
    --gap-card-padding:       var(--space-lg);     /* 32 */
    --gap-grid:               var(--space-md);     /* 24 */

    --control-padding-inline: 28px;

    --portrait-step:   var(--space-xl);  /* 40 */

    --nav-height:      80px;
    --nav-logo-height: 44px;
  }
}
