/* components/tokens.css - the Vault design tokens, two levels.

   PRIMITIVE = raw values, no opinion about purpose. Every value here already
   existed in ui-kit/kit.css; the only edits are the drift merges, each marked
   "merged:".
   SEMANTIC  = roles. Colour only. Each role points at a primitive through var()
   and carries the usage it was read from (ui-kit/docs/tokens-audit.md).

   Geometry, type and motion get NO semantic level: a radius or a gap has nothing
   for a theme to override, so components read those primitives directly.
   Colour does have something to override, so a component may only read colour
   through a semantic role.

   Source of the reading: ui-kit/docs/tokens-audit.md (step 1). No em dash. */

/* A CONTROL'S OWN HEIGHT, AND THE ONE CUSTOM PROPERTY IN THIS SYSTEM THAT DOES
   NOT INHERIT. A component that declares a height writes it here and the touch
   floor in base.css reads it as `max(--control-44, --control-h)`, which is what
   makes a floor a floor instead of an assignment: written as a plain
   `min-height:44px` the floor OUT-SPECIFIES the size that declared 48, so a
   button would have stood 48 under a mouse and 47 under a finger. Measured, both
   ways, before this was written.
   IT IS DECLARED WITH @property FOR ONE REASON: a control's height is not
   something its children are entitled to. A plain custom property inherits, so
   `--control-h:48px` on a button would reach every box inside it, and any of
   those already named by the floor would silently claim the button's height.
   `inherits:false` with a 0px initial makes the unset case exactly `max(44,0)`,
   which is the floor unchanged for the fifteen families that do not set it. */
@property --control-h{syntax:'<length>';inherits:false;initial-value:0px}

/* ==========================================================================
   1. PRIMITIVE
   ========================================================================== */

:root{

  /* ---- graphite ramp (dark to light). The Vault canvas and every stone ---- */
  --graphite-950:#0b0c0e;   /* engraved groove line */
  --graphite-940:#0d0f12;   /* every recess: input well, segmented rail, chart well */
  --graphite-930:#0f1013;   /* page */
  --graphite-920:#111316;   /* card gradient end */
  --graphite-910:#121417;   /* content plate, and the course sidebar */
  --graphite-900:#141619;   /* device canvas, card base, the prompt on hover */
  --graphite-880:#15171b;   /* quiet card block and the mobile bet dock */
  --graphite-870:#17191d;   /* stone gradient start (slab, plate, card) */
  --graphite-860:#191b1f;   /* outer slab base, dock gradient start, a chip pressed */
  --graphite-850:#1b1e23;   /* graphite chip (cat-nav, tabs, options, load-more) */
  --graphite-830:#1c1f24;   /* raised surface: header, dialog, dropdown, footer, state block */
  --graphite-800:#20242a;   /* control hover, and the dialog head gradient */
  --graphite-780:#24282f;   /* quiet control fill */
  --graphite-750:#2b2f38;   /* hairline */
  --graphite-720:#282b32;   /* course sidebar hairline */
  /* graphite with alpha (photo veils on the featured hero) */
  --graphite-a12:rgba(20,22,26,.12);
  --graphite-a28:rgba(18,20,23,.28);
  --graphite-a50:rgba(20,22,26,.5);
  --graphite-a55:rgba(20,22,26,.55);
  --graphite-a82:rgba(20,22,26,.82);

  /* ---- bone (the warm light ink of the Vault) ---- */
  --bone-080:#f2eee4;       /* the plate a brand mark sits on */
  --bone-100:#ede7da;       /* primary text, and a chip label on hover */
  --bone-200:#ded6c5;       /* notification icon */
  --bone-250:#d8d2c6;       /* course sidebar text */
  --bone-300:#cbc3b2;       /* default icon stroke */
  --bone-500:#a49d8f;       /* secondary text */
  --bone-600:#8b8579;       /* course sidebar muted */
  --bone-650:#6f6a60;       /* the scrollbar thumb on graphite. A new step, and the reason is a
                               measurement: the two neighbours in this ramp are 5.03:1 and 2.46:1
                               on the plate, so one is louder than a scrollbar should be and the
                               other misses the 3:1 a non-text control has to clear. This lands
                               at 3.43:1 */
  --bone-700:#5a544a;       /* neutralised error border (toast) */
  --bone-a06:rgba(237,231,218,.055);  /* lit half of the engraved groove */

  /* ---- brass (the brand metal) ---- */
  --brass-200:#e6c877;      /* lit brass text: hero eyebrow, SEO plate */
  --brass-220:#d8bf7f;      /* the volume tag on the featured hero */
  --brass-250:#e7d6a6;      /* active chip label */
  --brass-300:#d9b968;      /* gradient lit end. merged: --lime, same value */
  --brass-400:#d7ac53;      /* text-safe brass on graphite */
  --brass-500:#c7a24e;      /* brand brass. merged: --brass, same value */
  /* one ladder, seven steps. A brass wash, a tint, a hairline and a glow are the
     same colour at different depths; a step of .05 is below what a screen shows */
  --brass-a06:rgba(199,162,78,.06);   /* the quietest wash */
  --brass-a09:rgba(199,162,78,.09);   /* a tint under a small block */
  --brass-a16:rgba(199,162,78,.16);   /* a selected tint, the focus glow */
  --brass-a30:rgba(199,162,78,.30);   /* the inset hairline of a plate, the card glow */
  --brass-a45:rgba(199,162,78,.45);   /* a border on hover */
  --brass-a60:rgba(199,162,78,.6);    /* a selected edge */
  /* THE SEVENTH RUNG EXISTS SO THE LIGHT LADDER CAN STEP, and until 2026-08-12 it
     did not. Section 3's law is that every brass tint moves up one rung on chalk;
     five of six could and the sixth had nothing above it, so --tint-brass-60 and
     --line-brass-strong stood on --brass-a60 in BOTH themes while --tint-brass-45
     and --border-brass-hover stepped up ONTO it. Four roles that are two distinct
     strengths on graphite were one value on chalk, and nobody had written down
     that the top of the ladder was the reason.
     .75 IS MEASURED, NOT EXTRAPOLATED. One ground held fixed (the .opt-row.sel
     border on ui-visual/event-detail-multi.html), the alpha swept, the rendered
     edge read out of a screenshot at device scale 4 rather than computed from the
     hex. Between neighbours a rung on chalk is worth 1.080, 1.096 and 1.087 of
     contrast (16 to 30 to 45 to 60); .75 lands 1.095 above .60, which is one rung
     and no more. The same three rungs on graphite are 1.315, 1.312 and 1.287, so
     a rung on chalk carries about a third of the separation it carries on
     graphite: that is the "about a third" section 3 already claimed, with a
     number under it for the first time.
     AND THE VAULT'S OWN SEPARATION IS NOT REACHABLE HERE, which is the half of
     this worth keeping. Measured edge against edge on one screen, .opt-row.sel
     beside .chip-amount.sel: the two stand 1.445 apart on graphite, 1.148 apart
     on chalk at this rung, and even OPAQUE brass would reach only 1.328. Daylight
     is honestly weaker at the top of this ladder in the same way it is for muted
     text and the brass link, and .75 is chosen because it is one rung and still a
     tint. 1.0 would buy 0.18 more separation by leaving the ladder altogether. */
  --brass-a75:rgba(199,162,78,.75);   /* daylight's selected edge, and its input hover */

  /* ---- plum (the near-black the brass carries as text) ---- */
  --plum-950:#180810;

  /* ---- outcome green ---- */
  --green-200:#4fd694;      /* YES line drawn over photography (hero chart) */
  --green-250:#42d18a;      /* the price line on the event chart */
  --green-300:#77d19b;      /* quiet YES text on graphite */
  --green-500:#4fa96b;      /* YES */
  --green-700:#3f7d55;      /* quiet YES line */
  --green-950:#0d1410;      /* ink on a filled YES */
  --green-975:#15211a;      /* win sheet head stone */
  --green-a12:rgba(79,169,107,.12);   /* quiet YES fill */
  /* --green-150, --green-a20, --red-250 and --red-a20 went with the four semantic roles
     they were the whole reason for: S49, 2026-08-06. A primitive exists because a role
     needs it, so when the role is retired the primitive is not kept in case, and the orphan-token pass
     asked the question a second time one level down. */
  --green-a42:rgba(66,209,138,.42);   /* the glow under the price line */

  /* ---- outcome red ---- */
  --red-300:#e79087;        /* quiet NO: on a control, and as a figure */
  --red-400:#e26d6d;        /* NO line drawn over photography (hero chart) */
  --red-500:#c85a50;        /* NO */
  --red-700:#8f4841;        /* quiet NO line */
  --red-950:#160b09;        /* ink on a filled NO */
  --red-a12:rgba(200,90,80,.12);      /* quiet NO fill */

  /* ---- the categorical series (the multi-outcome chart draws one line per
     candidate). Not outcome colour: these say "a different thing", not "yes" or
     "no", and for two of them that used to be a lie. The series read --green-200
     and --brass-300, so a candidate line was drawn in the YES ink and another in
     the brand metal: the same pixels the product uses to mean "this side won" and
     "this is us". A reader cannot be asked to hold two meanings for one colour.
     Green, red and gold are therefore reserved, and the series lives in the arc
     they leave free, cyan 187 through magenta 328, with one desaturated neutral.
     All five clear 4.5:1 on the chart well in both themes, because the reading
     under the chart is drawn in the selected line's colour. ---- */
  --cyan-400:#45c8d8;
  --blue-400:#5b9df0;
  --violet-400:#c77dff;
  --magenta-400:#f07ab8;
  --slate-400:#9aa0aa;
  --cyan-700:#17697a;       /* the same five, taken down onto chalk */
  --blue-700:#22589b;
  --violet-700:#7038a4;
  --magenta-700:#a33372;
  --slate-700:#4f5560;

  /* ---- neutral extremes and the bevel alphas the embossed plates need ---- */
  --white:#fff;
  --black:#000;
  /* the light ladder: six lips of an emboss, quietest first */
  --white-a04:rgba(255,255,255,.04);
  --white-a06:rgba(255,255,255,.06);
  --white-a10:rgba(255,255,255,.1);
  --white-a16:rgba(255,255,255,.16);
  --white-a20:rgba(255,255,255,.2);
  --white-a32:rgba(255,255,255,.32);
  /* the shade ladder: five depths of ink, from an emboss edge to an overlay */
  --black-a30:rgba(0,0,0,.3);
  --black-a45:rgba(0,0,0,.45);
  --black-a60:rgba(0,0,0,.6);
  --black-a72:rgba(0,0,0,.72);
  --black-a85:rgba(0,0,0,.85);

  /* ---- material: the grain of each stone and the brass graph grid ---- */
  /* 150px, the coarse grain of the outer slab */
  --grain-coarse:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='s'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23s)' opacity='0.9'/%3E%3C/svg%3E");
  /* 130px, the finer grain of the inner plate and the cards */
  --grain-fine:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='130' height='130'%3E%3Cfilter id='d'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23d)' opacity='0.8'/%3E%3C/svg%3E");
  /* the graph grid that fades in from the top-right corner of a card */
  --brass-grid:repeating-linear-gradient(0deg,transparent 0 27px,var(--brass-a06) 27px 28px),
               repeating-linear-gradient(90deg,transparent 0 27px,var(--brass-a06) 27px 28px);
  /* THE BRAND MARK, and it is the one drawing in this file that is not a
     material. It is a fork: a line rises, splits, and one branch is lit while
     the one not taken stays at 30 per cent. Two strokes, butt caps, a mitred
     shoulder, so it reads as cast rather than drawn and holds its mass at 16px.
     It replaced an up-trend tick on 2026-08-11 with the rename to Yonder, where
     the same shape is also the Y. Ink 15.8 x 17 centred in a 24 box: the box it
     stands in is 18px and `contain` paints it at about 12, which is the display
     face's ascender at 16px. `../docs/decisions.md`. */
  --logo-y:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none'%3E%3Cpath d='M11.6 12.2 5.8 6' stroke='%23c99e3f' stroke-opacity='.3' stroke-width='4.6'/%3E%3Cpath d='M11.6 20.5V12.2L18.2 5.1' stroke='%23c99e3f' stroke-width='4.6'/%3E%3C/svg%3E");

  /* ---- space (the grid is 4px, 2 is the only half step) ----
     A value off the grid is not a distance. 1px is a line, so it is --hairline;
     the measurement of a thing is --size-* or --control-* below. */
  --space-2:2px;   --space-4:4px;   --space-8:8px;   --space-12:12px;
  --space-16:16px; --space-20:20px; --space-24:24px; --space-28:28px;
  --space-32:32px; --space-40:40px; --space-56:56px;
  --hairline:1px;  /* a rule, a 1px inset, the 1x1 box of a visually hidden input */
  --ring:2px;      /* the width of a focus outline, and its offset from the control */

  /* ---- radius (one corner per job) ---- */
  --radius-2:2px;      /* the near-square card corner of the Vault */
  --radius-6:6px;      /* a chip, a tag, a small well */
  --radius-10:10px;    /* the default control corner */
  --radius-16:16px;    /* a sheet, a dialog head, a plate */
  --radius-pill:100px; /* a track, a badge, anything with round ends */
  /* AND `50%` IS THE SIXTH SHAPE, WHICH IS LEGAL ON A SQUARE AND ON NOTHING
     ELSE. Backlog 43, the shape half, decided 2026-08-10 by reading the boxes.
     The two are not interchangeable: on a box whose width is its height they
     draw the same circle, and on a rectangle `50%` is an ELLIPSE while
     --radius-pill is a stadium. So the rule is the box, not the value.
     **`50%` means: this box is square and I want a circle.** --radius-pill
     means: this box is longer than it is tall and I want round ends. A circle
     asked for with 100px is still a circle and a stadium asked for with 50% is
     not a stadium, so only one of the two directions is safe and it is not the
     one that looks tidier.
     CHECKED, ALL SEVEN IN components/: dialog.css twice at 210 x 210, and
     comments.css --size-28, hiw.css 224 x 224, course-chrome.css
     --size-4, hero.css --size-8, toggle.css --icon-16, each declaring its two
     axes equal. **0 of the 7 stand on a rectangle**, so the ambiguity the row
     was filed for does not arise anywhere in the system, and it is written here
     so that the first rectangle to reach for `50%` is caught rather than
     counted. The row's other half, 81 raw px that are genuine layout
     dimensions, is Responsive's and stays open. */

  /* ---- control and icon sizes ----
     Value named like the rest of the geometry, and each one says what it is for.
     A control size is the height of the box a finger or a pointer lands on; an
     icon size is the drawn mark inside it. */
  --control-28:28px;   /* the header band's labelled press */
  --control-32:32px;   /* dense icon button, desktop chrome */
  --control-36:36px;   /* the standard control height */
  --control-44:44px;   /* the mobile touch target (WCAG 2.5.5) */
  --control-48:48px;   /* the product's most common control */
  --control-56:56px;   /* the sheet's Confirm, the one large step */
  /* THE LADDER WENT FROM THREE RUNGS TO SIX ON 2026-08-09, AND THE ARGUMENT
     AGAINST DOING THAT WAS WRITTEN RIGHT HERE. --control-52 stood in this block
     and went with .btn-lg on 2026-08-03 under the sentence "a ramp is not a
     reason to keep a step: three heights are what the product's controls are,
     and the fourth was an offer nobody took". That was correct and it is not
     this case. 52 was a rung NOBODY DREW; 28, 48 and 56 are rungs the product
     ALREADY DRAWS, on 105, 575 and 6 placements, and the reason they were not
     tokens is that nothing was declaring a control's height at all.
     WHAT THE MEASUREMENT SAID, 2026-08-09, 160 pages of both trees at 1280 and
     at 390, 13,021 controls. TWENTY DISTINCT HEIGHTS in the painted tree, and
     the split is total: every height that landed on the 4px grid came from one
     of these tokens, and every height that did not was accumulated out of
     padding plus a border plus a line box. 2,907 readings of 5,607 accumulate.
     THE PART THAT DECIDES PARITY IS THE ONE PART THIS SYSTEM NEVER DECLARED.
     A control's box is padding + border + the line box, and the first two are on
     the ladder while the third is `line-height:normal`, which is the font's
     opinion: DM Sans gives 21px at 14px, 18px at 12px and 16.5px at 11px. So
     .btn-md came out 12+12+2+21 = 47 and .btn-lg 16+16+2+21 = 55, and no value
     in --space-* can make either even, because 21 is odd. Parity was flipping
     on the FONT SIZE: the same padding ladder gives sm a clean 36 at 12px.
     That is why a height is now a token the control reads (--control-h in
     button.css) rather than a number three other values happen to add up to.
     Backlog 40. The frozen kit keeps 52px as a literal in its own <style>,
     because a page kept as provenance must not follow a scale that has moved on
     without it. */
  --icon-12:12px;      /* a mark inside a control that already has a label */
  --icon-16:16px;      /* the default mark */
  --icon-18:18px;      /* a mark that carries a control on its own */
  --icon-20:20px;      /* a mark that IS the control, in a slot with a label under it: the bottom
                          bar. Added 2026-08-13 by backlog 133, and it was `--size-20` in
                          navitem.css: a mark sized off the SPACE ladder is a mark that moves when
                          somebody re-spaces a row */
  --icon-22:22px;      /* a state or hero mark */
  --icon-28:28px;      /* the mark of a whole SCREEN state, standing alone above a sentence: the
                          empty, error and resolved blocks. Added 2026-08-13 with --icon-20 and for
                          the same reason, from `--size-28` in state-block.css */
  /* THE FOUR RUNGS ABOVE WERE READ AND THE LADDER WAS STILL SHORT, WHICH IS BACKLOG 133 AND IS NOT
     WHAT THAT ROW SAID. It said `--icon-16` and `--icon-18` are "reachable only by writing a width
     and a height by hand", and they are: `base.css` gives `.ic` the 22 and `.ic-sm` the 12, and
     everything else is a placement writing the size itself. **That is the correct mechanism and
     the row asked for the wrong fix.** A class in the markup would put a SIZE in a document, and
     the size of a mark is a fact about where it stands: the same rule that says `container-type`
     is declared by whoever places a component. Counted 2026-08-13: 13 rules in the system resize a
     mark, and 8 of them read the two rungs the row called unreachable.
     **What was actually off the ladder was three placements and one of them was on no ladder at
     all**: `.nav-slot .ic` at `--size-20`, `.state-block .ic` at `--size-28` twice, and
     `.market-title .ic` at a raw `13px` on 9 placements, one pixel off a rung that already exists.
     The first two are why this ladder is six rungs now; the third reads `--icon-12`.
     **AND THE INSTRUMENT WAS WRONG BEFORE THE FINDING WAS, for the fourth pass running.** A sweep
     over all 105 screens reported marks at 13, 28 and 40 with 362 placements off the ladder, and
     40px is a 22px mark inside a padded tile: it was reading `getBoundingClientRect()`, which is a
     border box. **A mark's size is its declared width and never the box around it.** */
  /* THE WEIGHT OF A STROKED MARK, IN SCREEN PIXELS AND NOT IN USER UNITS, which is the
     whole point of it. `stroke-width` inside a 24 viewBox scales with the box, so one
     declared number renders at as many weights as the set has sizes: measured 2026-08-10
     over 2,044 stroked marks, **1,818 of them in the product**, where 2.2 declared
     rendered 1.20 at 12, 1.47 at 16, 1.65 at 18 and 2.02 at 22. Four weights, 1.68 to 1
     between the ends. The two extremes were in the STAND rather than in the product:
     3.67 on a 40px demonstration figure, and 0.92 on six `ui-kit/trustbar.html`
     specimens that had no declaration at all and took the SVG default of 1, which is
     under one device pixel at DPR 1. The rules that own a stroked mark declare THIS and pair it
     with `vector-effect:non-scaling-stroke`, so the number here is the number on screen.
     1.65 because 1,122 of the 2,044 already rendered there: it is the default mark at 18,
     and it is the weight the set was drawn against. docs/backlog.md 29. */
  --stroke-mark:1.65;
  /* AND THE SAFE FIELD STOPPED BEING A PROPERTY OF THE DRAWING THE DAY THIS
     TOKEN ARRIVED. Backlog 30, closed 2026-08-10, and the row named one mark
     where there are two.
     A safe field is the distance from the cell edge to the ink, in 24ths, and
     the rule is 2. While the stroke was 2.2 USER UNITS it scaled with the box,
     so half of it was 1.10 modules at every size and a mark had ONE field.
     `vector-effect:non-scaling-stroke` makes 1.65 a SCREEN width, so half of it
     is 0.825px, which is 1.65 modules in a 12px box and 0.90 in a 22px one:
     **the same drawing now has a different field at every size it stands in.**
     Measured by INK rather than by the path, painted at device scale 10 and
     summed by opaque pixel, over the eleven stroked marks in the product:
                             12px    16px    18px    22px
       close (sheet), chevron   4.20    4.65    4.80    5.02
       envelope, close (large),
       arrow-right, send-note,
       note-flag, plus          3.20    3.75    3.87    4.04
       tick                     2.20    2.70    2.80    3.05
       menu, send               1.20    1.65    1.87    2.07
     **Two marks fall inside the rule and only below 22px**, and they fall
     together because both paths sit 3 modules from the edge: the menu, whose
     three bars run x 3 to 21, and the send arrow, whose tip touches x 21. Row 30
     read 1.9 for the menu, which is the 18px column, and it was right for one
     box of four and blind to the second mark.
     THE RULE IS RESTATED ON THE PATH AND NOT ON THE INK, because the path is
     what a person draws and the overhang is arithmetic anybody can redo:
     **a mark's geometry keeps 2 modules clear of the cell, and the ink then
     overhangs it by 0.825px whatever the box.** All eleven clear that with the
     smallest at 3. What the table above is for is the other question, which is
     not this one: a 12px mark carries the same 1.65px of ink as a 22px mark, so
     it reads heavier and stands closer to its own edge, and whether a 12px
     stroked mark should exist at all belongs to whoever sets optical sizing. */
  /* The measurement scale. It shipped with two steps, 56 and 72, and a product
     needs ten: every other box in the system therefore borrowed a --space-* step
     for its own width and height. That reads as an accident in the file and it is
     a real one, because the two scales answer to different questions. A gap is a
     distance between things and can be retuned as a rhythm; the side of an avatar,
     an icon plate or a legend swatch is the size of a thing and moves when the
     thing does. Same numbers today, different reasons, so different names. */
  --size-2:2px;        /* the thickness of a drawn bar: a status rule, a tab underline */
  --size-4:4px;        /* a thin track: the odds bar, the hero probability bar */
  --size-8:8px;        /* a thick track, a skeleton line, a legend swatch */
  --size-12:12px;      /* a rank column, a grab handle */
  --size-16:16px;      /* a small square block */
  --size-20:20px;      /* a nav glyph box */
  --size-24:24px;      /* a toast mark */
  --size-28:28px;      /* an avatar, a state mark, a footer mark */
  --size-32:32px;      /* an icon plate inside a dialog */
  --size-40:40px;      /* a badge, a toggle track, a sheet grab */
  --size-56:56px;      /* the card thumbnail */
  --size-72:72px;      /* the profile avatar and the event-detail thumbnail */

  /* ---- layers ----
     A stacking order is a list, so it is written as one. The number is only the
     order; the name is the reason, and nothing outside this block may type a
     raw z-index. What was here before was 0 1 2 3 4 5 6 10 40 49 50 60 199 200
     201: three of those did the same job, five belonged to markup that has been
     deleted, and 199 next to 201 is the shape of a value that was picked to win
     an argument rather than to sit in an order.
     Below the product: a photograph, a veil, a decorative pseudo, the fill
     behind the text of its own row. Above it: everything that has to be read. */
  --z-under:0;         /* the thing something else is read against */
  --z-content:1;       /* content lifted above its own decoration */
  --z-float:2;         /* a frame or a control floating over a card */
  --z-close:3;         /* the close control on a photographic head */
  --z-dock:4;          /* fixed furniture at the foot of the window: the bet dock, the CTA bar.
                          It said `sticky` until 2026-08-16 and both bars were, which is why they
                          left the viewport with the footer. See --bottom-nav-h in the page frame */
  --z-nav:5;           /* the mobile bottom nav, over the dock it meets */
  --z-header:6;        /* the sticky app header */
  --z-menu:7;          /* what opens from the header or the toolbar */
  --z-chrome-scrim:8;  /* course chrome only, below: the drawer scrim */
  --z-chrome:9;        /* the course drawer */
  --z-chrome-top:10;   /* its toggle, which stays reachable while it is open */

  /* ---- page frame ---- */
  /* ONE MEASURE FOR THE PAGE. Every band (header, category strip, trust bar,
     content, footer) spans the window and what is INSIDE it sits on this
     width, so the logo, the first card and the first footer column share one
     left edge. The paint had set max-width:none on five of the six and left
     the footer capped, so above 1400 the content ran 200px wider than the
     footer under it and the page had two measures. */
  --container-max:1400px;    /* the product grid, and every band inside it */
  --container-read:800px;    /* SEO reading column: the plate at the foot of the feed.
                                MEASURED, 2026-08-03: 792px of it is 89ch of the 13px prose face
                                (1ch = 8.89px). THAT LINE READ "89 characters" UNTIL 2026-08-12
                                AND IT IS 132, because 1ch is 1.48 prose characters in DM Sans:
                                see --measure below, which had the same mistake for the same
                                reason. It is left alone anyway, and now for a measured reason
                                rather than an assumed one: this is the PLATE, not the line. The
                                prose inside it is capped one selector deeper and runs 87
                                characters, measured at 1440 on event-feed-crypto.html. A
                                container is not a measure. */
  /* `--container-doc:600px` STOOD HERE AND IT IS GONE, 2026-08-19, and the sentence that killed
     it was already written in its own comment: "The day a document puts uncapped prose straight
     into this column, the cap belongs on the prose and not here." That day had come and gone
     without anyone noticing, because the cap went on the prose AND stayed here, which is one
     question answered twice at two different numbers. `.read-col` was 600px with `--measure` on
     the paragraphs inside it, so a document page carried a 600px column holding 409px of text,
     and the 191px between them was the ragged edge a reader sees. The column takes `--measure`
     now and every block in it fills the column. It had exactly one reader, `.read-col` in
     patterns/browse-shell.css, so this is a deletion and not a rename; the reason it existed
     (a long document is a different question from the feed's SEO plate) is still true and is
     now answered by the reading SIZE rather than by a second container. */
  --container-dialog:464px;  /* how-it-works dialog */
  --container-sheet:420px;   /* outcome overlay */
  /* HOW TALL A MODAL SURFACE MAY GET, and it is ONE number now, 2026-08-14, backlog 153.
     Three surfaces in this system are fixed sheets over a page: `.bet-sheet`, the phone
     face of `dialog.app-dialog:modal`, and the filters sheet. They carried 92vh, 88dvh
     and 92svh: **two numbers, two units, and only one of the three had an argument
     written beside it.** 92 because it was already two of the three; `svh` because it is
     the SMALL viewport, the one measured with the browser's own bar shown, so a sheet
     capped in it always fits and never resizes under a reader's thumb. `dvh` grows and
     shrinks as that bar retracts, which for a fixed modal means the surface moves while
     somebody is reading it; `vh` is the LARGE viewport and lets the tail sit behind the
     bar. The `vh` fallback that stood beside two of them is gone: `CSS.supports` for
     `svh` is true in Chromium 151 and WebKit 26.5, measured, so it protected nothing here
     and cost one number written in two places.
     AND IT COULD NOT HAVE BEEN WRITTEN HERE ANYWAY, WHICH IS THE PART WORTH KEEPING. The
     fallback pair the other four sites write works because both halves are DECLARATIONS: the
     second is thrown out at parse time by an engine that cannot read its unit, so the first
     survives. A custom property is not parsed that way. It accepts any token sequence, so the
     unit is never rejected and the fallback never comes back. Measured in both engines on
     2026-08-15: `max-height:92vh` computes to 588.8px at 640; `--cap:92zzh` with
     `max-height:var(--cap)` computes to **`none`**; and `max-height:92vh` FOLLOWED by
     `max-height:var(--cap)` also computes to **`none`**, because `var()` is valid at parse
     time, wins the cascade, and only fails at computed-value time, where the fall is to the
     property's initial value and not to the declaration above it.
     THE OTHER FOUR SITES FOLLOWED ON 2026-08-15, backlog 155, so there is no pair left in this
     folder to be read against this token and the trap below is now a trap only for somebody
     putting one back. A reader who meets a pair somewhere and this token's single value and tidies
     the difference away in the obvious direction does not add a fallback, **they delete the cap**:
     three sheets, `dialog.css`, `betpanel.css` and the filters sheet, go from 92 per cent of the
     small viewport to no maximum at all. The inconsistency is therefore load-bearing, and this
     is the paragraph that has to be read before it is removed.
     THIS IS NOT THE RAILS' QUESTION AND THEY ARE DELIBERATELY NOT IN IT. A sticky rail
     caps at the viewport MINUS its own top offset, `calc(100svh - X - var(--space-16))`,
     which is one formula with a parameter in `catnav.css`, `betpanel.css` and `toc.css`,
     and it is already one answer. A sheet is a fraction of the viewport; a rail is the
     viewport less what is above it. Unifying the two would be a fifth number, not fewer. */
  --sheet-cap:92svh;
  --container-sidebar:220px; /* course sidebar, and the body inset that clears it */
  /* THE TWO PAGE INSETS, AND THEY RAMP RATHER THAN STEP. --gutter is the inset OUTSIDE the
     two-stone plate and --plate-inset the one INSIDE it; the second is a token because it is
     the half of a step that had only ever been taken by the first, so on a 360 phone the two
     nested gutters took 42px a side, 84 of 360, before a card began. Both are read in ONE
     place for the frame, base.css, the two-stone plate.

     UNTIL 2026-08-13 THEY BOTH STEPPED AT DESK 640, in a `:root` block below this registry:
     14 to 40 and 16 to 28, **38px a side and 76px in total, spent against a window that had
     grown by one pixel.** Measured on `event-feed.html`: the card was 577 wide at 639, 502 at
     640, and did not reach 577 again until the window was 715. **A column that shrinks while
     the window grows makes every width query above it a question asked the wrong way round**,
     and nine component rules are keyed to this rung: the bookmark pull in `card.css`, the
     outcome wrap in `options.css` and `yesno.css`, the four-column figures in `position.css`
     and three rules in `footer.css`. Every one of them turns on at the moment its own box gets
     smaller. That is the finding `docs/backlog.md` 129 went looking for in container queries.

     THE RAMP RUNS FROM ONE RUNG TO THE NEXT, DESK 640 to DETAIL 760, and its LENGTH is derived
     and not chosen: 38px a side has to be spent at no more than 0.5px per pixel of window or
     the column still goes backwards, so the ramp cannot be shorter than 76px. 760 is the next
     rung on the ladder, so this adds no width the system does not already name. Below 640 the
     clamps floor at 14 and 16 and at 760 they ceil at 40 and 28, which are the two pairs that
     were there before: **the product is unchanged outside 640 to 759, and this is only ever
     the fix for the band that was broken.** It is also one width query fewer, which is the
     right direction: a ramp is mechanism one on this stage's own ladder and a rung is three.

     Both clamps read 100vw, which is the same instrument the deleted query read: it counts the
     scrollbar and it does not know about a docked panel. Neither fact reaches the ramp, because
     the review sidebar docks at 1140 and both clamps are pinned long before that. */
  --gutter:clamp(14px, calc(14px + (100vw - 640px) * 13 / 60), 40px);
  --plate-inset:clamp(16px, calc(16px + (100vw - 640px) / 10), 28px);

  /* ---- the two values the width stage added, 2026-08-11, Responsive step 2 ---- */
  --rail-width:214px;        /* THE VERTICAL RAIL THAT ARRIVES AT RUNG 900, and it is a token
                                because TWO components already agreed on it in prose and nothing
                                held the agreement: `toc.css` says "the same 214px" in a comment
                                beside `.toc{flex:0 0 214px}` and `catnav.css` says it again for
                                `.subcat`. A number written twice with a sentence explaining that
                                it is the same number is the definition of a fact with no name. */
  --menu-min:196px;          /* THE FLOOR OF A DROPPED PANEL, read by `header.css` for the avatar
                                and notification menus and by `filters.css` for the filter panel.
                                Two files, one role: a panel hanging off a control is never
                                narrower than this, whatever the control is. */
  /* ---- AND WHY THERE ARE ONLY TWO OF THEM, which is backlog 43's actual question ----
     43 asked whether the raw layout px in this system need a scale of their own. Censused
     2026-08-12, comments and media queries excluded because prose is not a rule and a rung is
     registered elsewhere: 88 genuine LAYOUT literals in 51 distinct values. The 81 the row
     counted included 127 box-shadow numbers and 6 filter numbers, and a shadow offset is not a
     layout dimension.
     THE ANSWER IS NO, AND IT IS NOT A SHRUG. A ladder is for values that stand in a RELATION,
     which is what makes --space-8 and --space-12 two steps of one thing. 214, 300, 196, 322 and
     160 stand in no relation at all: each is one measurement of one part, and putting them on a
     shared scale would invent an arithmetic nobody measured and would make every later reader
     look for a meaning in the gaps.
     WHAT SOME OF THEM NEEDED WAS A NAME, AND THE TEST IS HOW MANY FILES USE THEM. 44 of the 88
     stand in exactly one file, 36 values across 18 files, and every one of those is that
     component's own measurement and stays where it is, with its reason beside it. The other 44
     are shared, and of the 15 shared VALUES only these two are one FACT: the rest are
     coincidences of arithmetic, a 4px that is an offset here and an inset there, an 18px that is
     a logo mark in one file and an icon in another.
     AND ONE WAS REFUSED FOR THE REASON --grid-gap WAS REFUSED. The `16px` at the foot of the
     three sticky columns is `var(--space-16)` now, not a `--sticky-gap`: the space ladder is
     already that number, and a second name for 16px is one number with two spellings. The three
     sticky TOP offsets stay raw, 120, 120 and 66, because they are not one fact either: they
     measure what happens to stand above each column, and what stands above differs. */

  /* ---- THE STICKY FURNITURE, 2026-08-16, and it is three names for three facts that four files
     were already writing down separately and getting wrong ----
     These pass the test the block above sets, which is how many files use a number rather than how
     round it is. `--bottom-nav-h` is read by `betpanel.css` for the dock's offset, by `base.css`
     for the page's bottom padding and for `scroll-padding`, and it is zeroed by `bottomnav.css`
     inside the rung block that hides the bar: three files and one fact, which is `--rail-width`'s
     case exactly.
     THE COST OF NOT HAVING THEM WAS ALREADY BEING PAID AND IT IS THIS FOLDER'S NAMED TRAP. The
     dock sat at `bottom:52px` to clear a bar that measures 56, so the two overlapped by 4px on
     every phone, and from DESK up the bar is `display:none` while the 52 stayed, leaving the dock
     floating over a 52px void from 640 to 759. Two numbers written to be one number and equal at
     no width at all, which is the `759.98px` against `47.5rem` defect the CLAUDE.md in this folder
     already carries.
     THEY ARE NOT PLAIN PIXELS AND THAT IS THE WHOLE POINT OF WRITING THEM DOWN. A bar's height is
     part padding, which is on the px ladder, and part the type inside it, which follows the
     reader's own browser default. Measured on event-detail.html at 390 with `Page.setFontSizes`,
     the only thing that moves a root, at defaults 16 / 20 / 24: the nav is **56 / 64 / 73**, the
     dock **68 / 76 / 85** and the header **59 / 63 / 69**. So a px token is exact at the default
     and 17px short at 24, which would show a strip of page between two bars that are supposed to
     touch; and a pure `rem` token overshoots, because the padding half does not grow. The fit is
     linear in both cases and the slope is the same 2.125 per pixel of root, so each token is
     written as its px half plus its type half and lands within half a pixel at all three defaults.
     `course-chrome.css` had already paid for the alternative: it bought the same clearance with
     `8.25rem`, an over-provision chosen because a fixed 132 was short at a 24px root, and it needed
     a width query and a borrowed rung to do it. Both are gone now, because a number that tracks
     the thing it clears does not need a rung to be cut at.
     MEASURED 2026-08-16 in Chromium, re-read in both engines after the change. They are CLEARANCES
     that other files ask for, not sizes assigned to the bars: each bar's height still comes from
     its own content, and the check that keeps these honest is to render the bar and compare. */
  --header-h:calc(39px + 1.25rem);      /* the sticky app header: 59 / 63 / 69 at roots 16 / 20 / 24,
                                           and this expression gives 59 / 64 / 69. It is what a
                                           control scrolled to by Tab has to clear at the top */
  --bottom-nav-h:calc(22px + 2.125rem); /* the mobile bottom nav: 56 / 64 / 73 measured, 56 / 64.5 /
                                           73 here. 0 from DESK up, overridden in bottomnav.css
                                           inside the rung block that already hides the bar, so the
                                           number and the reason it becomes 0 stand in one place */
  --dock-h:calc(34px + 2.125rem);       /* the event detail's bet dock, which rides on top of the
                                           nav: 68 / 76 / 85 measured, 68 / 76.5 / 85 here. Only the
                                           documents that HAVE one pay for it, through
                                           `html:has(.bet-dock)` in betpanel.css, because a screen
                                           with no dock must not be padded for one. It is 0 from
                                           DETAIL up, set in betpanel.css beside the rule that
                                           hides it */
  --measure:46ch;            /* THE LINE MEASURE FOR CONTINUOUS TEXT.
                                `ch` IS NOT A CHARACTER, and until 2026-08-12 this token was
                                written as though it were. `ch` is the advance width of the
                                digit ZERO in the element's own font, and a lining digit is
                                one of the widest things a prose face draws. MEASURED IN DM
                                SANS, by rendering every capped element and counting the
                                characters inside each line BOX rather than dividing the box
                                by 1ch: 1ch runs 1.45 to 1.53 times the mean prose advance,
                                ten readings over five placements at 1440 and at 360, mean
                                1.48. So 66ch was never 66 characters. It was about 98, and
                                the longest full line measured 99 in .sys-note, 93 in
                                .resolution and 93 in .protect-page at 1440: the improvement
                                66ch bought was real and it stopped short. Uncapped, the same
                                elements run 153.6 / 105.7 / 79.7ch, which is 224, 153 and 114
                                characters on the longest line; 66ch took them to 99, 93 and
                                93, still 30 per cent over the band in the only unit the band
                                is about. The instrument could not see it because the
                                census computed width / 1ch, so it asked the token's own
                                question back and got the token's own answer.
                                THE BAND IS IN CHARACTERS AND SO IS THE FIX. DESIGN.md section
                                3 says 60 to 75 characters, its middle is 67.5, and 67.5 / 1.48
                                is 45.6. AFTER, at 1440, longest full line: .resolution 65,
                                .sys-note 62 and 68, .protect-page 66 and 60. Swept as well as
                                derived, because one number that lands is a coincidence and a
                                window is a reading: the range in which all five placements sit
                                inside 60 to 75 is 45ch to 48ch. 42ch drops .protect-page to 57
                                and 50ch takes .sys-note to 78. 46 is where the arithmetic lands
                                AND it is inside the swept window, which is two instruments
                                agreeing rather than one being trusted.
                                AT 360 NOTHING IS CAPPED AND NOTHING EVER WAS. The same
                                elements run 32.7 to 36.4ch of available width and 46 to 51
                                characters, so this token is a desk rule; the phone is capped by
                                the gutter.
                                WHY `ch` SURVIVES THE CORRECTION. It is the only unit that holds
                                a character count across type sizes, which is what the rule is
                                about, and the sizes here are real: 11, 12, 13 and, from
                                2026-08-19, 16px. It said THREE until that day. The
                                px alternative was swept rather than dismissed, and it does
                                work TODAY: one px value puts all five placements inside 60 to
                                75 anywhere from 373 to 405px, a window 33px wide, and it is
                                that wide only because 11, 12 and 13 sit close together. What
                                it cannot do is hold when a size arrives. At the advance this
                                face actually draws, 0.465 of the font size, a 16px paragraph
                                capped anywhere in that window runs 52 to 54 characters, under
                                the band at every point of it, and a 14px one runs 60 to 62 and
                                clears only at the top. A px cap is solved for the sizes that
                                happen to be there; the cap is a typographic rule and it takes
                                a typographic unit.
                                AND THE INVENTED EXAMPLE ARRIVED, 2026-08-19, WHICH IS THE
                                CLOSEST THING A TOKEN COMMENT EVER GETS TO A PREDICTION HOLDING.
                                This paragraph read: the old justification names a case the
                                product does not have, it said one number caps "an 11px legal
                                line and a 16px paragraph" at one character count, there is no
                                16px capped paragraph, the argument is right and the example was
                                invented. Then the five document pages were raised from
                                `--text-13` to `--text-16` and `.read-col` took this token as its
                                own cap, and the sentence above it was tested rather than
                                reasoned. The px alternative would have given 50 to 54 characters
                                at a cap anywhere in the 373 to 405 window, under the band at
                                every point of it, exactly as predicted; `46ch` gives **70 to 73
                                characters measured on all five documents from DESK 640 up**, and
                                the column that carries it is 503px rather than 409. Swept as
                                well as derived, because the whole point is that one number is a
                                coincidence: at 13, 14 and 16px against caps of 38 / 40 / 42 /
                                44 / 46 / 48ch, the longest full line is the SAME count down
                                every size column, 62 / 66 / 69 / 71 / 73 / 77. The size is free
                                and the cap is what moves it, which is the sentence this token
                                has been making since 2026-08-12 with no fourth size to prove it
                                on.
                                THE THREE RIGHT EDGES ARE NOT AN ARGUMENT AGAINST IT EITHER, and
                                that was checked rather than waved off. A constant character
                                count buys three pixel widths, 409 / 378 / 346, so the page frame
                                twenty lines up cannot give these blocks a shared edge. Measured
                                across all 106 painted screens: .resolution, .sys-note and
                                .protect-page NEVER STAND ON THE SAME SCREEN, 9 and 2 and 2
                                screens with no overlap, so there is no page on which two of
                                those edges can disagree. THE FOURTH EDGE, `.read-col` at 503px,
                                ARRIVED ON 2026-08-19 AND IT IS THE ONE THAT COULD HAVE BROKEN
                                THIS, because it stands on five screens where `.protect-page` and
                                `.feed-seo p` also stand. It does not, and the reason is the fix
                                rather than luck: inside that column those two no longer take the
                                cap, so the five documents have ONE right edge and not three.
                                Re-counted from the rendered DOM of 115 painted documents on
                                2026-08-19, the denominator of 106 above being the tree of the
                                day it was written. The page frame is about the bands a
                                page is built from; a measure is about one block of prose.
                                THE CENSUS THIS TOKEN EXISTS FOR, taken at step 4 at 1440 over
                                all 106 painted screens with every dialog open. It counts only
                                CONTINUOUS TEXT THAT WRAPS, and the filter is the finding: of
                                775 candidates it throws out 261 that stand on one line, 40 that
                                are line-clamped, 27 whose child is laid out as a ROW, and 3 that
                                hold a block of their own. 15 were over 75ch, in five kinds. What
                                it recorded is CH, which is all the census could see. The second
                                column is this pass: the first three rows counted in the browser
                                with the cap taken off, the last two converted at 1ch = 1.48
                                characters, and the two agree to within 4 per cent.
                                                                       ch    characters
                                   9  .rules-panel .resolution     up to 106      153
                                   1  .sys-note in .cat-main             154      224
                                   2  .protect-page in .cat-main          77      114
                                   2  .feed-seo p                         89      131
                                   1  a dd, and it is .feed-seo too       89      131
                                STEP 1 SAID 38 AND THE NUMBER IS 12, because the first pass
                                measured the element's BOX. `.related-list li` is a flex row of
                                a 46px thumbnail, a question clamped to two lines and an odds
                                figure: it has a 106ch box and no 106ch line, and it was 27 of
                                the 38. The loudest of the 12 is `.resolution`: the NAMED
                                RESOLUTION RULE, the second design principle of this product
                                stated as a sentence, on the screen where trust is decided.
                                PLACED AT STEP 4 on three classes in the three components that
                                own them, .resolution (event-detail.css), .sys-note
                                (state-block.css) and .protect-page (notice.css), and the
                                .feed-seo family joined them on 2026-08-12 in seo-plate.css, so
                                the token had five reader selectors in four files. SIX IN FIVE
                                FROM 2026-08-19: `.read-col` in patterns/browse-shell.css takes
                                it as the COLUMN's own max-width, which is the first reader that
                                is not a paragraph. On the five documents that column is the only
                                measure in force, because the same pass took the cap off
                                `.feed-seo p` and `.protect-page` inside it: a reading column
                                that carries a measure and holds blocks that carry it again is
                                one question answered twice, and the two answers were 600px and
                                409px with a ragged 191px between them.
                                IT IS DELIBERATELY NOT A BLANKET RULE ON `p`: a paragraph inside
                                a card is already at 38ch and a floor that reaches it teaches
                                the wrong thing.
                                ONE READER STILL OUT-SPECIFIES THIS TOKEN and it is worth naming
                                rather than assuming: `seo-plate.css` line 83 caps the SEO prose
                                at a literal 60ch inside `.feed-seo-wrap`, which wins on 9 of the
                                11 screens that carry the plate. Measured 2026-08-12 at 1440 on
                                event-feed-crypto.html: 533.5px, 87 characters on the longest
                                line. That is over the band by the same arithmetic as everything
                                above, and it is that file's decision to take. */
  --grid-col-min:300px;      /* THE CARD FLOOR, and the only number the fluid track needs.
                                Moved out of patterns/card-grid.css on 2026-08-11, where it
                                stood as a literal inside `minmax(min(100%,300px),1fr)`: a
                                pattern may not invent a width any more than a component may.
                                THERE IS NO --grid-gap AND THAT IS A DECISION. The track gaps
                                at var(--space-16) and the space ladder is already the gap
                                ladder, so a second name for 16px would be one number with two
                                spellings, which is the thing this repository spent a week
                                removing. A grid is not a special case of spacing.
                                AND THE COLUMN COUNT IS NOT A TOKEN EITHER. `auto-fit` counts
                                the columns, so the track works at 1100px, which nobody
                                designed for. The grey tree does the opposite and it is the
                                clearest argument for this line: 104 wireframes hard-code
                                `repeat(3,...)` at 960 and `repeat(4,...)` at 1280, which are
                                two widths that appear nowhere else in the product and are on
                                no ladder. Two numbers and two rules to say what one fluid
                                track says with none. */

  /* ---- the breakpoint ladder ----
     A BREAKPOINT CANNOT BE A TOKEN AND THAT IS NOT AN OVERSIGHT. A media query
     condition does not read a custom property: `@media(min-width:var(--x))` is
     invalid CSS, `@custom-media` is unimplemented in every browser, and this
     repository has no build step to compile either. Declaring `--bp-rail:900px`
     here would put a value in the one place that lies: it would look usable and
     no rule could use it. So the ladder is DECLARED BY BEING READ, like every
     other rule in this repository since the gates were deleted, and the literal
     stays where the browser needs it.

     THREE RUNGS, and each is named by what ARRIVES at it, because a width with
     no event at it is a width somebody will round:

       640   DESK      the one divide. Below it a single column, a bottom nav and
                       a mobile dock; above it the desk. 13 rules in 13 files, and
                       it is the only rung most components ever meet.
       760   DETAIL    the event detail gains its second column: .bet-panel docks
                       as a sidebar, .bet-dock goes, .ed-layout turns to a row,
                       the chart gets its full height. 5 rules.
       900   RAIL      a vertical rail arrives BESIDE the content: sub-categories,
                       the table of contents, the how-it-works side column, and
                       .chip-lane changes shape to live in one. 6 rules.

     AND ONE THAT IS NOT THE PRODUCT'S:

       1140  HARNESS   the review sidebar docks and the body takes its 220px inset.
                       Not a product rung. It is 900 + 220 + 20 on purpose, so the
                       chrome can never take width the widest product rung is
                       counting on. It was 860 until 2026-08-10 and that cost 73
                       pages horizontal scroll at exactly one width. The reason is
                       written where the rule is, in base.css and course-chrome.css.

     THREE WIDTHS ARE NOT RUNGS, and each of them was measured before it was
     allowed to stay: 560 (.ed-head padding, .icon-btn-tile size), 620 and 980
     (the hero's own two steps). 520 was the fourth and it is gone: the record's
     four columns arrive at DESK now, because 520 was the width at which four
     columns first FIT and not the width at which they first read, and from 520 to
     555 the fourth label wrapped on every profile. The other three each cost
     something real to collapse and closed nothing, and the numbers are written
     beside the rules, in event-detail.css and hero.css. docs/backlog.md 72,
     closed 2026-08-10.

     A RUNG IS ONE PIXEL AND IT BELONGS TO THE WIDE SIDE. Until 2026-08-10 this
     ladder was written as pairs - `max-width:640px` in eight files and
     `min-width:40rem` in five - and BOTH of those match at exactly 640, so the
     rung rendered a page that exists at no other width. Measured on ten screens:
     nine showed the desk utility, the balance figure and its icon button, on a
     14px mobile gutter under a mobile header with no bottom nav, matching neither
     639 nor 641. Below a rung is `max-width:39.99875rem` now, and the .98 is not
     ceremony: a zoomed window reports a fractional width, 639.4 has to be mobile,
     and an integer bound would leave a gap where NEITHER branch applies, which is
     worse than the overlap it was fixing. The same pair stood at 760 and is
     closed the same way.

     THE RULE THAT MAKES THIS WORTH WRITING: a component may not invent a width.
     If a file needs a break that is not one of the three, it is either a one-off
     that says so in a comment beside it, or it is a fourth rung and this table
     is where it gets named first. The kit has already paid for the alternative,
     with a stand label written at 900 standing beside a bar that goes at 640.

     ---- THIS TABLE IS A REGISTRY, AND THE REGISTRY IS THE INSTRUMENT ----
     Added 2026-08-11 by the Responsive stage, step 2. Because the literal has to
     be repeated in every query, the only way this ladder can be checked is by
     reading every @media in the product and asking whether its number is on this
     list. That check is cheap and it is the whole reason the list is written out:
     NO NUMBER MAY APPEAR IN A PRODUCT MEDIA QUERY THAT IS NOT ON IT.

     The census, RE-TAKEN 2026-08-12 by reading every @media condition in the
     four trees with comments stripped first, because a width quoted in prose is
     not a rule and this table quotes several. Occurrences, and the files they
     stand in, out of 52 components, 104 wireframes, 58 ui-kit and 106 ui-visual:

       width      components  wireframes   ui-kit   ui-visual   what it is
       560px         2 [2]       9 [9]      0 [0]     0 [0]     one-off, named below   [35rem]
       620px         1 [1]       1 [1]      0 [0]     0 [0]     one-off, named below   [38.75rem]
       639.98px      8 [8]      57 [57]     4 [1]     0 [0]     DESK, narrow side   [39.99875rem]
       640px         6 [6]     401 [104]    1 [1]     0 [0]     DESK   [40rem]
       759.98px      4 [4]     183 [87]     5 [1]     0 [0]     DETAIL, narrow side   [47.49875rem]
       760px         3 [3]     184 [92]     1 [1]     0 [0]     DETAIL   [47.5rem]
       900px         6 [5]      94 [93]     0 [0]     0 [0]     RAIL   [56.25rem]
       980px         1 [1]       1 [1]      0 [0]     0 [0]     one-off, named below   [61.25rem]
       1140px        2 [2]       0 [0]      0 [0]     0 [0]     HARNESS, not the product
       1170px        0 [0]     104 [104]    0 [0]     0 [0]     GREY HARNESS, and it derives

     TEN DISTINCT WIDTHS IN THE PRODUCT, and the system holds nine of them, which
     is really five: three rungs each written twice for its two sides, plus the
     harness, plus three named one-offs. `ui-kit/_page.css` invents nothing and
     the 106 painted screens carry NO width query at all, which is the rule in
     ../CLAUDE.md rendered as a column of zeroes.
     IT WAS 1440 UNTIL 2026-08-13 AND IT IS 1170, WHICH IS THE SAME ARITHMETIC ITS
     TWIN ALREADY USED. Backlog 119. 1140 is `900 + 220 + 20`: the RAIL rung, the
     review sidebar and its inset, which is why it is a harness and not an
     invention. The grey screen-tree rail measures 250px in the same file, so the
     same derivation gives 900 + 250 + 20 = 1170, and 1440 was 270px of window
     that no rung, no rail and no gutter asked for. **The row was the arithmetic
     and not the number**: a harness width that does not derive is a preference
     wearing a harness's name, and 1440 is a laptop somebody had. Moved in all 104
     grey files, 312 occurrences: the media query, the drawer script's own
     threshold and the sentence above it, which is three copies of one number per
     file and the reason a grey file cannot hold a token. 0 occurrences of 1440
     are left in `wireframes/`.

     IT SAID TWELVE AND THREE UNTIL 2026-08-12, AND THE CORRECTION IS RECORDED
     RATHER THAN OVERWRITTEN, because a table that only ever shows today's number
     teaches nobody what moved. 960 and 1280 stood here as off-ladder widths in
     all 104 grey files: they hard-coded a three- and a four-column track that the
     paint computes with `auto-fit` and no query at all. Backlog 116 closed the
     same day and deleted both from every grey file, so two of the three rows are
     gone and the table went from twelve widths to ten. Every OTHER row in it
     re-counted byte-identical to the 2026-08-11 reading, which is the control
     that says the two censuses are the same instrument and not two opinions.

     ---- WHY THIS LADDER WAS IN PX, AND WHY IT IS IN rem SINCE 2026-08-13 ----
     The argument for rem is real and it is not the one that decides here: a rung
     in px reads the window only, so a person who enlarges their browser font sits
     at a desk width with a phone's worth of text in the line. Measured before it
     was believed, root font 16px against 24px, control first:

       10rem measures 160px then 240px          so rem DOES respond
       body                16px  ->  24px       follows, because nothing sets it
       heading             36px  ->  36px       clamp(28px,4vw,38px), vw not rem
       feed prose          13px  ->  13px       --text-13
       footer legal        11px  ->  11px       --text-11

     THE WHOLE TYPE SCALE WAS PX AND IT IS REM SINCE 2026-08-12, so the table above
     is the reading that CAUSED the change and not a description of the tree. Ten
     --text-* steps and eight --display-* clamps carried px literals, 213 of the
     229 font-size declarations here come through them, and a reader who set a
     24px default therefore got a page 0.2 per cent taller and not one additional
     word. The ramp is ratios now and the same reader gets every word. That was
     docs/backlog.md 115 and it is closed.
     THE RUNGS WENT TO rem ON 2026-08-13, docs/backlog.md 135, AND THE HEADING ABOVE
     IS LEFT STANDING BECAUSE IT IS THE ARGUMENT THAT WAS MADE. It was "a rung in
     rem while the type is px switches the layout at a different window width while
     every word stays the same size", which was true and was an argument about the
     TYPE. The type moved on 2026-08-12, so the argument was spent, and the rungs
     were then held by nothing except that nobody had decided.

     THE LADDER IS NOW: DESK 40rem, DETAIL 47.5rem, RAIL 56.25rem, and the three
     one-offs with them, 35rem, 38.75rem and 61.25rem. **The narrow side of a rung
     is 39.99875rem and 47.49875rem**, which is 639.98px and 759.98px converted
     exactly rather than rounded, because the rule that a rung is one pixel and
     belongs to the wide side is a rule about the PAIR: any rounding here reopens
     the hole that put 73 of 106 documents into horizontal scroll for a day.
     **The 1140 harness stays in px and so does the review toggle's 759.98**, both
     in course-chrome.css and one line here in base.css: the review sidebar is 220
     physical pixels of chrome whatever the reader's font is, so a rung that
     clears it may not move with the type.

     WHAT IT BUYS, MEASURED. At a browser default of 24px the DESK rung arrives at
     **960px** instead of 640 and the RAIL at 1350: a reader whose words are half
     as wide again keeps the one-column layout until the window is half as wide
     again, which is the whole content of the sentence "a rung responds to the
     reader". At 20px it is 800. Swept over all 105 screens at twenty widths from
     320 to 1600 at three browser defaults, **6,300 readings: 0 horizontal scroll,
     0 readings with anything other than exactly one navigation carrier**, and at
     the default root **0 differing readings of 2,100 against the tree before the
     change**.

     **AND THE INSTRUMENT WAS WRONG FIRST, WHICH IS THE PART WORTH KEEPING.** The
     first sweep set `html{font-size:24px}` and reported the rungs NOT moving: the
     bottom bar still went at 640 on all 105 screens at every root. That is correct
     CSS and a useless measurement, because **a length in a media query resolves
     against the INITIAL font size and ignores every declaration on the root
     element**, including the one the sweep had just written. A reader does not set
     `html{font-size}`, they set the browser's default, and only that changes both.
     It takes CDP `Page.setFontSizes` to simulate, and with it `(min-width:40rem)`
     is false at a 700px window while `(min-width:640px)` is true. **The same
     injection was the right instrument for item 115 and the wrong one here**,
     because that pass measured the type and this one measures a rung: the two
     instruments agree at the default, 0 of 2,100, which is what says they are the
     same instrument and not two opinions. */

  /* ---- type ---- */
  --font-display:'Space Grotesk',sans-serif;
  --font-body:'DM Sans',sans-serif;             /* every component */
  --font-body-root:'DM Sans',system-ui,sans-serif; /* body only: the root fallback chain is longer */
  --font-mono:'IBM Plex Mono',monospace;

  /* the ramp steps by 1 up to 14, then by 2 and 4. The half pixels it used to
     carry (9.5, 10.5, 11.5, 12.5, 13.5) were rem arithmetic from the grey
     wireframe, not sizes anyone chose; they were rounded up, so no line got
     smaller. 10px is the floor a mobile product should set. */
  /* THE NAME IS THE PIXEL AT THE DEFAULT ROOT AND THE VALUE IS A RATIO TO IT, since 2026-08-12.
     `--text-13` is 13px for everyone who has changed nothing, which is what it always was, and
     0.8125 x whatever the reader has set for everyone who has. Every step divides by 16 exactly,
     so the move is arithmetically inert at the default and there is no rounding anywhere.
     WHY THE NAMES STAYED PIXELS. A ramp named `--text-0-8125` would be a scale nobody can read,
     and the number in the name is what a designer says out loud. The name is the value at the
     root the whole world ships; the unit is what happens when a reader disagrees with it.
     WHAT THIS FIXED, MEASURED RATHER THAN ASSUMED, docs/backlog.md 115. Nothing in this system
     sets font-size on html, :root or body, so the root has always been the reader's own setting
     and rem has always responded to it. It reached nothing: with the ramp in px, a reader who
     set a 24px default got a page 0.2 PER CENT taller and not one additional word, on all 105
     screens, because 213 of the 229 font-size declarations here come through this ramp. The 717
     computed sizes that did move were containers with no text of their own. */
  --text-10:0.625rem;   --text-11:0.6875rem;  --text-12:0.75rem;    --text-13:0.8125rem;
  --text-14:0.875rem;   --text-16:1rem;       --text-18:1.125rem;   --text-20:1.25rem;
  --text-24:1.5rem;     --text-30:1.875rem;
  /* a display size is fluid, so it is one token and not a size plus a rule.
     THE FLOOR AND THE CEILING FOLLOW THE READER AND THE MIDDLE TERM FOLLOWS THE WINDOW, and
     that is the whole point of the shape rather than a compromise inside it: `vw` is left in vw
     because it answers a different question. At a 24px root on a 360 phone the middle term is
     the smallest of the three and the clamp sits on its rem floor, so the reader is obeyed; on
     a desk it sits in the fluid band, where the window is what should decide. */
  --display-feed:clamp(1.75rem,4vw,2.375rem);        /* the feed heading */
  --display-hiw:clamp(1.875rem,7vw,2.375rem);        /* the how-it-works hero */
  --display-sheet:clamp(1.5rem,5.6vw,1.875rem);      /* a dialog or outcome sheet heading */
  --display-seo:clamp(1.25rem,2.6vw,1.5rem);         /* an SEO section heading */
  --display-question:clamp(1.1875rem,2vw,1.5rem);    /* an event question as a page heading */
  --display-hero:clamp(1.1875rem,1.5vw,1.4375rem);   /* the same question on the featured card, one rank down */
  --display-quote:clamp(1.25rem,1.85vw,1.625rem);    /* the brand tile quote */
  --display-step:clamp(1.25rem,3.4vw,1.5rem);        /* a step title in the how-it-works stepper. It is
     a rank under --display-sheet on purpose: the sheet no longer carries a heading of its own there, so
     this is the loudest thing in the dialog, and three of them in a row at sheet size would each read as
     the title of the whole rather than of one step. */
  --display-tagline:clamp(1.4375rem,2.3vw,1.9375rem);/* the SEO brand tagline */

  /* 400 is the root default and the weight 192 of 260 text elements on the feed
     render at, and until 2026-08-07 it was the one step of this ramp with no
     name. betpanel.css had already needed it and written `font-weight:normal`,
     the single weight literal in the system. A step a scale does not name is a
     step somebody types. Found by step 3c, ui-kit/typography.html. */
  --weight-regular:400;
  --weight-medium:500;
  --weight-semibold:600;
  --weight-bold:700;        /* merged: font-weight bold, written as 700 everywhere now */
  /* THE MONO FAMILY HAS TWO OF THESE FOUR, and this is where that is written down, because
     fonts.css says "--weight-bold is documented as unavailable here" and until 2026-08-10 it
     was pointing at a line that said nothing. IBM Plex Mono ships 500 and 600 only, four
     files because each weight is cut for two unicode ranges. So on mono, `--weight-regular`
     renders the 500 face and `--weight-bold` renders the 600, by CSS font matching and not by
     synthesis: the row that filed this counted painted pixels and got 5380 / 5380 / 5993 /
     5993 at 400 / 500 / 600 / 700, two faces wearing four names.
     IT IS NOT A TRAP, IT IS LIVE, which the filing missed by reading declarations instead of
     the page: font-weight INHERITS, so an element that sets the mono family and no weight
     takes the body's 400. Measured over 160 documents, **1,041 mono elements in the painted
     tree compute 400 and 34 compute 700**, against 310 at 500 and 262 at 600: **1,075 of
     1,647 ask for a face that is not there**. Nothing renders wrong and nothing is faked, so
     the 700 face is NOT added - it would be 33 KB for a step no screen can show is missing.
     What is fixed is the silence: on mono there are two steps, and a rule that wants the
     heavier one asks for --weight-semibold. docs/backlog.md 36. */

  /* ---- tracking ----
     The last axis in the type system to get one, added 2026-08-07. It shipped
     with 59 declarations, 13 distinct values and no tokens at all, including two
     spellings of nothing (`0` and `normal`). Read on the screens rather than
     sorted by number, the 59 fall into three jobs and every value is one of them
     at one size band, which is what makes a scale possible here and not a
     rounding exercise. Found by step 3c, ui-kit/typography.html.

     The tighten half was ALREADY a scale and only needed names: three steps, each
     on its own size band. The open half was seven values doing one job at 10 to
     13px, and it collapses to three. Nineteen declarations move, none by more
     than .02em, and the widest label on the screens grew 1px. */
  --track-display:-.03em;   /* 24px and up: a display heading closes at size */
  --track-head:-.02em;      /* 13 to 20px: a card question, a logo, a name */
  --track-question:-.01em;  /* a question set as a heading, 14 to 24px */
  --track-none:0;           /* undo an inherited tracking. The one spelling */
  --track-caps-lg:.01em;    /* uppercase at display size, which needs almost none */
  --track-label:.03em;      /* the 10 to 11px micro-label. 12 declarations, the base */
  --track-caps:.06em;       /* a 12 to 13px small-caps heading, category or eyebrow */
  --track-caps-loud:.1em;   /* the widest, and it is a divider rather than a word */

  --leading-none:1;         /* an icon or a badge: the box is the line */
  --leading-flat:1.05;      /* display headings */
  --leading-tight:1.15;     /* a card question, a headline */
  --leading-snug:1.3;       /* a compact row */
  --leading-base:1.5;       /* body, and every long line */
  --leading-loose:1.6;      /* a reading column */

  /* ======================== MOTION, SYSTEMATISED 2026-08-15 ========================
     TWO DURATIONS, NOT THREE, AND THE COUNT IS THE FINDING. The Animation stage asks for
     three, named by the job the movement does: a RESPONSE (a control answers a finger), a
     change INSIDE a component already on screen, and an element ARRIVING. The inventory of
     moments was taken over all 105 grey screens, `ia/docs/flows.md` and every state selector
     in this folder, and the middle job has exactly one member, `.market-chevron` turning over
     when its market opens, which is feedback for a click and therefore a response. **So the
     product has two jobs with movement in it and the third rung would have been a number with
     nobody to spend it**, 20ms from the rung above and indistinguishable from it. A third
     arrives the day the inventory hands it a row, and that is a decision taken out loud, not a
     ladder written in advance. The same branch the rung ladder took at Responsive: three
     because three things arrive, never because three is the shape of a scale.

     THE PRODUCT HAD FIVE AND NONE OF THEM WAS A LITERAL. Measured 2026-08-15 by computed
     style over 163 documents in Chromium 151 and WebKit 26.5, 4,904 moving elements on the
     105 painted screens, both engines identical to the element: `.16s` 9,528 slots, `.12s`
     2,346, `.18s` 743, `.3s` 636, `.25s` 63. Every one of the five was already a token and
     `ui-visual/` holds 0 transitions of its own, so this stage did not find eight loose
     numbers to tidy. **What it found was one ROLE wearing four of them**: a hover is 160ms on
     a button, 180 on a photo tile, 250 on a trust plate and 300 on a card, which is the drift
     that only shows when the readings are grouped by job rather than read file by file.

     --dur-fast IS .16s AND THAT IS 10ms OVER THE GUIDELINE, KNOWINGLY. A response slower than
     roughly 150ms stops reading as an answer and starts reading as lag, and the argument for
     taking .16 anyway is what the response IS here: 18 of the 20 response declarations
     cross-fade a colour, a border or a ground, and two move something. A colour fade at 160ms
     is a fade; the ceiling is about a control that MOVES under the finger. Taking .12 instead
     would have been one line here and 9,528 slots following it, which is exactly why the
     number lives in this file and not in twenty. */
  --dur-fast:.16s;   /* RESPONSE: hover, press, focus, select. 20 declarations, 71 per cent of
                        every slot the product renders. It was .12s until 2026-08-15 with 3
                        readers, against .16s with 41, so the name kept its job and took the
                        value the product had already chosen. */
  --dur-slow:.25s;   /* AN ELEMENT ARRIVING: a modal sheet rising, the condensed category band
                        opening under the header, the harness drawer. Two numbers did this
                        until 2026-08-15, .25s on `sheet-rise` across 337 dialogs and .3s on
                        `betSheetUp` across 4 bet sheets, one job with two answers and two
                        spellings of the same 100 per cent translate. 337 decided it. */

  /* A PERIOD IS NOT A RUNG OF THE LADDER, AND IT CARRIES A DIFFERENT NAME SO THAT NOBODY
     RECONCILES IT WITH ONE. The two durations above answer "how long does this change take". A
     pulse answers "how often does it come round", and putting 1.4s beside .16s and .25s under a
     `--dur-` prefix would invite the next tidy-up to notice a ladder with a rung six times the
     one below it. One reader: the skeleton, which is the only place this product performs the
     status job. 1.4s is slow enough that the eye reads a breath rather than a blink, and the
     amplitude is deliberately shallow, .55 of full, because a plate that goes almost invisible
     reads as content flashing in and out rather than as a wait. */
  --pulse-period:1.4s;

  /* TWO CURVES, AND THE THIRD IS REFUSED WITH ITS REASON. The stage asks for standard, enter
     and exit. Nothing in this product animates a DEPARTURE: a dialog closes at once, the
     condensed band collapses through the rule that opened it, and no state has a leaving face
     anywhere in `ui-visual/`. An `--ease-exit` would be a token with no reader, which fails
     the idle control as loudly as an undeclared case. It arrives with its first exit. */
  --ease-standard:ease;                  /* 12,821 of 13,406 rendered easing slots, 95.6 per
                                            cent, and until today not one of them read a token:
                                            the keyword was typed out 54 times and defaulted the
                                            rest. The VALUE is unchanged on purpose. Renaming a
                                            curve is free and re-drawing one is a change to how
                                            every hover in the product feels, and no row of the
                                            inventory asked for that. */
  --ease-enter:cubic-bezier(.2,.7,.2,1); /* A sharp start and a long settle, which is what an
                                            arrival wants. 585 slots, 8 declarations. It was
                                            `--ease-out`, a name for the SHAPE; this one names
                                            the job, because the job is what decides which
                                            curve a new rule should take. */

  /* FIVE OLD NAMES STOOD HERE AS ALIASES FOR THE LENGTH OF ONE STEP AND ARE GONE, 2026-08-15.
     `--dur-quick` had 41 declarations, `--dur-base` 14, `--dur-slower` 5, `--ease-out` 8 and
     `--ease-inout` 1, and every one of the 69 was read, given a job and rewritten before the
     names were removed. **A token that survives a stage with an alias for a name is a token
     that was never merged**, which is why the alias had a deadline written beside it on the day
     it was created. `--ease-inout` was cubic-bezier(.4,0,.2,1) with one reader, the review
     harness drawer, which is an arrival: the harness curve changed and the product's did not.
     The rewrite skipped every comment on purpose, so the six historical quotes in `card.css`,
     `chip.css` and `docs/decisions.md` still say what was true when they were written. */
  /* THE MOVEMENT SWITCH, AND IT IS A MULTIPLIER RATHER THAN A RULE. 1 normally, 0 under
     `prefers-reduced-motion`, and every transform that MOVES something multiplies its distance by
     it: `translateY(calc(-3px * var(--motion)))`. Backlog 132, 2026-08-13.
     **The row asked for one rule keyed to a family and the family does not exist as a selector.**
     `base.css` already declares the global floor for duration, but shortening a transition never
     removes a `transform`, so five components each carried their own
     `@media(prefers-reduced-motion:reduce){...:hover{transform:none}}`. Censused first, because a
     blanket rule was the obvious fix and it is wrong: **the system holds 20 transform declarations
     and only 5 of them are movement.** Nine are rest geometry (a chevron drawn by rotating a
     square, a knob centred with `translate(-50%,-50%)`), two are a STATE that carries meaning
     rather than motion (`.toc-d[open]` and `.market-box[open]` turning a chevron over), and the
     rest are the harness drawer and two keyframes. **`*:hover{transform:none}` would flatten
     `.market-chevron` while the pointer is on it, so an open market would point its chevron the
     wrong way for exactly as long as you looked at it.** Pseudo-elements survive that rule and
     `.market-chevron` is an element, which is the whole difference and is why the count had to be
     taken before the rule was written.
     A multiplier works where a selector cannot, because it is the DECLARATION that knows whether
     its transform is a movement, and each component keeps its own distance in its own file: a card
     lifts 3px, a badge 2 and a provider button 1, and those are three decisions and not a ladder.
     At rest it is exactly what was there before, `calc(-3px * 1)` being `-3px`. */
  --motion:1;

  /* ======================================================================
     The light theme's own raw values. Declared here, with the rest of the
     primitives, because a primitive is a value and not a purpose: nothing
     below carries an opinion about where it goes. Section 3 is where they
     get one. The dark palette above is untouched, which is the point of the
     two levels.
     ====================================================================== */

  /* ---- chalk (the pale stone of daylight) ----
     The ramp is the graphite ramp REFLECTED ABOUT ITS OWN GROUND, and getting
     there took two corrections. The first cut inverted the ORDER and not the
     DIRECTION: the page landed in the middle of the ramp and every surface still
     came forward by getting LIGHTER, exactly as it does on graphite. Daylight
     read as a generic grey theme because that is literally what it was.

     In the Vault the ground is the extreme. The page is the darkest thing on
     screen and every surface rises off it toward the light. Reflect that and the
     page becomes the LIGHTEST thing on screen and every surface settles onto it
     by getting darker:

         chalk L* = page L* minus (graphite step L* minus graphite page L*)

     The step SIZES come across unchanged, which was the other half of the fix.
     The graphite fills span 11.7 L* and the first chalk ramp spanned 15.5, so
     every separation was a third too loud: a chip stood 9.5 L* off its bar where
     the Vault puts it at 4.0, and that is why the chips read as grey blocks.

     The reflection covers the STONE and nothing else, and the ramp is exactly as
     long as the stone is: eight steps for the eight graphite fills that carry
     depth, gradient stops included, none of them tuned by hand. The four graphite
     fills that carry PRESENCE rather than depth (the raised surface, the chip,
     the control and its hover) have no chalk step of their own, because on a pale
     ground presence is not a lightness at all; section 3 says where they went and
     why. That is why a name here carries the number of the graphite step it
     answers to and not a position in this ramp, which is also why the numbers run
     the other way: --chalk-940 is the lightest because --graphite-940 is nearly
     the darkest.

     What it costs: a graded face reads as lit from below, since its lit stop
     reflects into the shaded one. That is the right trade. The alternative was
     to hold the light overhead and reflect only the fills, and then an element
     sitting on the light end of a gradient loses its ground: it was the chip on
     the category bar that showed it, standing 7.7 L* off its plate where the
     Vault puts it at 2.4. The bevel still carries a lit lip on top in both
     themes, so the face is never read upside down, and on a pale stone a plate
     that darkens toward its top edge is what an inset actually does.

     Nearly NEUTRAL, which was the first correction: a constant R minus B of +8,
     shaped the way bone is shaped (blue drops further than red rises), because
     the graphite is faintly COOL (-4 to -8) and every bit of the Vault's warmth
     is in the ink, where bone sits at +19.

     Every value below is within 0.2 L* of its computed target. */
  --chalk-940:#fdfbf5;      /* every recess: input well, segmented rail, chart well */
  --chalk-930:#fcfaf4;      /* page, and a control: the ground, and the thing furthest forward */
  --chalk-920:#f9f7f1;      /* card gradient end, a control on hover, the dialog head */
  --chalk-910:#f8f6f0;      /* content plate, and the raised surface that stands on it */
  --chalk-900:#f5f3ed;      /* device canvas, card base, the chip, the prompt on hover */
  --chalk-880:#f3f1eb;      /* quiet card block, the mobile bet dock, a chip pressed */
  --chalk-870:#f0eee8;      /* stone gradient start (slab, plate, card) */
  --chalk-860:#eeece6;      /* outer slab base, and the dock gradient start */
  --chalk-750:#acaaa4;      /* the hairline, and the only step that is NOT a reflection: an edge
                               inverts its direction but not its distance, so on chalk it has to be
                               darker than its surface and it is cut by eye, not by the formula */

  /* THE RAMP HAS A FLOOR AND ONE ROLE IS STANDING ON IT, which belongs here beside the steps
     rather than only in a backlog row. `--icon-brass` is `--brass-600` in daylight, #a9822f, and
     it is a MARK: a graphical object, so its floor is 3:1 and not 4.5. Measured against every
     step of this ramp: 3.43 at chalk-940, 3.40 at 930 (the page), 3.31 at 920, 3.28 at 910 (the
     plate), **3.20 at 900, which is the card it actually stands on**, 3.14 at 880, 3.06 at 870
     and **exactly 3.00 at chalk-860**. So there are three steps of headroom under it and the
     eighth lands on the floor: any surface deeper than `--chalk-860` fails, and a surface made
     one step deeper takes it 0.06 to 0.08 closer. **The direction matters and docs/backlog.md 32
     had it backwards**: it wrote "any card ground made one step lighter puts it under", which is
     the number going down while the stone goes darker. Nothing else measured in this system is
     within 0.5 of a floor. It stands on 4 placements in daylight, all of them the saved bookmark
     on a card, all of them at 3.20. docs/backlog.md 32. */

  /* ---- ink (the dark text of daylight) ----
     Warm, not neutral. The Vault reads warm in the dark because its light ink is
     bone; a neutral black would cool the whole product on the way over. */
  --ink-900:#211f19;        /* primary text */
  --ink-700:#3a352c;        /* an icon that carries a control on its own */
  --ink-500:#565042;        /* secondary text */
  --ink-400:#6e6757;        /* the default icon stroke */
  --ink-300:#787262;        /* a FILLED glyph. Reflecting an icon keeps its contrast and changes
                               its weight: light on dark spreads and thins, ink on paper sits
                               solid, so the same ratio reads heavier here and has to come down.
                               4.3:1 on a card, against the Vault's 6.7:1 for the same mark */
  /* the shade ladder of daylight, against the five-step black one it answers to.
     Black at .45 is an outline on a pale stone and not a shadow, so daylight
     shades with the ink instead. The ladder is NOT read uniformly: a 1px inset
     edge wants the quiet end and a blurred drop shadow wants the loud one, and
     the first cut gave both the quiet end, which is why the blocks went flat.
     Five steps, not six: .06 had a consumer until the depth pass took it and
     nothing else wanted it, and a value nobody reads is a transcript. */
  --ink-a10:rgba(33,31,25,.1);
  --ink-a16:rgba(33,31,25,.16);
  --ink-a24:rgba(33,31,25,.24);
  --ink-a32:rgba(33,31,25,.32);
  --ink-a44:rgba(33,31,25,.44);
  --white-a70:rgba(255,255,255,.7);  /* the lit lip of an emboss, which needs far more on pale stone */
  /* the veils that sit between a photograph and the words on top of it */
  --chalk-a12:rgba(252,250,244,.12);
  --chalk-a35:rgba(252,250,244,.35);
  --chalk-a55:rgba(252,250,244,.55);
  --chalk-a62:rgba(252,250,244,.62);
  --chalk-a90:rgba(252,250,244,.9);

  /* ---- the metal and the outcomes, taken down onto a pale ground ----
     One dark brass, not three. On graphite there is room to separate a link
     brass from a lit brass from a chip brass and keep all three legible; on
     chalk there is not, and a legible brass beats a distinguishable one. */
  --brass-600:#a9822f;      /* brass as a SYMBOL rather than a word. A saved bookmark has to read
                               gold, and the text-safe brass below reads brown at 16px. 3.2:1, which
                               is the bar for a graphic, not the 4.5 a word answers to */
  --brass-700:#684f18;      /* text-safe brass on chalk, INCLUDING over a brass tint:
                               a selected chip puts brass on brass and that is the
                               ground the value was solved against, not bare stone */
  --green-800:#225b35;      /* quiet YES text on chalk, over its own 12% fill */
  --green-a42-lit:rgba(63,125,85,.42); /* the pair of --green-a42, derived the same way: the alpha is
                               unchanged and the INK is the green daylight's chart line is actually
                               drawn in (--green-700). --green-a42 is --green-250 at .42, and
                               --green-250 is the bright chart green that section 3 already says
                               "disappears on chalk", so the glow was the only part of that line
                               still wearing it. Measured on ui-visual/event-detail.html by shooting
                               the chart twice, once with the drop-shadow and once with filter:none,
                               and differencing: the halo's mean delta per byte was -3.25 red, -0.97
                               green, -2.02 blue, a green cast of +1.11 about its own mean, which is
                               the Vault's own halo (+1.14) because it was the Vault's own ink. With
                               this value it is -3.33 / -2.21 / -2.77, a cast of +0.56, the cast of
                               the line under it. 13 chart screens */
  --red-800:#863228;        /* quiet NO text on chalk, over its own 12% fill */
  --green-050:#e4f0e7;      /* the win sheet head stone, in daylight */

  /* the same noise at the same strength as the Vault's. The first cut dropped it
     to a third on the theory that an overlay blend bites harder on a pale
     ground; it does not, it bites LESS, because overlay above mid grey behaves
     like screen. So the grain came out invisible and the stone read as flat
     paper. These are the dark opacities, and the only reason the light theme
     needs its own copy at all is that the value is inside the file. */
  --grain-coarse-lit:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='sl'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23sl)' opacity='0.9'/%3E%3C/svg%3E");
  --grain-fine-lit:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='130' height='130'%3E%3Cfilter id='dl'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23dl)' opacity='0.8'/%3E%3C/svg%3E");
  /* the same graph grid, one rung up the brass ladder, for the same reason */
  --brass-grid-lit:repeating-linear-gradient(0deg,transparent 0 27px,var(--brass-a16) 27px 28px),
                   repeating-linear-gradient(90deg,transparent 0 27px,var(--brass-a16) 27px 28px);
  /* the same mark, drawn in the dark brass. The colour is inside the file, so a
     data URI can only be themed by having a second one, and the two have to be
     edited together: the mark is the one place in this system where a shape is
     written down twice on purpose */
  --logo-y-dark:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none'%3E%3Cpath d='M11.6 12.2 5.8 6' stroke='%23684f18' stroke-opacity='.3' stroke-width='4.6'/%3E%3Cpath d='M11.6 20.5V12.2L18.2 5.1' stroke='%23684f18' stroke-width='4.6'/%3E%3C/svg%3E");
}

/* ===================== LESS MOTION, AND IT IS ONE MECHANISM =====================
   THE OVERRIDE IS THE TOKENS, AND EVERY COMPONENT THAT READS A `var()` OBEYS WITHOUT KNOWING
   IT EXISTS. That includes the component nobody has written yet, which is the whole reason the
   switch belongs to the value rather than to a list of selectors: this folder has already paid
   twice for a promise kept as a list, the 44px touch floor that stood in six files as six
   lists, and the `transform:none` that stood in five.

   AND @media WORKS HERE WHILE IT COULD NOT AT RESPONSIVE, WHICH IS NOT A CONTRADICTION.
   A width query cannot READ a custom property, which is why the rung ladder above is a
   registry rather than a token. This query does not read one, it REDECLARES one inside itself.
   Asking a media query for the value of a variable and redefining a variable within a media
   query are opposite operations, and the ladder comment fifty lines up is about the first.

   1ms RATHER THAN 0s, AND THE DIFFERENCE IS THE INSTRUMENT. A duration of 0 removes the
   transition rather than shortening it, so `transitionend` never fires and any script waiting
   on it stalls. 1ms keeps the event and removes the movement, and it is also the value the
   check at step 5 looks for: anything ABOVE 1ms is a rule that is not reading these tokens.

   `--motion:0` MOVED HERE FROM `base.css` ON 2026-08-15, unchanged. It is a token override and
   it belongs with the tokens it overrides; it sat in the `*` net's block only because that
   block was written first. The census that says why the distance is a multiplier rather than
   `*:hover{transform:none}` is beside `--motion` above.

   WHAT IS NOT HERE: the blanket net on `*`. `base.css` still carries one, from before this
   stage, and it forces `.01ms` onto every element in the document whether or not that element
   reads a token. Measured 2026-08-15 over 163 documents: 6,555 moving elements per engine in
   the normal pass against **115,028** under `reduce`, which is the net counting the whole DOM,
   and 0 rows above 1ms out of 230,056. **That green says the net is present and nothing else**,
   because under a rule with `!important` a component that reads no token is indistinguishable
   from one that reads all of them. Step 5 takes the net off, measures without it, repairs each
   element that moves, and only then decides whether the net returns as a last line. An
   instrument may not hide the defect it is looking for. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-fast:1ms;
    --dur-slow:1ms;
    --motion:0;
  }
}

/* THE ONLY WIDTH QUERY THIS FILE EVER HELD WAS HERE AND IT IS GONE, 2026-08-13. It set
   `--gutter:14px` and `--plate-inset:16px` below DESK 640, and the two values still exist:
   they are the FLOORS of the two clamps in the page-frame group above, which is where the
   argument for them now lives. It is deleted rather than kept beside them because a token
   with a fluid value and a query that overrides it is a token with two answers, and the
   query was the half that asked the wrong question: it took 76px out of the content column
   at the pixel where the window got wider. `docs/backlog.md` 129 and `decisions.md`
   2026-08-13. The registry was 34 width queries that day, not 35, and this file holds none
   of them. **It reads 36 on 2026-08-21**, and the sentence above is dated for the reason
   this repository already wrote down: a count typed as a live fact is a live claim, and
   nothing re-reads it. This one was typed in five files and disagreed in four. */

/* WHEN TWO ROLES HOLD THE SAME VALUE. Twenty-seven groups below resolve to one
   colour in both themes: --outcome-yes and --result-won are both #4fa96b,
   --border-brass and --tint-brass-30 and --glow-brass are all brass at 30%.
   That is deliberate and it is not a duplicate to merge. A role is a reason, not
   a value: an outcome is what a market says, a result is what happened to your
   money, and the day those two need to differ the file has somewhere to put the
   difference. Merge them now and that day costs a rename across every component.
   The rule, then: two roles may share a value, and each says so where it is
   declared. A role that has never had a reason of its own is the one to delete,
   and the gate for that is 11, not this note.
   ========================================================================== */

/* ==========================================================================
   2. SEMANTIC - colour roles, and the two opacities a theme has to answer for
   Each role names WHY the colour is there, so a theme can move it without a
   component knowing. Read out of the usage table in ui-kit/docs/tokens-audit.md.

   It said "colour roles only" until the states pass, and the exception is worth
   the sentence. The rule in CLAUDE.md is that colour is the only thing with a
   second level, because a radius or a gap has nothing for a theme to override.
   An opacity has: it blends an element toward whatever is behind it, and the
   ground is the one thing a theme changes. The same .45 measures 3.79:1 on
   graphite and 2.48:1 on chalk, so the value being written is not the value
   being seen, and a role is the only place that difference can be recorded.
   Both are written out below rather than left to inherit, which is the point:
   a token declared once here would silently hand the dark number to daylight.

   The selector carries [data-theme="dark"] as well as :root so that a theme is
   not only a page-level state. Any element can be marked with either theme and
   its subtree gets that theme's roles, which is what lets the token page show
   both grounds side by side and what would let a screenshot, a preview or an
   embedded card render in the theme it is describing rather than the one the
   page happens to be in.
   ========================================================================== */

:root,
[data-theme="dark"]{

  /* ---- surfaces ---- */
  --bg-page:var(--graphite-930);        /* from: body, the ground behind the device */
  --bg-canvas:var(--graphite-900);      /* from: .device, .cat-nav, .feed (5 declarations) */

  --bg-slab:var(--graphite-860);        /* from: the outer stone, the whole page width */
  --bg-slab-from:var(--graphite-830);   /* same value as --bg-surface for now: the app header IS the
                                           lit top of the device slab, and it is also a raised panel.
                                           In daylight both step off the reflection together .      From: the slab top-lit gradient, and the sticky header */
  --bg-slab-to:var(--graphite-870);     /* from: the same gradient, bottom */

  --bg-plate:var(--graphite-910);       /* from: the inset content plate (feed, category, dialog body) */
  --bg-plate-from:var(--graphite-870);  /* from: the plate gradient, top */
  --bg-plate-to:var(--graphite-930);   /* same value as --bg-page for now: a plate that fades into its own ground .       From: the plate gradient, bottom */

  --bg-card:var(--graphite-900);        /* from: .card base under its grain */
  --bg-card-from:var(--graphite-870);   /* from: the card gradient, top */
  --bg-card-to:var(--graphite-920);     /* from: the card gradient, bottom */
  --bg-card-quiet:var(--graphite-880);  /* from: .pos, .cta-bar, .toast, .cc-banner, .wd-flow.
                                           These five read var(--card), which is declared NOWHERE
                                           (audit C4), so they render transparent today. The value
                                           restored here is --card3d, the token stage 08 declared
                                           for exactly this and never wired. The one intentional
                                           pixel change of this stage; it is shown in the step-3 diff */

  --bg-surface:var(--graphite-830);     /* from: .app-header, dialog, dropdown, .filter-panel,
                                           .state-block, .app-footer (18 declarations) */
  /* THE TWO BRASS BLENDS, AND THEY ARE A ROLE RATHER THAN SIX EYEBALL DECISIONS. Backlog 120.
     A blend is not an alpha: `--tint-brass-*` is one ink at a depth over whatever is behind, and
     these END in an opaque base, so row 14 was right that they cannot share that ladder. **What
     row 14 did not ask is whether they agree with each other, and they did not.** Ten brass
     `color-mix` declarations in seven files fell into two jobs and five values: brass into
     `--bg-control` at 11 (card), 15 (position), 16 (hiw), 20 (comments), 20 (iconbtn) and 22
     (hero), and brass into `--border-hairline` at 30 (hiw), 30 (hero) and 42 (position).
     **Resolved in a browser in both themes, three of the five distinctions do not exist.** 15 and
     16 are 0.0045 apart in oklab lightness, which is below anything an eye reads on a 32px tile;
     the two 20s are the same number; and **the 42 edge and the 30 edge are 0.001 apart in
     DAYLIGHT**, because the light hairline is already close to brass in lightness, so the one value
     that looks like a deliberate emphasis is invisible on half the product. Counted by placement,
     **568 of 576 already agreed at 20 per cent**: `.icon-btn-lift:hover` is 525 and `.cmt-av` is
     43, against 4 hiw tiles, 2 hero badges and 2 position figures. **That reading was taken hours
     before backlog 144 took the 525 social marks out of the footer**, so the standing population is
     43 of 57 and the majority is `.cmt-av` alone. The decision does not move with it, because it
     never rested on the majority: three of the five distinctions do not exist at all. **The 11, the value furthest
     from every other, stands on 6 cards and a first probe said zero**, because `.gallery .card`
     lives inside `.ptab-panel#p-wins` on the two profile screens and a sweep that walks a page as
     it loads sees the tab that is open. It is the same lesson as the dialog that is `display:none`
     and the odds bar a script writes: **a component behind a tab is invisible to a reading taken at
     load, and zero is what a blind probe reports.** The before-and-after caught it because that one
     compares every element whether painted or not.
     So it is one value per job, and it can be a role precisely because the BASE is the same in all
     six: what varies is the amount of brass, which is the thing a role is for. Declared in both
     theme blocks rather than once, because a theme here is an attribute on any element and a
     custom property holding a `var()` resolves against the element that DECLARES it: a single
     `:root` copy would bake the dark base into a light figure on the kit. */
  --bg-control-brass:color-mix(in oklab,var(--color-action) 20%,var(--bg-control));
  --border-hairline-brass:color-mix(in oklab,var(--color-action) 30%,var(--border-hairline));
  --bg-control:var(--graphite-780);     /* from: 58 declarations, background only: every quiet
                                           button, chip, toggle track and hover fill */
  --bg-control-hover:var(--graphite-800); /* from: 8 hover fills on those same controls */
  --bg-chip:var(--graphite-850);        /* from: the graphite chip family: cat-nav, tabs, options,
                                           load-more, profile tabs */
  --bg-well:var(--graphite-940);        /* from: the amount input, comment input, segmented rails */
  --bg-dialog-head-from:var(--graphite-800); /* same value as --bg-control-hover for now .    From: the dialog head gradient over the brass glow */
  --bg-chart-well:var(--graphite-940);  /* same value as --bg-well for now: a chart is a well with a plot in it .     From: the plotted chart and the range switcher */
  --bg-dock:var(--graphite-880);        /* same value as --bg-card-quiet for now: two quiet blocks at one depth .           From: the mobile bet dock */
  --bg-pressed:var(--graphite-860);     /* from: any control while it is held down. It was
                                           --bg-chip-pressed with one consumer, the Load more chip,
                                           and the states pass gave it twenty: a press is one answer
                                           for the whole system, not a chip's private idea. Named for
                                           the STATE and not for the chip, because the icon button in
                                           the header is not a chip and settling it onto a chip's
                                           ground is a category error the next reader would inherit.
                                           A brass action does not use it: it is already lit, so it
                                           presses by reversing its own gradient, which is geometry
                                           and needs no value at all */
  --bg-prompt-hover:var(--graphite-900);/* from: the logged-out comment prompt on hover */
  --bg-brand-mark:var(--bone-080);      /* from: the plate under the X and Apple marks */

  /* ---- text ---- */
  --text-primary:var(--bone-100);       /* from: 111 colour declarations, body downwards */
  --text-muted:var(--bone-500);         /* from: 93 colour declarations, every secondary line */
  --text-icon:var(--bone-300);          /* from: .ic, .ic-sm default stroke */
  --icon-quiet:var(--bone-500);         /* from: the unsaved bookmark, a glyph drawn as a fill and
                                           not as a stroke. Same value as --text-muted on graphite
                                           and a different one on chalk, which is the whole reason
                                           it is its own role */
  --icon-brass:var(--brass-400);        /* from: the saved bookmark. Brass as a mark, not as a word */
  /* `--text-icon-strong` STOOD HERE UNTIL 2026-08-13 AND IS GONE, backlog 131. It was drawn for the
     notification bell, which was the one FILLED mark in a row of stroked ones and was lifted a step
     so it did not read as heavier than its neighbours. The row is filled now, so the exception went
     with its reason and the role stayed: `header.css` still NAMED it in a `Reads:` header, and a
     header naming a role is a reader as far as a grep is concerned, which is exactly why a sweep
     that measured 0 dead tokens an hour earlier could not see it. **Zero readers in `components/`,
     in `ui-visual/` and in `wireframes/`**, and the only thing left drawing it was its own swatch
     on `colour.html`, which is a page showing a role that nothing wears. Deleted in both themes,
     with the swatch and its `.tk-c-*` class. */
  --text-chip-hover:var(--bone-100);    /* same value as --text-primary for now: a chip label reaching full ink .       From: the chip family on hover */
  --text-brass:var(--brass-400);        /* from: 37 colour + 8 stroke declarations: links, active
                                           nav, saved bookmark, the logo tick. Also the focus ring
                                           today; the focus role itself waits for the states stage */
  --text-brass-lit:var(--brass-200);    /* from: hero eyebrow, SEO tagline, trust badge. 58 placements */
  --text-brass-chip:var(--brass-250);   /* from: the active chip label. 219 placements */
  /* `--text-brass-vol:var(--brass-220)` stood here and went out on 2026-08-10, docs/backlog.md
     33. It was the volume tag on the featured hero and it had ONE placement in the product,
     `.hf-tag.vol`, which takes --text-brass-lit now: the same plate, the role that plate
     already uses. In graphite it was 10.08 against the eyebrow's 11.13, a step of 1.05 in
     contrast on the same ground; in chalk it was byte-identical to the other three. A role
     is a reason, not a value, and "it is on the hero" is a reason that already had a name. */
  --text-on-brass:var(--plum-950);      /* from: 15 selectors on brass CTAs, hardcoded until now */
  --text-on-yes:var(--green-950);       /* from: the filled YES side */
  --text-on-no:var(--red-950);          /* from: the filled NO side */
  --text-on-photo:var(--white);         /* from: dialog hero headings and their close mark */

  /* ---- edges ---- */
  --border-hairline:var(--graphite-750);   /* from: 89 border declarations */
  --border-field:var(--bone-500);          /* THE BOUNDARY THAT IS THE CONTROL, and it is a second
                                              role rather than a stronger hairline because the two
                                              answer different questions. A hairline SEPARATES two
                                              areas and 1.4.11 does not reach it; a field's edge is
                                              the only thing on the screen that says text may be
                                              typed here, so it is "visual information required to
                                              identify a user interface component" and owes 3:1.
                                              Measured from the render, not the token: on the
                                              hairline the amount field read 1.32 in graphite and
                                              2.11 in daylight against the surface it stands on.
                                              This step reads 5.49. docs/backlog.md 121. */
  --border-brass:var(--brass-a30);         /* from: the inset hairline of card, plate, thumb, badge */
  --border-brass-hover:var(--brass-a45);   /* from: the hover border of the quiet button family */
  --tint-brass-06:var(--brass-a06);        /* from: the quietest brass wash (funds-safety line) */
  --tint-brass-09:var(--brass-a09);        /* from: the condensed category chip when active */
  /* AND, SINCE 2026-08-07, THE HEADER BAND'S SKIN. Four controls stand in that
     band - the icon button (242 uses, all of them there), the notification and
     account <summary> disclosures and the How-it-works pill (105, likewise all
     there) - and measured at 1440 and 380 in both themes they are one skin to
     the byte: nothing at rest inside a --border-hairline pill, this tint on a
     --color-action edge under a pointer, --bg-pressed under a finger. Nowhere
     else in the product wears it; every other icon button carries a face.
     WHY A SKIN IS A TOKEN AND NOT A FILE. A fourth file drawing .icon-btn,
     summary and .btn would be three more file-slots on two atoms, which is what
     the atom map counts, and a shared CLASS is that file with a name on it: a face's
     rules live in its atom's file. So components/iconbtn.css, header.css and
     button.css each keep their own rule saying their control stands on the band,
     and the band says HERE, once, what it does to whatever stands on it.
     IT WAS OFF THE LADDER FOR AS LONG AS IT EXISTED, and the reason is the whole
     lesson. The wash was typed as color-mix(in oklab,var(--color-action) 14%,
     transparent) in all three files, and the ladder has mapped
     --brass-a14 to --brass-a16 since the scale was declared - but it matches
     TOKEN NAMES, and a value written longhand as a colour function is invisible
     to it. Three copies of a rung that is not on the ladder, hidden by their own
     spelling. Naming it is what made the tool able to see it, and the tool
     answered within the minute. */
  --tint-brass-16:var(--brass-a16);        /* from: a selected quick amount, and the header band hovered */
  --tint-brass-30:var(--brass-a30);        /* same value as --border-brass and --glow-brass for now:
                                              a fill, an edge and a glow at one depth of the metal .           From: the funds-safety border */
  --tint-brass-45:var(--brass-a45);        /* from: the price-moved border */
  --tint-brass-60:var(--brass-a60);        /* from: a selected outcome row */
  /* THE SECOND ALPHA LADDER IS GONE, AND WHAT LOOKED LIKE THE REST OF IT IS A
     DIFFERENT OPERATION. Backlog 14, closed 2026-08-10 by counting again.
     The row said 20 declarations built a colour with color-mix at 16 different
     percentages beside the declared --brass-a* one. Counted today across
     components/ and components/patterns/: **0 color-mix on --color-action ends
     in `,transparent)`**. Every one of them is a token now, which is what the
     six rungs above are for, and the note beside --tint-brass-16 records the
     three copies that were hidden by their own spelling.
     WHAT IS LEFT IS 34 MIXES AND 28 OF THEM END IN A SOLID: --bg-control (10),
     --border-hairline (7), --bg-pressed (4), --bg-surface (3), --color-action
     (2), --text-primary (1), --control-knob (1). **An alpha and a blend are not
     the same thing.** An alpha is one ink at a depth and it belongs on a ladder,
     because the ground underneath is whatever the element happens to stand on.
     A blend is a GROUND: brass mixed into --bg-control at 11 per cent is a
     tinted control, and the number that is right depends on the ground being
     tinted, so a shared ladder would be a rung fitted to one surface and applied
     to another. That is the same mistake as a media query reading the window
     while the layout gets the container.
     THE SIX THAT DO END IN `,transparent)` ARE THREE OTHER ROLES and none of
     them is a ladder: --scrim-photo at 34/55/72 is one gradient over a
     photograph, --outcome-yes at 55 is the odds bar's fade written twice at one
     value, and --result-won at 42 is a single dialog head. A ladder is a set of
     steps a reader has to choose between; three values inside one gradient are
     not steps. If a fourth placement ever wants one of them, it gets a token
     first, which is the rule that emptied this row. */
  --edge-groove-dark:var(--graphite-950);  /* from: 27 declarations, the recessed line */
  --edge-groove-light:var(--bone-a06);     /* from: 25 box-shadows, the lit lip of the same groove */

  /* ---- brand roles ----
     Two roles, one value today. The action colour and the trust colour are both
     brass now and have no reason to stay tied: an action can be re-toned without
     touching the trust language, and a trust tint can warm up without repainting
     every CTA. Split now, before a component reads the wrong one. */
  --color-action:var(--brass-500);      /* from: Confirm bet, Add funds, balance +, .auth-btn.primary,
                                           the active category tab, the unread badge.
                                           It read .btn-primary until 2026-08-03, a class no screen
                                           carried: even the provenance of a role can name a thing
                                           that is not in the product */
  --color-action-lit:var(--brass-300);  /* from: the second stop of every action gradient */
  --color-action-pressed:var(--brass-600); /* from: a brass ground held down. Only FLAT brass needs
                                           it: a two-stop action gradient presses by reversing its
                                           own angle, which reads the two roles above and adds no
                                           value at all, but the switch that is ON is one colour and
                                           has no angle to flip. Declared once for the same reason
                                           --color-action is: a mid-luminance metal reads on both
                                           stones, so section 3 does not move either of them */
  --color-trust:var(--brass-500);       /* from: the trust bar, the USDC 1:1 line, the push banner,
                                           the footer trust cards, the shield icon.
                                           Same value as --color-action today, different role */

  /* ---- outcome roles: the live bet side and the odds ---- */
  --outcome-yes:var(--green-500);            /* from: odds-bar fill, selected YES side */
  --outcome-no:var(--red-500);               /* from: odds-bar track, selected NO side */
  --outcome-yes-text:var(--green-300);       /* from: the quiet YES text on 15 selectors */
  --outcome-yes-fill:var(--green-a12);       /* from: the tinted YES button (spectator, not trader). The
                                                spectator's button gave the tint back on 2026-08-16; the
                                                readers left are the position row and the depth-table bar */
  --outcome-yes-line:var(--green-700);       /* from: the tinted YES border. Same date, same move: the pair
                                                takes --border-hairline now and position.css keeps this */
  --outcome-no-text:var(--red-300);          /* from: the quiet NO text on a button or chip */
  --outcome-no-figure:var(--red-300);        /* same value as --outcome-no-text and --result-lost-text
                                                for now. From: the NO figure, column head and market stat.
                                                Same job, one step brighter; the two merged into --red-300 in step 6 */
  --outcome-no-fill:var(--red-a12);          /* same value as --result-lost-fill for now. From: the tinted NO
                                                position row. It said "the tinted NO button" until 2026-08-16,
                                                and the button it named stopped being tinted that day: the
                                                readers left are position.css and market.css */
  --outcome-no-line:var(--red-700);          /* same value as --result-lost-line for now .             From: the tinted NO border */
  /* --outcome-yes-fill-soft and --outcome-no-fill-soft went on 2026-08-16 and this is the
     READER side of a zero, not the placement side. `yesno.css` was their only reader, at the
     compact pair in an outcome list, and that pair now rests on --bg-control like every other
     quiet control: nothing asks for the value, so the value is not a face waiting for markup
     to come back, it is rent on a decision already taken. They were --green-a12 and --red-a12,
     the same two alphas --outcome-yes-fill and --outcome-no-fill still carry for `position.css`
     and `market.css`, so no primitive left with them. */
  /* --outcome-*-fill-strong and --outcome-*-text-lit were retired on 2026-08-06 when
     backlog S49 was answered: the chosen side in the mobile dock is the solid
     --outcome-yes / --outcome-no the panel and the option list already used, so the
     lighter fill and its brighter ink had nothing left to paint. Four tokens, four
     values, and the orphan-token pass is what said they were orphans. */
  --outcome-line-yes:var(--green-200);       /* from: the YES line drawn over the hero photo, where
                                                the quiet green loses against the image */
  --outcome-line-no:var(--red-400);          /* from: the NO line on the same photo */
  --chart-line:var(--green-250);             /* from: the price line on the event chart */
  --chart-line-glow:var(--green-a42);        /* from: the glow under that line. Themed as a PAIR with
                                                --chart-line above it, because one rule reads both */

  /* ---- the categorical series ----
     from: the five lines and legend swatches of the multi-outcome chart. Until
     the light theme they were an array of five hex literals inside the page
     script on 13 screens: a whole palette the token file could not see, the
     gates could not check and no theme could reach. Declared as roles so the
     script can write var(--series-N) and the browser resolves it live.
     (The open question this block used to end on, that --series-1 was the YES
     green, is what the paragraph above answers: step 7 moved all five. The note
     outlived the defect by one pass, which is how a comment starts lying.)
     --series-5 GOT ITS FIRST READER IN A STYLESHEET on 2026-08-16: the hero chart's
     volume dot, label and dashed line, which had been drawing in the brand's brass.
     That is what the desaturated neutral is FOR - a series member that is not a
     candidate - and it is the reason this block exists at all, one level up: the
     first cut of the series wore the YES green and the lit brass, and the flagship
     chart was still wearing the second of those. It shares its value with the fifth
     candidate line of the multi-outcome chart, and the two never stand in one
     document: that chart is on the event detail and this one is on the feed. */
  --series-1:var(--cyan-400);
  --series-2:var(--blue-400);
  --series-3:var(--violet-400);
  --series-4:var(--magenta-400);
  --series-5:var(--slate-400);

  /* ---- result roles: the market already resolved ----
     Separate from the outcome pair on purpose. A live YES and a settled WON are
     the same green today and answer to different design questions: the outcome
     pair belongs to the odds and the bet side, the result pair belongs to the
     record (WON / LOST chips, the win overlay, the resolved history). Either can
     move without dragging the other. */
  --result-won:var(--green-500);        /* same value as --outcome-yes for now: a market saying YES
                                           and your bet paying out are two facts, not one .                From: the win overlay glow and its reconcile box */
  --result-won-text:var(--green-300);        /* from: .pos-won, the win figure */
  --result-won-fill:var(--green-a12);        /* from: the WON chip */
  --result-won-line:var(--green-700);        /* from: the WON chip border */
  --result-won-stone:var(--green-975);       /* from: the win sheet head, green-tinted graphite */
  --result-lost-text:var(--red-300);         /* from: .pos-lost, the loss figure */
  --result-lost-fill:var(--red-a12);         /* from: the LOST chip */
  --result-lost-line:var(--red-700);         /* from: the LOST chip border */
  /* the loss overlay itself carries NO red: neutral graphite, by voice principle 4
     (mark the win, design the loss). That is why there is no --result-lost-stone */

  /* ---- feedback ---- */
  --border-notice:var(--bone-700);      /* from: the neutralised error toast, warm grey, never red */
  --bevel-notice:var(--white-a06);      /* from: the same toast, its inset lip */
  /* The scrollbar thumb, and it is a ROLE because one value cannot serve both
     themes: every scroller in this product sits on the plate, which is
     rgb(18,20,23) on graphite and rgb(248,246,240) on chalk, and a colour that
     is quiet on one is invisible on the other. Measured against the plate:
     3.43:1 here, 3.39:1 in daylight, both over the 3:1 a non-text control owes.
     The track is transparent on purpose, so the thumb is the only thing drawn
     and the ground behind it is what it is measured against. */
  --scroll-thumb:var(--bone-650);       /* from: every scroll container, base.css */

  /* ---- depth and material ----
     The embossed two-stone system is built from light and shade, and light is a
     colour role like any other: invert the theme and every highlight becomes a
     shadow. So the bevel and shadow inks live here, not as literals in a card. */
  --bevel-hi:var(--white-a16);          /* from: the plate top lip, strongest */
  --bevel-md:var(--white-a16);          /* same value as --bevel-hi and --bevel-lit for now: three
                                           lips that happen to catch the light equally .             From: the raised plate */
  --bevel-lit:var(--white-a16);         /* from: the dialog lip */
  --bevel-strong:var(--white-a20);      /* from: an action on a photographic head */
  --bevel-lo:var(--white-a10);          /* from: the win and loss sheet head */
  --bevel-xs:var(--white-a10);          /* from: the inner plate */
  --bevel-faint:var(--white-a06);       /* from: the quiet blocks */
  --bevel-hair:var(--white-a04);        /* from: the left lip of a plate, a card and a trust item.
                                           There used to be a --bevel-hair-soft beside it, described
                                           as "the same, one step quieter" and identical in both
                                           themes: a role that never had a reason of its own, which
                                           is the one kind this file says to delete */
  --edge-shade:var(--black-a30);        /* from: the dark side of an emboss */
  --edge-shade-strong:var(--black-a45); /* from: the bottom edge of a lifted sheet */
  --shadow-ink:var(--black);            /* from: every drop shadow of the product */
  --shadow-ink-45:var(--black-a45);     /* same value as --edge-shade-strong for now: an edge and the lightest drop .        From: the card and plate depth stack, lightest */
  --shadow-ink-60:var(--black-a60);     /* from: the dialog */
  --shadow-ink-72:var(--black-a72);     /* from: a card on hover */
  --shadow-ink-85:var(--black-a85);     /* from: the big drop under a card, a plate and a dialog */
  --scrim:var(--black-a30);             /* from: the mobile drawer backdrop */
  /* the disc behind a close mark on a photographic head. It was reading
     --shadow-ink, which is the ink of a drop shadow: two jobs on one role, and
     the light theme is what showed it. A shadow follows the theme, a scrim over
     a photograph does not, because the photograph is dark in both */
  --scrim-photo:var(--black);
  /* the stops of a mask. A mask keeps only the alpha of the colour it is given,
     so these are not colours at all: they are "show it" and "half show it", and
     a theme must never move them. Nine masks were reading --shadow-ink, which is
     opaque in the Vault by coincidence and 32% ink in daylight, so every masked
     photograph came out washed. Same shape of mistake as --scrim-photo. */
  --mask-solid:var(--black);                 /* same value as --scrim-photo, and for opposite reasons: one is a colour that must not move, the other is not a colour at all */
  --mask-mid:var(--black-a45);
  --surface-grid:var(--brass-grid);     /* from: the brass graph grid on the card corner */
  /* the grain of the stone is material, and material is what a theme moves: the
     same noise that grips graphite washes out a pale stone. It was read straight
     from the primitive by 11 component files until the light theme asked for it */
  --surface-grain:var(--grain-fine);        /* from: card, plate, dialog, hero, dock, trust item */
  --surface-grain-coarse:var(--grain-coarse); /* from: the outer slab */
  --mark-logo:var(--logo-y);            /* from: the brand mark before the wordmark, components/logo.css */
  --glow-brass:var(--brass-a30);        /* from: the card and chip glow */
  --glow-focus:var(--brass-a16);        /* from: the focus ring on a field */
  --focus-ring:var(--brass-400);        /* same value as --text-brass for now: the ring is not a word,
                                           and the day a states pass gives focus its own colour the
                                           links must not follow it .           From: :focus-visible, declared once in base.css.
                                           MEASURED in the browser, not computed from the hex, and
                                           RE-measured on 2026-08-02 after the first pass got it wrong:
                                           every focusable control on 153 pages, 179 kinds, the ring
                                           composited over the ground it stands on. 6.67:1 at worst,
                                           9.06:1 at best. A ring answers to 3:1.
                                           The first numbers here (6.98, 7.80) came from a sweep that
                                           read the ground with a regex, and a regex reads the components
                                           of color-mix(in oklab, ...) as sRGB bytes, so a pale banner
                                           measured as near black. The browser parses the colour now: a
                                           canvas resolves any css colour string to bytes */
  --tint-hover:var(--white-a06);        /* from: the quiet hover wash on a chip (cat-nav condensed,
                                           feed subfilter). It was reading --bevel-faint, which is
                                           the LIT LIP of an emboss: the same value on graphite and
                                           nothing at all on chalk, where more light is not available */
  --line-grid:var(--white-a06);         /* same value as --tint-hover for now: a line on a chart and
                                           a wash under a cursor, which move apart the moment either
                                           needs to .            From: the chart grid, same story: a line, not a lip */
  --line-brass-strong:var(--brass-a60); /* from: the input hover border. One rung above
                                           --border-brass-hover, which is what makes it its own
                                           role; section 3 keeps that step, and did not until
                                           2026-08-12 */
  --bg-dock-from:var(--graphite-860);   /* from: the mobile bet dock gradient */
  --veil-photo-edge:var(--graphite-a12);/* from: the featured hero veil, transparent end */
  --veil-photo-from:var(--graphite-a50);/* from: the same veil, where it starts to bite */
  --veil-photo-mid:var(--graphite-a55);/* from: the same veil, mid stop */
  --veil-photo-to:var(--graphite-a82);  /* from: the same veil, where the text sits */
  --veil-brand:var(--graphite-a28);     /* from: the brand tile over its photograph */
  --line-on-photo:var(--white-a16);     /* from: the close mark on a photographic dialog head */
  --line-on-photo-hover:var(--white-a32);  /* from: the same close mark, hovered */
  --control-knob:var(--white);          /* same value as --text-on-photo for now: both sit on something that does not theme .             From: the toggle knob when it is on */
  --text-strong:var(--white);           /* from: the label of a quiet button on hover */

  /* ---- course chrome (the roadmap sidebar every course page carries).
     Not product. Kept apart so a product theme never has to think about it. ---- */
  --chrome-bg:var(--graphite-910);
  --chrome-border:var(--graphite-720);
  --chrome-text:var(--bone-250);
  --chrome-muted:var(--bone-600);
  --chrome-accent:var(--brass-400);
  --chrome-pressed:var(--graphite-860); /* from: 5 rows of the panel while they are held down. The
                                           product's --bg-pressed is the same step and cannot be
                                           borrowed here: it is overridden in section 3 and the panel
                                           is not, so on a daylight page a shared role would press a
                                           graphite row to chalk. A chrome role is one that section 3
                                           deliberately does not reach, and this is one */
  /* the two swatches on the theme switch. They are roles, not primitives read
     raw, and section 3 deliberately does not move them: a control that shows you
     the two grounds has to keep showing both, whichever one you are standing on */
  --swatch-dark:var(--graphite-930);    /* from: the two swatches on the theme switch itself */
  --swatch-light:var(--chalk-930);    /* from: the pale half of the theme switch */

  /* ---- states: the two ways a control stops taking input ----
     They are two roles and not one, and the markup is what decides it. A field
     you cannot type into is UNAVAILABLE and may go quiet. The Necessary cookie
     category is <input type="checkbox" checked disabled>: it is not unavailable,
     it is LOCKED ON, and the whole message is that it is on, so muting it to the
     point where the tick is hard to read would mute the answer rather than the
     control. One number for both would have taken the second to .45 and made a
     consent screen say less. */
  --opacity-disabled:.45;  /* from: 3 rules, the amount field in the dialog and in the bet panel,
                              and the toggle switch that cannot be flipped. Those three carried
                              .45, .45 and .42, which is drift and not two decisions: nothing in
                              the product distinguishes a field you cannot type in from a switch
                              you cannot flip. Renders at 3.79:1 against the ground behind it here */
  --opacity-locked:.7;     /* from: 1 rule, the checkbox of a cookie category that cannot be
                              turned off. Higher on purpose: its value is the message. A role with
                              one consumer is normal in this file (--bg-prompt-hover has one too);
                              a role that means two things is not */
}

/* ==========================================================================
   3. THEME - the Vault in daylight
   ==========================================================================

   The product is dark, so its theme is a LIGHT one. The attribute says what it
   is, not what the exercise was called: [data-theme="light"].

   This block is the proof that the two levels are worth their cost. A rebrand
   would not prove it: swapping brass for cobalt is a change of primitive, and it
   works fine on a flat file with no roles at all. A theme is different. The
   ground inverts, the ink inverts, the light and the shade swap places, and the
   ACTION has to stay the action through all of it. That is a sentence you can
   only write if the reason a colour is there is stored somewhere, and here it is
   stored in the role name.

   Three rules held while writing it:

   1. Only roles are overridden. Not one primitive is redefined, so the dark
      palette above still says exactly what it said. The values daylight needs
      are declared as their own primitives at the end of section 1.
   2. Not everything inverts, and the roles that do not are the interesting
      ones. --text-on-photo stays white and --scrim-photo stays black, because a
      white glyph and the disc behind it sit straight on the photograph, and a
      photograph does not get lighter. Brass stays the brand metal:
      --color-action does not move at all, because a mid-luminance metal reads on
      both stones. What moves is brass as INK.
      The veils looked like they belonged in that list and do not, which is the
      sharpest thing the theme taught. A veil is not "a dark colour over a
      picture", it is the layer that guarantees the words, so it follows the INK
      and not the photograph: when the ink went dark the veils had to go pale, or
      the featured card reads dark grey on dark grey. --scrim-photo and
      --veil-photo-* look like one idea and are two.
   3. Contrast is measured, not assumed. Every text pair below was computed
      against every ground it can land on; the number after a role is its worst
      case. AA is 4.5 for text and 3.0 for a line or an icon.

   Where the light theme is honestly weaker: on graphite the muted text sits at
   6.1:1 and the brass link at 7.8:1, which no pale ground can match while
   staying brass. Daylight clears AA; it does not clear it by the same margin.

   No em dash. */

[data-theme="light"]{

  /* ---- surfaces. The reflection, step for step; the rule is written above the
     chalk ramp in section 1. The order of depth is preserved and its DIRECTION
     inverts: on graphite a card rises off the page toward the light, here it
     settles onto it. So the page is the lightest thing on screen, because the
     thing it answers to is the darkest thing on screen ---- */
  --bg-page:var(--chalk-930);
  --bg-canvas:var(--chalk-900);

  --bg-slab:var(--chalk-860);
  --bg-slab-from:var(--chalk-910);      /* presence, not depth: this one is the app header */
  --bg-slab-to:var(--chalk-870);

  --bg-plate:var(--chalk-910);
  --bg-plate-from:var(--chalk-870);
  --bg-plate-to:var(--chalk-930);

  --bg-card:var(--chalk-900);
  --bg-card-from:var(--chalk-870);
  --bg-card-to:var(--chalk-920);
  --bg-card-quiet:var(--chalk-880);

  /* ---- and here the reflection stops. See the note under it ---- */
  --bg-surface:var(--chalk-910);
  --bg-control-brass:color-mix(in oklab,var(--color-action) 20%,var(--bg-control));
  --border-hairline-brass:color-mix(in oklab,var(--color-action) 30%,var(--border-hairline));
  --bg-control:var(--chalk-930);
  --bg-control-hover:var(--chalk-920);
  --bg-chip:var(--chalk-900);
  --bg-pressed:var(--chalk-880);
  --bg-dialog-head-from:var(--chalk-920);

  --bg-well:var(--chalk-940);
  --bg-chart-well:var(--chalk-940);
  --bg-dock:var(--chalk-880);
  --bg-dock-from:var(--chalk-860);
  --bg-prompt-hover:var(--chalk-900);
  /* WHY THOSE SIX LEAVE THE REFLECTION. On graphite, lightness carries two jobs at once: how deep a
     stone sits, and how far forward an object stands. A reflection can only invert one of them.
     Depth inverts, and that is what makes daylight the Vault rather than a generic light theme: a
     card settles onto the page instead of rising off it. Presence cannot, because the Vault spends
     6 to 11 L* lifting a control off its page and daylight has 1.7 L* of room above white. Reflect
     it and the most present thing in the system becomes the most buried: the header, the dropdown
     and every pill land 7 to 11 L* under the page, which on a pale ground does not read as stone,
     it reads as dirt, and it is the exact grey the grey-box wireframes were drawn in.
     So the six roles that carry presence sit at the top of the ramp, in the Vault's own order and
     the Vault's own direction (a control lighter than a panel, a hover darker than its rest, a
     pressed chip deeper than an idle one) and the EDGE carries what the fill used to. That trade is
     only available here: daylight's hairline runs at 2.2:1 against its surface where the Vault's
     runs at 1.1:1, so on chalk an edge can hold a shape and on graphite it cannot.
     Area is the tell. A chip 6.5 L* under white is a quiet pill; a header band the same 7 L* under
     white is a dirty field. Depth is read against how much of the screen it covers. */
  --bg-brand-mark:var(--ink-900);       /* NOT a plate, whatever the name in section 2 says: it is
                                           the colour of the X and the Apple mark themselves, and a
                                           glyph on a pale button has to be ink. 13.9:1 */

  /* ---- text. Worst case is the deepest ground the role can land on. That used
     to be --bg-control; once the presence roles left the reflection the deepest
     fill in daylight is the outer slab at L* 93.4, so every number here rose
     again ---- */
  --text-primary:var(--ink-900);        /* 13.9:1 */
  --text-muted:var(--ink-500);          /* 6.8:1 */
  --text-icon:var(--ink-400);           /* 4.8:1, and an icon is a line */
  --icon-quiet:var(--ink-300);          /* 4.1:1 worst case, and a filled glyph is neither a line nor
                                           a word: the bar it answers to is 3:1 */
  --icon-brass:var(--brass-600);        /* 3.2:1 */
  --text-chip-hover:var(--ink-900);     /* 13.9:1 */
  --text-brass:var(--brass-700);        /* 5.3:1 over the deepest brass tint, 6.5:1 on bare stone */
  --focus-ring:var(--brass-700);        /* 5.71:1 to 7.46:1 across every ground it reaches, on the same
                                           153 pages. The 5.3:1 written here before this stage was
                                           computed against one reference ground rather than the ones the
                                           ring actually stands on, and the 6.96 to 7.40 that briefly
                                           replaced it came from the regex sweep described above.
                                           ONE GROUND USED TO FAIL AND IT WAS NOT THE PRODUCT'S: the
                                           course roadmap sidebar stays graphite in daylight on purpose, so
                                           a ring that followed the theme landed on it at 2.39:1 against a
                                           3:1 floor. Fixed in course-chrome.css on 2026-08-02, where the
                                           theme switch had already solved it: the panel now takes
                                           --chrome-accent, which section 3 does not override, and measures
                                           8.71:1 in BOTH themes. This role no longer reaches that panel */
  /* THREE ROLES, ONE VALUE, AND THAT IS THE ANSWER RATHER THAN THE DEFECT. docs/backlog.md 33
     filed this as a ladder with four rungs in the Vault and one in daylight, and asked whether
     the ladder is real in both themes or one role with aliases. Measured over the 106 painted
     screens in both themes: in graphite the four resolve to four values and stand at 5.70 to
     8.98, 11.13 to 11.33, 9.86 to 11.79 and 10.08 against their REAL composited grounds; in
     chalk they resolve to one value on 704 placements, worst 5.24 and best 7.40. The steps in
     graphite are not a hierarchy of emphasis, they are brass ink compensating for four
     different grounds - a brass-tinted chip, a photograph under a veil, a plate, bare stone -
     and on chalk one value clears every one of those grounds with 0.74 to spare over the 4.5
     floor. So it is neither a broken ladder nor three aliases: it is one ink with per-ground
     compensations that the pale stone does not need. Two roles may share a value and each says
     so where it is declared, which is the rule twenty-seven other groups in this file already
     live under. The fourth name is gone; it had one placement. */
  --text-brass-lit:var(--brass-700);    /* 5.3:1 on the deepest brass tint, 7.40 on bare stone */
  --text-brass-chip:var(--brass-700);   /* 5.3:1, and 5.24 is the worst of all 704 in daylight */
  --text-strong:var(--ink-900);         /* 13.9:1. White on graphite, ink on chalk: the label
                                           of a quiet button when it is hovered */
  /* --text-on-brass, --text-on-yes, --text-on-no and --text-on-photo are NOT
     here. Their ground is a brass fill, a green fill, a red fill and a
     photograph, and none of those four moved. */

  /* ---- edges. The hairline is the clearest inversion in the file: on graphite
     it is LIGHTER than its surface, on chalk it has to be darker ---- */
  --border-hairline:var(--chalk-750);   /* 2.0:1 on the outer slab, 2.2:1 on the page. A hairline is not
                                           held to AA: it separates two stones, it does not carry a
                                           word. The Vault's own runs at 1.1:1 */
  --edge-groove-dark:var(--chalk-750);
  --edge-groove-light:var(--white-a70);
  /* EVERY brass tint steps up one rung, and until 2026-08-12 "every" was six of
     seven roles rather than all of them: the same alpha over a pale stone reads
     about a third as strongly as it does over graphite, and the number under that
     sentence is now beside --brass-a75 in section 1 (a rung is 1.08 to 1.10 of
     edge contrast here against 1.29 to 1.32 there). The rule only holds while the
     ladder has a rung above the loudest role, which is what --brass-a75 is for. */
  --border-brass:var(--brass-a45);
  --border-brass-hover:var(--brass-a60);
  --tint-brass-06:var(--brass-a09);
  --tint-brass-09:var(--brass-a16);
  /* THIS RUNG IS WHERE THE HEADER BAND'S HOVER ARRIVED ON 2026-08-07, and the
     step it takes here is the point of the whole ladder. The band wash used to
     be a flat 14 per cent in both themes because it was typed as a colour
     function rather than named, and a flat alpha does not read the same on two
     stones: measured against its own band it separated at 1.27:1 on graphite and
     1.11:1 on chalk. On this rung it is 1.33 and 1.25, so the two themes answer
     a pointer with the same strength for the first time. The dark band moves 3
     units of sRGB to get there, which is under this ladder's own declared
     resolution; the light band moves visibly, and that is the correction. */
  --tint-brass-16:var(--brass-a30);
  --tint-brass-30:var(--brass-a45);
  --tint-brass-45:var(--brass-a60);
  /* THE TOP OF THE LADDER, AND THE TWO ROLES THAT USED TO HAVE NO LIGHT VALUE AT
     ALL. Both were --brass-a60 in section 2 and unmentioned here, so daylight
     inherited the graphite number in silence, which is the one thing a theme block
     exists to prevent. Measured on ui-visual/event-detail-multi.html at 1440 in
     both themes, the rendered edge read from a screenshot at device scale 4:
       .opt-row.sel [--tint-brass-60] vs .chip-amount.sel [--tint-brass-45]
         graphite  edge/ground 3.749 and 2.759, the two edges 1.445 apart
         chalk BEFORE          1.759 and 1.682, the two edges 1.048 apart, and the
                               .048 was the two GROUNDS, not the ink: both roles
                               resolved to the same rgba(199,162,78,.6)
         chalk AFTER           1.927 and 1.682, the two edges 1.148 apart
       .amount-input:hover [--line-brass-strong] vs .btn-secondary:hover
       [--border-brass-hover], measured on ui-visual/deposit.html with the sheet open
         graphite  edge/inside 3.519 and 2.388, one rung apart as designed
         chalk BEFORE          1.632 and 1.593, a tie the input was meant to win
         chalk AFTER           1.852 and 1.593
     --line-brass-strong is one rung louder than --border-brass-hover on purpose:
     a field under a pointer says more than a quiet button does. It said it in the
     Vault only, on 105 screens, for as long as the light theme existed. */
  --tint-brass-60:var(--brass-a75);
  --line-brass-strong:var(--brass-a75);
  --glow-brass:var(--brass-a45);
  --glow-focus:var(--brass-a45);
  --tint-hover:var(--ink-a10);          /* on chalk a hover reaches for the ink, not for the light */
  --line-grid:var(--ink-a10);

  /* ---- outcome. The fills and the lines carry over: a 12% green wash and a
     mid green border work on either stone. Only green and red as INK move ---- */
  --outcome-yes-text:var(--green-800);      /* 6.1:1 over the YES fill, 5.7 over the strong one */
  --outcome-no-text:var(--red-800);         /* 6.2:1 over the NO fill, 5.6 over the strong one */
  --outcome-no-figure:var(--red-800);       /* 5.3:1 */
  --chart-line:var(--green-700);            /* the bright chart green disappears on chalk */
  --chart-line-glow:var(--green-a42-lit);   /* ONE RULE READS BOTH OF THESE, chart.css line 25, so a
                                               theme that moves one and not the other paints a halo in
                                               a hue the line no longer has. That is what daylight did
                                               on 13 chart screens: a dark green line under a mint
                                               halo. A pair is themed as a pair */
  /* the series is text as well as line: the reading under the chart is drawn in
     the selected series colour, so all five clear 4.5 on the chart well */
  --series-1:var(--cyan-700);               /* 6.1:1 */
  --series-2:var(--blue-700);               /* 6.9:1 */
  --series-3:var(--violet-700);             /* 7.3:1 */
  --series-4:var(--magenta-700);            /* 6.2:1 */
  --series-5:var(--slate-700);              /* 7.3:1 */
  /* --outcome-line-yes and --outcome-line-no stay: they are drawn straight on
     the hero photograph, above the veil, and a line is not text. */

  /* ---- result ---- */
  --result-won-text:var(--green-800);   /* 5.3:1 over the WON chip */
  --result-lost-text:var(--red-800);    /* 5.3:1 over the LOST chip */
  --result-won-stone:var(--green-050);

  /* ---- feedback ---- */
  --border-notice:var(--chalk-750);
  --border-field:var(--ink-300);        /* 4.43:1 on the page and the sheet. The graphite half argues it. */
  --bevel-notice:var(--white-a70);
  /* One step lighter than the graphite thumb, not darker, which is the whole
     argument for the role: chalk needs LESS ink than graphite needs light. */
  --scroll-thumb:var(--bone-600);

  /* ---- depth. Light and shade swap weight. On graphite the emboss is carried
     by the lit lip and the shade only steadies it; on chalk the lip has to be
     nearly opaque white to register at all, and the shade carries the block.
     The first cut read that as "drop the whole shade ladder", and the blocks
     went flat. The correct reading is that a 1px inset EDGE needs the quiet end
     of the ladder while a blurred DROP needs the loud one, so they are set
     apart here instead of scaled together ---- */
  --bevel-hi:var(--white-a70);
  --bevel-md:var(--white-a70);
  --bevel-lit:var(--white-a70);
  --bevel-strong:var(--white-a70);
  --bevel-lo:var(--white-a32);
  --bevel-xs:var(--white-a32);
  --bevel-faint:var(--white-a20);
  --bevel-hair:var(--white-a20);
  --edge-shade:var(--ink-a10);          /* a 1px inset edge: the quiet end of the ladder */
  --edge-shade-strong:var(--ink-a16);
  --shadow-ink-45:var(--ink-a16);       /* the card and plate BORDER, not a shadow */
  --shadow-ink:var(--ink-a32);          /* a tight shadow with heavy negative spread */
  --shadow-ink-60:var(--ink-a24);
  --shadow-ink-72:var(--ink-a32);
  --shadow-ink-85:var(--ink-a44);       /* the big drop under a card: the loud end */
  --scrim:var(--ink-a44);

  /* ---- the veils. They follow the ink, not the photograph: see rule 2 ---- */
  --veil-photo-edge:var(--chalk-a12);
  --veil-photo-from:var(--chalk-a55);
  --veil-photo-mid:var(--chalk-a62);
  --veil-photo-to:var(--chalk-a90);
  --veil-brand:var(--chalk-a35);

  /* ---- material ---- */
  --surface-grid:var(--brass-grid-lit);
  --surface-grain:var(--grain-fine-lit);
  --surface-grain-coarse:var(--grain-coarse-lit);
  --mark-logo:var(--logo-y-dark);

  /* ---- states ----
     The numbers are the same as on graphite and they are written out anyway,
     which is the whole reason this pair is a role. A token declared once in
     section 2 would be inherited here in silence, and silence is indistinguishable
     from a decision. This IS the decision, and it was measured before it was
     made: the same .45 lands at 3.79:1 on graphite and 2.48:1 on chalk, because
     an opacity blends toward the ground and the ground is what a theme moves.
     Daylight mutes harder for the same number. It is kept anyway, because a
     disabled control is exempt from the contrast minimum by WCAG 1.4.3 and the
     alternative is two numbers that make the same control look like two
     different states depending on the hour. Recorded rather than tuned: the day
     this becomes a complaint, the number to change is here and so is its cost. */
  --opacity-disabled:.45;  /* 2.48:1 on chalk against the field behind it */
  --opacity-locked:.7;     /* the locked checkbox, same reasoning as above */

  /* The course chrome is deliberately absent. It is the frame the stage is shown
     in, not the product, and section 2 already keeps it apart for exactly this
     reason: a product theme should not have to think about the roadmap sidebar. */
}
