/* ============================================================================
 * tokens.css — Team Portal design tokens (Phase 1 Task 1)
 *
 * Source of truth: window.dcTok in the Claude Design handoff bundle
 * (project/redesign/icons-and-mockups.jsx) + the metrics catalogued in
 * docs/research/design-analysis/design-system-impl.md SS2.
 *
 * SUPERSEDED IN PART, 2026-08-14 — the cohesion boards at
 * docs/design/2026-08-14-cohesion-artboards/ are the current conformance
 * baseline for the type ladder, the greys, the scrim, the shadow ladder, the
 * layering order and the radii. Where dcTok and those boards disagree, the
 * boards win (PASS-3-RULINGS.md is the settled system; PASS-2-CONFLICTS.md
 * records where pass 2 re-opened a pass-1 value). dcTok still governs
 * everything the boards do not name.
 *
 * These custom properties are a PUBLIC API: every later component stylesheet
 * consumes them. Do not rename or retune values without a design-delta entry;
 * tests/test_design_tokens.py pins the canonical values, and
 * tests/styleguide/test_cohesion_token_ladder.py holds the ladder LAWS (five
 * weights, em-only tracking, one scrim, and — since §9.6 — the 26-role weight
 * matrix, the rule that NO RUNG OFFERS A MENU, and the ban on 800 anywhere in
 * the portal) plus the self-expiring registers that retire a deprecated alias.
 *
 * A DEPRECATED ALIAS in this file holds a var() read of its successor instead of
 * a value, with a dated comment naming what it used to be. (Written out longhand
 * rather than as a worked example, because the drift guard parses this file for
 * declarations and a specimen alias in a comment reads as a real, unpinned
 * token — which is precisely the guard doing its job.) It exists so a rename
 * cannot break a
 * consumer mid-migration; it is NOT a second name you may reach for. Every
 * existing read is pinned by selector in the guard above, so the list can only
 * shrink — and when one empties, the guard tells the next session to delete the
 * alias.
 *
 * The portal is LIGHT-ONLY. No dark-theme overrides belong in this file or
 * any portal stylesheet (UCheck PWA and RAPID are separate, sanctioned
 * exceptions that do not live here).
 * ========================================================================= */

/* ---- Self-hosted fonts (woff2 only; never the trial Industry fonts) ------
 * Inter ships as the variable font: one file covers the full 100-900 weight
 * axis (the portal uses 400-800). JetBrains Mono ships as static weights
 * 400/500/600 because the upstream release provides no variable woff2. */

@font-face {
  font-family: 'Inter';
  src: url('../fonts/InterVariable.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'JetBrains Mono';
  src: url('../fonts/JetBrainsMono-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'JetBrains Mono';
  src: url('../fonts/JetBrainsMono-Medium.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'JetBrains Mono';
  src: url('../fonts/JetBrainsMono-SemiBold.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

/* ---- Base reset ------------------------------------------------------------
 * Zero the browser's default 8px body margin. tokens.css is the one sheet every
 * page loads (portal shell + the standalone login/activate/reset/onboarding
 * surfaces), so the reset belongs here — a full-viewport page must reach every
 * edge with no white gutter and no 16px of phantom scroll. */
body {
  margin: 0;
}

/* Border-box everywhere. Lives HERE (not components.css) because the standalone
 * login/activate/reset/set-pin/onboarding surfaces link tokens.css but NOT
 * components.css — without the reset they rendered content-box, so width:100%
 * cards + padding overflowed the viewport (horizontal scroll; the session-
 * expired Sign-in button spilled past both phone edges). Audit A1/A2/A3. */
*,
*::before,
*::after {
  box-sizing: border-box;
}

:root {
  /* ---- Palette (dcTok) -------------------------------------------------- */
  /* THE ELEVEN GREYS (§9.1, ratified by §9.5 — the whole neutral system).
   * The 2026-08-14 cohesion audit measured **229 distinct near-greys** in the
   * bundle, and **218 of them are one shade off a token that already existed**.
   * The eleven below are the complete set; a twelfth near-grey is drift, not a
   * value. Two hairline jobs and only two:
   *   --tp-hairline   (#E5E8EE) — the CONTAINER EDGE
   *   --tp-hairline-2 (#EEF1F5) — EVERY internal rule
   * Anything that is neither of those jobs is not a hairline. */
  --tp-bg: #F7F8FA;              /* 1  page background */
  --tp-surface: #FFFFFF;         /* 2  cards / rail / topbar / inputs */
  --tp-sunken: #FAFBFD;          /* 3  a recessed band INSIDE a surface (table head, group head) */
  --tp-wash: #F2F4F8;            /* 4  a neutral wash ON a surface (mono chip, inline fill) */
  --tp-hairline: #E5E8EE;        /* 5  the container edge */
  --tp-hairline-2: #EEF1F5;      /* 6  every internal rule; neutral soft fill */
  --tp-ink: #0F1722;             /* 7  primary text; primary button fill */
  --tp-ink-2: #384153;           /* 8  secondary text; neutral badge solid */
  --tp-muted: #6B7385;           /* 9  tertiary text / icons / eyebrows */
  --tp-disabled: #9AA2B2;        /* 10 disabled text and glyphs — the ONE disabled grey.
                                        NEW 2026-08-14: the eleventh grey had no token, so
                                        every disabled surface picked its own near-#9AA2B2
                                        by eye. shell.css:596-597 already names the hex in a
                                        comment and substitutes --tp-muted for want of this. */
  --tp-avatar-bg: #D9DEEA;       /* 11 the NEUTRAL avatar fill — and half of a
                                        COUPLE: its ink is --tp-avatar-ink, in the
                                        avatar role ramp below. It keeps this
                                        historic name because it is read widely.
                                        The ramp is NOT part of this neutral set;
                                        read its header before adding to either. */
  --tp-brand: #2456E6;           /* links/selection/focus/active nav — NEVER button fills */
  --tp-brand-soft: #E7EDFE;

  /* ---- Status tones (solid / soft pairs) -------------------------------- */
  --tp-info: #2456E6;
  --tp-info-soft: #E7EDFE;
  --tp-warn: #B6651A;
  --tp-warn-soft: #FCEFDC;
  --tp-danger: #B42318;
  --tp-danger-soft: #FBE5E2;
  --tp-ok: #1F7A4D;
  /* okSoft normalized to the dcTok value; the slightly-different green seen
   * in some mockups is drift (see tests/test_design_tokens.py). */
  --tp-ok-soft: #DDF1E5;
  /* Fifth badge tone: neutral = ink-2 solid on hairline-2 soft. */
  --tp-neutral: #384153;
  --tp-neutral-soft: #EEF1F5;

  /* ---- Tone BORDERS and the warn INK (2026-08-16, cohesion Stage 3) ------
   * Adopted from the Stacks Design System import
   * (docs/design/2026-08-16-stacks-design-system/RECONCILIATION.md § (a)),
   * which found this app's tone system HALF-BUILT: every tone shipped as a
   * base + a soft fill, with no edge and no on-soft ink.
   *
   * WHAT WAS ACTUALLY IN THE TREE, which is why these have real consumers
   * rather than being a suggestion. Fourteen rules hand-derived a tone edge
   * with `color-mix`, at NINE different percentages — warn at 20, 22, 28, 30,
   * 33, 45 and 55; ok at 50; danger at 20, 44 and 50 — each one an author
   * eyeballing "a tinted edge" because no value existed to read. That is the
   * same fragmentation the eleven greys and the eight type rungs end, one
   * family later. Three names replace nine guesses.
   *
   * --tp-warn-ink IS A CONTRAST FIX, not a shade. `--tp-warn` on
   * `--tp-warn-soft` computes 3.81:1 — legal for a glyph, and BELOW SC 1.4.3's
   * 4.5 floor for text, which a 12/600 chip label is. It was registered as
   * debt in tests/styleguide/test_chip_family_and_card.py with two faces
   * SKIPPED, and §9.2 · 02's own chipTones data already drew the ink as
   * #8A4F14. This is that value arriving: 5.77:1 on the soft fill, and 6.54:1
   * under white when it carries the SOLID face, which was the other failing
   * one at 4.32:1.
   *
   * THE BASE TONE DOES NOT MOVE. `--tp-warn` stays #B6651A — both design
   * systems agree on it, and it is still correct for a glyph, a dot and a
   * border, where the 4.5 text floor does not apply. What arrives is the
   * DARKER ink for the faces that carry TEXT.
   *
   * DELIBERATELY NOT ADOPTED, and each for the same reason a token with no
   * consumer is a suggestion (D-216(2)): `--stk-ok-text` / `-warn-text` /
   * `-danger-text` (the app sets --tp-ink / --tp-ink-2 on toned notices, and
   * changing that is a taste call across every notice in the portal, not a
   * defect), `--stk-brand-strong` (a link-hover ink; this portal's links do
   * not change colour on hover), and `--stk-danger-disabled` (one consumer,
   * already expressed as a color-mix within 6/255 of the board value). */
  --tp-ok-border: #BFE4D0;
  --tp-warn-border: #F1D7A8;
  --tp-danger-border: #F3C4BE;
  --tp-warn-ink: #8A4F14;
  --tp-brand-soft-border: #C6D4FA;

  /* ---- The MATCH pair — a SEARCH-MATCH highlight, not a tone (LS-A7) ------
   * Board: docs/design/2026-08-16-damage-claims-redesign/damage-claims-deep.jsx,
   * board `list-filters` — the live-results dropdown at lines ~80-151 draws
   * `background:'#FFEEC2'` on the matched span of a result row (line ~117), and
   * the table rows behind it that correspond to a rendered hit carry `hi: true`
   * (lines ~239, 252-254) and a '#FFFCF2' wash. Neither hex exists anywhere in
   * dcTok, which is why LS-A7 mints them here rather than letting a component
   * sheet hand-type them.
   *
   * WHY THIS IS A PAIR AND NOT TWO STRAY VALUES. They are the two ENDS of one
   * job: "the characters you typed" and "the row those characters are in". The
   * strong fill goes behind a run of two or three characters and has to survive
   * being that small; the wash goes behind a whole table row and has to stay
   * legible under twelve columns of ordinary text. A single value cannot do
   * both, which is the same reason `-soft` and `-wash` are already two words in
   * this file: `--tp-brand-soft` is a FILL BEHIND SOMETHING SMALL and `--tp-wash`
   * is a NEUTRAL WASH ON A SURFACE. The suffixes are read from that vocabulary,
   * not invented for this pair.
   *
   * WHY NOT `--tp-warn-soft` (#FCEFDC), WHICH IS CLOSE. A tone says WHAT A
   * THING IS — warn means "this needs attention". A match highlight says
   * WHERE YOUR QUERY LANDED, which is transient, is owned by the search UI and
   * disappears when the dropdown closes. Painting it in a tone would make every
   * matched name read as a warning, and would put a fifth job on a four-tone
   * ramp. This is the `--tp-avatar-bg` precedent one section up: a near value
   * that lives under a ROLE name because the role is not a surface or a tone.
   *
   * THE EDGE THAT PAIRS WITH THE WASH IS `--tp-warn`, AND THAT IS NOT A
   * CONTRADICTION of the paragraph above. LS-A8 pairs the row wash with a 3px
   * `--tp-warn` left edge — a BORDER, where the 4.5:1 text floor does not apply
   * and where warn is still the right amber. What is refused is painting the
   * TEXT FILL in a tone, not borrowing the tone's line for an edge.
   *
   * CONSUMERS TODAY: `--tp-match-soft` is read by `.tp-typeahead-mark` and
   * `--tp-match-wash` by `.tp-typeahead-cohighlight`, both in components.css's
   * TYPEAHEAD section. Neither is a token published ahead of its reader
   * (D-216(2)); the co-highlight rule ships WITH the primitive because the
   * treatment belongs to the dropdown that causes it, not to the page whose
   * table wears it. */
  --tp-match-soft: #FFEEC2;
  --tp-match-wash: #FFFCF2;

  /* ---- Avatar role ramp — THE VETTED PAIRS -------------------------------
   * Cohesion board 02 (docs/design/2026-08-14-cohesion-artboards/
   * cohesion-primitives.html, `avatarTones` in the <script> block — the pairs
   * are DATA there, not markup). Six pairs, each measured >= 4.5:1, and
   * tests/styleguide/test_atoms_avatar_tile.py COMPUTES the ratio from these
   * hexes rather than trusting any number written here. No ratio is quoted in
   * this file for that reason: the board's own figures are off by as much as
   * 1.6 (it says 6.02 for neutral; WCAG luminance on #D9DEEA/#384153 says
   * 7.61), and a hand-copied ratio is the drifting-value shape a pointer
   * replaces.
   *
   * READ THIS BEFORE ADDING A PAIR: fill and ink are ONE prop. They are named
   * in `-bg` / `-ink` couples and .tp-avatar sets both in a single rule,
   * because the 1.35:1 defect on /me was exactly a half-applied pair —
   * `background: var(--tp-avatar-bg)` (a FALLBACK fill, meant to be overpainted
   * by tp.js's data-tp-swatch) kept beside `color: var(--tp-surface)` (white,
   * meant for the PAINTED strong fill) on a template that carries no swatch, so
   * the paint never arrived and white initials sat on #D9DEEA. There is no
   * strong/solid avatar fill in this system any more: that is what removes the
   * failure rather than fixing one instance of it. A new role pair adds BOTH
   * halves here or neither.
   *
   * THIS RAMP IS NOT PART OF THE ELEVEN GREYS, AND --tp-avatar-admin-bg IS NOT
   * A TWELFTH GREY. #E1E4EA is 4/4/4 per channel off --tp-hairline #E5E8EE, so
   * the grey law above ("a twelfth near-grey is drift, not a value") looks like
   * it applies. It does not, and collapsing this fill onto --tp-hairline —
   * which is what shipped — breaks two rules of this file at once:
   *   1. THE HAIRLINE HAS EXACTLY TWO JOBS, the container edge and the internal
   *      rule, and "anything that is neither of those jobs is not a hairline".
   *      An avatar FILL is neither. Painting one with --tp-hairline gives the
   *      container-edge token a third job, which is the drift the two-jobs rule
   *      exists to stop.
   *   2. THE GREY LAW GOVERNS THE NEUTRAL PALETTE — the free-composition set
   *      any component may draw a surface, a divider or an ink from. Its point
   *      is that a component author must not eyeball a near-#E5E8EE for a
   *      divider. These eleven values are interchangeable within a job. A ramp
   *      member is not: it is one half of a closed, vetted, six-member contrast
   *      COUPLE whose only job is to carry one specific ink at a measured
   *      ratio. Substituting the nearest existing token changes the measured
   *      pair (14.67:1 computed on --tp-hairline vs 14.14:1 on the board's own
   *      fill) and makes ramp membership a question of "what is nearest"
   *      instead of "what was vetted" — which is exactly how a role token stops
   *      describing a role.
   * The precedent is already here: --tp-avatar-bg is itself a near-grey that
   * lives in the palette under a ROLE name, because an avatar fill is a role
   * and not a surface. Do not collapse any of these into the greys.
   *
   * `--tp-avatar-bg` is the NEUTRAL fill and stays above under its historic
   * name (it is referenced widely); `--tp-avatar-ink` is its other half, here
   * so the pair is addressable as a pair. Manager's pair is value-identical to
   * --tp-brand-soft / --tp-brand and is still spelled out: these are the avatar
   * ramp's public names, and the contrast sweep enumerates the
   * `--tp-avatar-*-bg` / `-ink` couples by NAME. */
  --tp-avatar-ink: #384153;            /* the other half of --tp-avatar-bg */
  --tp-avatar-admin-bg: #E1E4EA;       /* Administrator */
  --tp-avatar-admin-ink: #0F1722;
  --tp-avatar-manager-bg: #E7EDFE;     /* Manager */
  --tp-avatar-manager-ink: #2456E6;
  --tp-avatar-corporate-bg: #EFE9FC;   /* Corporate Director */
  --tp-avatar-corporate-ink: #6D28D9;
  --tp-avatar-regional-bg: #E2F1F5;    /* Regional Director */
  --tp-avatar-regional-ink: #0E7490;
  --tp-avatar-maint-bg: #FBF0DF;       /* Maintenance Director */
  --tp-avatar-maint-ink: #92400E;

  /* ---- Heatmap VALUES ramp — S11's six solid fills, THE VETTED PAIRS -----
   * Round 11's ASK 286 (S26 #r11-286): "Ruled: S11's own six fills when the
   * heatmap prints values. Every printed value already clears 4.5:1 on them."
   * UCheck S11 draws the six at :215-220 and a printed cell at :238 (the
   * round-11 ref), "Ink on the three dark steps, dark ink on the three light
   * ones" (:296) — drawn as white on the dark three and #0F1722 on the light.
   * Read ONLY by `.tp-chart-heat--values` (components.css, the chart family's
   * heatmap block), lightest step first. R1's alpha ramp has no tokens and stays
   * the heatmap that prints nothing.
   *
   * THIS RAMP IS NOT PART OF THE ELEVEN GREYS, for the avatar ramp's reason
   * above: each member is one half of a closed, vetted, six-member contrast
   * couple, so membership is "what was vetted", never "what is nearest".
   * #D7DDE8 and #A9B3C4 are no grey of the eleven and are not a twelfth; the
   * other four equal --tp-wash / --tp-muted / --tp-ink-2 / --tp-ink and are
   * still spelled out, as Manager's avatar pair is — these are the ramp's
   * public names, so a grey that moves for its own job does not move a vetted
   * pair with it. No ratio is quoted here; tests/styleguide/test_chart.py
   * computes them from these hexes.
   * (This block did not exist until the lane's fix round of 2026-10-11: steps 1
   * and 2 shipped in components.css as ink mixed into the surface at 15% and
   * 33% — luminance-matched but ΔE76 5.1 and 7.6 off the board, the ramp's blue
   * gone — on a reading of the grey law that this file's avatar clause already
   * refutes.) */
  --tp-heat-0-bg: #F2F4F8;
  --tp-heat-0-ink: #0F1722;
  --tp-heat-1-bg: #D7DDE8;
  --tp-heat-1-ink: #0F1722;
  --tp-heat-2-bg: #A9B3C4;
  --tp-heat-2-ink: #0F1722;
  --tp-heat-3-bg: #6B7385;
  --tp-heat-3-ink: #FFFFFF;
  --tp-heat-4-bg: #384153;
  --tp-heat-4-ink: #FFFFFF;
  --tp-heat-5-bg: #0F1722;
  --tp-heat-5-ink: #FFFFFF;

  /* ---- DELETED 2026-08-16 — the two greys named after their first consumer
   * ------------------------------------------------------------------------
   * `--tp-table-header-bg` and `--tp-mono-chip-bg` were promoted in Phase 1 as
   * "recurring literals" and took the name of the surface that happened to want
   * them first, which is exactly how a role token stops describing a role: the
   * popover GROUP HEAD read --tp-table-header-bg and is not a table header.
   * They became aliases on 2026-08-14 with the instruction "migrate the reads,
   * then delete these two lines", and cohesion Stage 3 did.
   *
   * ALL 27 READS WERE A PURE RENAME — not one pixel moved, and that is the
   * finding rather than a shortcut. §9.1's split is narrow: sunken is a
   * recessed BAND inside a surface (table head, group head, row hover) and wash
   * is the mono chip / code ground. The Stacks Design System, built
   * independently, publishes the identical pair with the identical role notes
   * (`--stk-sunken: #FAFBFD` — its note reads "table header, row hover"), so
   * the twenty-six
   *
   * ⚠ DO NOT PUT A NESTED COMMENT MARKER BACK IN THIS SENTENCE. It quoted the
   * design system's line VERBATIM, inline comment and all, and CSS comments do
   * NOT nest: the inner terminator closed the block opened above, so every line
   * from there to the next semicolon was parsed as CSS. The parser's error
   * recovery then swallowed the FIRST declaration after it — which was
   * `--tp-scrim` — leaving it undefined app-wide while this file plainly showed
   * it declared. `background: var(--tp-scrim)` computed to `transparent`, so
   * EVERY confirm dialog, bottom sheet and drawer rendered with no dimming at
   * all: the D-150/D-152 confirm surfaces lost the signal that the page behind
   * them is unreachable, and nothing failed, because a stylesheet test reads
   * the FILE and the file was right. Found 2026-08-16 by measuring a live
   * confirm (`getComputedStyle(scrim).backgroundColor === "rgba(0, 0, 0, 0)"`),
   * and reproduced with a synthetic sheet: of three declarations around such a
   * comment, only the one immediately after it is dropped.
   * band-and-hover reads all wanted sunken and the one chip wanted wash. What
   * was broken was the NAME a reader saw at the call site, and only that.
   *
   * ⚠ ONE REUSE FINDING FELL OUT OF READING THE 26, recorded here because the
   * token sweep is not where it gets fixed: five page-level "note" boxes
   * (`.tml-caveat`, `.tml-rung-note--boxed`, `.tml-reach-note`,
   * `.tml-intro-card--quiet`, `.tm-prof-pin-note`) re-implement the shared
   * `.tp-note` primitive — same fill, same hairline, same shape — each in its
   * own sheet. That is the 2026-07-24 directive's own defect, and it needs the
   * macro call sites changed rather than a token renamed. */

  /* ---- The scrim — ONE value (§9.1, owner-ruled 2026-08-14) --------------
   * `rgba(15,23,34,0.45)`. It replaces BOTH shipped tokens; there is no light
   * scrim and no heavy scrim any more, because 20 scrim darknesses in the
   * bundle were 20 authors guessing, not a two-step system.
   *
   * THIS FILE SAID SOMETHING ELSE UNTIL 2026-08-14, and so does the prose in
   * components.css:4502-4515 / me.css:878-882: *"the two tokens are not
   * interchangeable — --tp-scrim is the POPOVER veil, --tp-scrim-heavy the
   * blocking one."* That was a true reading of a two-token system and it is now
   * superseded: the distinction the comment defends is exactly the one the
   * ruling deletes. Following it would keep two darknesses alive under one
   * value and re-fork them the first time somebody "restored" the difference.
   *
   * NON-BLOCKING OVERLAYS GET NO SCRIM AT ALL — popover, anchored menu, toast.
   * A scrim is the signal that the surface beneath is unreachable; painting one
   * under something you can still click through is a lie about the interaction.
   *
   * The whole `rgba(0,0,0,·)` family is deleted — *"it renders colder than the
   * ink scrim beside it."* No portal stylesheet has ever carried one; the guard
   * in tests/styleguide/test_cohesion_token_ladder.py keeps it that way.
   *
   * The VIOLET impersonation scrim SURVIVES as a sanctioned hue variant at this
   * same 0.45 alpha (owner ruling 2026-08-14; design-delta #44 keeps
   * impersonation.css as its scoped definition site, because impersonation is a
   * mode the whole UI signals). It still reads 0.5 at impersonation.css:39 —
   * registered as the one live divergence, self-expiring.
   *
   * `--tp-scrim-heavy` IS DELETED (2026-08-16, cohesion Stage 3). It shipped
   * here as `var(--tp-scrim)` for two days, with the instruction "migrate every
   * read to --tp-scrim, then delete this line". Its last four reads — the admin
   * stepped-sheet underlay, the claims photo-tile caption gradient, the install
   * sheet's scrim and the nav drawer's — are migrated, so the line is gone
   * rather than resolving forever. */
  --tp-scrim: rgba(15,23,34,0.45);

  /* ---- Font stacks -------------------------------------------------------
   * Inter for UI; JetBrains Mono for identifiers ONLY (claim IDs, plates,
   * codes) — never for body copy. */
  --tp-font-ui: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  --tp-font-mono: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace;

  /* ---- THE TYPE LADDER — eight portal rungs, each with a named role -------
   * §9.5 ruling 2 (docs/design/2026-08-14-cohesion-artboards/PASS-3-RULINGS.md):
   *
   *     "A SIZE IS LEGAL ONLY IF A NAMED ROLE HOLDS IT."
   *     11 · 12 · 13 · 14 · 15 · 18 · 22 · 30, plus kiosk 32 · 44.
   *     NO HALF-STEPS ANYWHERE.
   *
   * NAMED BY ROLE, NEVER BY SIZE — `--tp-type-page-title-size`, never
   * `--tp-type-22`. A size-named token re-opens the door the ruling closes: the
   * next author picks by eye instead of by job, and 41 type sizes come back.
   *
   * WHAT THIS BLOCK USED TO BE. Seven semantic role ANCHORS, and it said so —
   * "never an exhaustive ladder" was the correct defence when the audit's §5
   * accused it of being an incomplete scale. §9.5 supplies the exhaustive ladder
   * those anchors sit on, so the defence is spent: snapping to a rung is now the
   * right fix where it was previously the wrong one. Anchors whose NAME already
   * meant the right role keep it; three whose name meant a different role than
   * the one they hold are renamed, with deprecated aliases at the foot of this
   * block.
   *
   * ROLES THAT BORROW A RUNG NEVER GET A SIZE TOKEN. This is the anti-drift
   * rule, and it is why `--tp-type-mono-id-size` is retired below. A MONO
   * IDENTIFIER takes the rung of the text beside it — 13 in a table cell, 12 in
   * a meta line, 11 in a badge. A BUTTON LABEL and an INPUT VALUE are both
   * CONTROL TEXT at 14. Give a borrowed role its own size and it drifts off the
   * text it is supposed to sit level with.
   *
   * RETIRED BY THIS RULING: 13.5 (194 occurrences in the bundle) · 19 · 24 —
   * plus the standing 6.5 · 7.5 · 8 · 8.5 · 9 · 9.5 · 10 · 10.5 · 11.5 · 12.5 ·
   * 14.5 · 15.5 · 16.5. Also 28: it held the display role here and is not a rung
   * (see --tp-type-display-size).
   *
   * KIOSK 32 / 44 ARE DELIBERATELY NOT PUBLISHED. They are legal on Launchpad
   * and nowhere else — "not legal in the portal" — and a portal sheet that can
   * reach a token will eventually use it.
   *
   * FONT WEIGHT is a FIVE-value scale, and WHICH of the five a role takes is no
   * longer a choice — see "THE FIVE WEIGHTS" and "THE ROLE MATRIX" below.
   * THIS PARAGRAPH USED TO END THE JOB HERE, and that was the gap §9.6 closed.
   * It read: *"400 long body · 500 secondary control · 600 label, section title,
   * page title · 700 title and control emphasis · 800 kiosk and display only"* —
   * §9.5's draft amendment, which §9.6 partly STRUCK (the label is 700, not 600)
   * and, more importantly, left three rungs offering a MENU. A rung with two
   * legal weights is a rung where the next author picks by eye, *"which is the
   * same open door that let 41 sizes in."* */

  /* ---- THE FIVE WEIGHTS — each one a JOB, not a level of importance --------
   * §9.6 (Weight and Role Matrix V1,
   * docs/design/2026-08-14-cohesion-artboards/PASS-4-WEIGHT-MATRIX.md):
   *
   *     "WEIGHT IS NOT IMPORTANCE. WEIGHT IS LENGTH AND SIZE: a sentence is
   *      light, a name is heavy, and small type gains weight to survive being
   *      small."
   *
   * That single sentence is why the PAGE TITLE is LIGHTER than a CARD TITLE.
   * `22/600 · 15/700 · 11/700` is ONE CURVE, NOT THREE DECISIONS — as the size
   * falls the stroke has to hold, so the weight climbs. *"An 800 page title was
   * the instinct that importance means heavy, and it is the instinct that
   * produced four page-head components."*
   *
   * These five are the SCALE. A role token below holds one of them as a literal
   * (never `var(--tp-weight-…)`) so the pin table shows each role's pair at a
   * glance and `var()` in this file keeps meaning exactly one thing: A
   * DEPRECATED ALIAS.
   *
   * ⚠ NO PORTAL SHEET MAY READ ANY OF THE FIVE — INCLUDING THE FOUR LEGAL ONES.
   * This paragraph used to end *"a page sheet that has no role token to read may
   * read the scale directly — that is what these names are for."* That sentence
   * was WRONG the moment §9.6 landed, and it was the batch's own hole: under
   * §9.6 there IS no such page sheet — *"if the thing being typeset is not one
   * of the 26 roles, it is one of them under another word; it is never a 27th."*
   * A sheet that reads `--tp-weight-name` beside `--tp-type-card-title-size`
   * renders a 15/600, which is the exact pair §9.6 struck by name, and it does
   * it while every role token in this file still reads correctly. The scale is
   * published for its NAMES — so the jobs are written down once, and so the
   * guard has something to watch — not as a second way to typeset anything.
   * Guarded by
   * tests/styleguide/test_cohesion_token_ladder.py::test_no_portal_stylesheet_reads_the_bare_weight_scale,
   * which derives the five names from THIS BLOCK and allows zero live reads of
   * any of them.
   *
   * The variable-font values (450 · 550 · 650 · 750 · 780) round to their
   * neighbours. Portal sheets carry 36 raw off-scale `font-weight` declarations
   * today — 25 of them variable-font values and 11 at 800 — and every one is
   * registered, per selector, in
   * tests/styleguide/test_cohesion_token_ladder.py, where the list can only
   * shrink. */
  --tp-weight-sentence: 400;  /* SENTENCES — anything that reads as prose and can
                               * wrap to a second line: body, dense body, helper,
                               * meta, tooltips. If it is a sentence, it is 400. */
  --tp-weight-value: 500;     /* VALUES IN A BOX — what a user typed or picked,
                               * read back inside a control. AND EVERY MONO
                               * IDENTIFIER: mono reads a step heavier per stroke,
                               * so 500 mono sits optically level with 400 Inter. */
  --tp-weight-name: 600;      /* NAMES OF THINGS — the name of an object or an
                               * action: page title, section title, list-row
                               * primary, button label, chip. Big enough not to
                               * need help. */
  --tp-weight-small: 700;     /* SMALL AND TITLES — titles inside a container, and
                               * small type that must survive being small: card
                               * title, field label, eyebrow, column header,
                               * selected tab, display numeral. */
  /* KIOSK ONLY — read at arm's length. ILLEGAL below 32px and ILLEGAL ANYWHERE IN
   * THE PORTAL. It is PUBLISHED rather than withheld, which is the opposite of
   * how the kiosk SIZES are handled (32/44 have no token, because a portal sheet
   * that can reach a token will eventually use it). The difference is that a
   * weight has a spelling every sheet can already type: `font-weight: 800` needs
   * no token at all, so withholding one buys nothing and forfeits the name. With
   * the name published, BOTH routes are guarded —
   * tests/styleguide/test_cohesion_token_ladder.py fails on a portal read of this
   * token AND on a raw off-scale `font-weight` literal in any portal sheet.
   * *"The 149 portal instances are the migration"* (§9.6); 36 are live in
   * app/static/css and every one is registered there.
   *
   * THIS ASYMMETRY ARGUMENT COVERS ONLY THE 800, and it was briefly misread as
   * blessing the four above it. It does not: it explains why withholding a NAME
   * for 800 buys nothing, not why a sheet may read one. 400/500/600/700 are
   * legal LITERALS in a portal sheet (`PORTAL_WEIGHTS`) and their names are as
   * unreadable as this one — see the warning at the head of this block. */
  --tp-weight-kiosk: 800;

  /* ---- THE ROLE MATRIX — 26 roles, one pair each (§9.6) --------------------
   *
   *     "NAME THE ROLE, READ THE PAIR. If the thing being typeset is not one of
   *      the 26 roles, it is one of them under another word — it is never a 27th.
   *      A screen that needs a new pair is a screen that needs a new role, and
   *      that is a DESIGN REQUEST, not a style attribute."
   *
   * So every rung below publishes its roles COMPLETE — size AND weight, plus the
   * tracking, the case, the family and the ink where the matrix states them. A
   * size token without a weight beside it IS the menu: it hands the next author a
   * rung and lets them pick. Two of batch 1's did exactly that
   * (`--tp-type-control-size` with *"the weight is the control's own business"*,
   * and `--tp-type-body-dense-size` with no weight at all) and both are closed
   * here.
   *
   * THE ROLE SET IS CLOSED, AND THAT IS A SEPARATE LAW FROM THE PAIRS. Publishing
   * the right pair on every named role does NOT stop a 27th role appearing: a new
   * prefix carrying a pair the rung already legally holds satisfies every
   * per-value guard there is. So the SET of published `--tp-type-*` prefixes is
   * asserted equal to §9.6's, in
   * test_cohesion_token_ladder.py::test_the_published_role_set_is_exactly_the_
   * matrix_and_never_a_twenty_seventh. That is what makes *"a screen that needs a
   * new pair is a screen that needs a new ROLE, and that is a design request"*
   * enforceable rather than aspirational — a new prefix fails until the matrix
   * names it, and naming it means a board ruled it.
   *
   * TWO ROLES DELIBERATELY GET NO TOKEN: KIOSK TITLE (32/700) and KIOSK DISPLAY
   * (44/800). They are roles on the matrix and they are *"not legal in the
   * portal"* — see the kiosk note at the foot of this block.
   *
   * ONE ROLE GETS A WEIGHT AND NO SIZE: the MONO IDENTIFIER borrows the size of
   * the text beside it (§9.5: *"a role that borrows a rung never gets a size
   * argument"*). It is the only sanctioned weight-without-size in the file, and
   * the guard names it as such rather than skipping it.
   *
   * ONE ROLE HOLDS TWO PAIRS: the TAB LABEL, `14/500` muted at rest and `14/700`
   * ink when selected — *"a state, not a choice, and the only place in the matrix
   * where one role holds two pairs."* It is published as TWO NAMED STATES
   * (--tp-type-tab-rest-* / --tp-type-tab-selected-*), never as one role with a
   * weight argument, because an argument is a choice and this is a state.
   *
   * CASE IS SENTENCE UNLESS A ROLE SAYS OTHERWISE. Only four roles are uppercase
   * — field label, eyebrow, table column header, mono badge — and each publishes
   * its own `-transform`. A role with no `-transform` token is sentence case; a
   * `-transform` holding anything but `uppercase` is drift.
   *
   * THE FAMILY IS INTER UNLESS A ROLE SAYS OTHERWISE, and exactly THREE say so:
   * §9.6 marks the count badge, the mono identifier and the mono badge `mono`.
   * Each publishes `-family: var(--tp-font-mono)`; every other role publishes
   * none and is Inter. This column was DROPPED when these roles were first
   * written — the family was left to the `.tp-mono` class — which left the count
   * badge as an 11/700/tabular role with nothing in it that says mono. The class
   * still exists and nothing migrates; the role now states its own face, which
   * is the point of a lookup.
   *
   * INK IS PUBLISHED WHERE IT IS CONSTANT, and deliberately absent where it is
   * not. Three roles draw their ink from the TONE they are painted in — button
   * label (*"per tone"*), chip / status pill and count badge (*"tone ink"*) — and
   * a single `--tp-type-…-ink` on any of them would be a lie about who owns the
   * colour. Roles whose ink changes with STATE publish the state as a suffix
   * (`-ink-empty`, `-ink-error`, `-ink-active`); their PAIR does not move, which
   * is what separates them from the tab label. */

  /* 11 — THE TRACKED-UPPERCASE RUNG. FOUR roles own it, plus the count badge and
   * the phone tab-bar caption, and §9.6 struck the ambiguity that used to sit
   * here: *"'Label at 600' is struck. 11 carries ONE weight: 700."* There is no
   * sentence-case label anywhere in the portal at 11px — every 11 is uppercase
   * and tracked — and uppercase at 11px loses more stroke to tracking than
   * lowercase does.
   *
   * THE THREE TRACKINGS ARE DRAWN, NOT PICKED: field label 0.06em · eyebrow
   * 0.06em · column header 0.08em · mono badge 0.06em (§9.6's own enumeration).
   *
   * ⚠ THE EYEBROW TRACKING CHANGED, 0.12em → 0.06em, AND IT MOVES FOUR LIVE
   * RULES. It shipped at 0.12em on 2026-08-14 from §8.3's stat-tile row —
   * `label 11/700 · 0.12em · uppercase` (cohesion-primitives.html:156) — and this
   * comment then said *"the eyebrow tracks 0.12em, the field label 0.06em"* and
   * that *"TABLE COLUMN HEADERS and MONO CHROME / BADGES borrow the eyebrow
   * pair."* §9.6 supersedes all of it: it enumerates the four tracked-11 roles
   * with their trackings, gives the column header 0.08em of its own, and puts the
   * eyebrow at 0.06em. So "borrow the eyebrow pair" was wrong for the column
   * header in the same breath it was written. The four rules that re-render at
   * half the previous tracking are `.tml-eyebrow` (team-levels.css),
   * `.dc-eqcost-label` (claims.css), `.tm-dir-group` (team.css) and the phone
   * `.tp-sheet-label` (components.css) — 11px type, so the change is ~0.66px per
   * character. It is a VALUE change ruled by a board, not a rename, so there is
   * no successor name and no alias to migrate onto. (Two of those four also
   * hand-type `font-size: 10.5px` beside the token — a retired half-step, and
   * Stage 3's to move onto this rung.) */
  --tp-type-eyebrow-size: 11px;
  --tp-type-eyebrow-weight: 700;
  --tp-type-eyebrow-tracking: 0.06em;
  --tp-type-eyebrow-transform: uppercase;
  --tp-type-eyebrow-ink: var(--tp-muted);
  --tp-type-field-label-size: 11px;
  /* ✅ CLOSED 2026-08-14 by §9.6 (Weight and Role Matrix V1), on the board this
   * row asked for. It read "⚠ OPEN AT CLAUDE DESIGN — DO NOT GUESS PAST THIS ROW"
   * and shipped 600, because 600 is what the portal renders today and the batch
   * that wrote it correctly refused to decide a design question in code.
   *
   * §9.6 STRUCK the 600 rather than clarifying it: rung 11 carries ONE weight,
   * 700. The reasoning is why this is settled and not a preference — there is no
   * sentence-case label anywhere in the portal at 11px. Every 11 is uppercase and
   * tracked (field label 0.06em, eyebrow 0.06em, column header 0.08em, mono
   * badge), and uppercase at 11px loses more stroke to tracking than lowercase
   * does, so it was drawn at 700 in §9.2 · 04 and stays there. §9.5's rung table
   * is what moves: row 11 loses its "600 · 700" and reads 700.
   *
   * NOTHING ON SCREEN MOVES YET, and that is deliberate: `.tp-field-label` still
   * reads the deprecated --tp-type-label-weight. Migrating consumers onto the
   * role tokens is Stage 3's job, not this layer's. */
  --tp-type-field-label-weight: 700;
  --tp-type-field-label-tracking: 0.06em;
  --tp-type-field-label-transform: uppercase;
  --tp-type-field-label-ink: var(--tp-muted);

  /* TABLE COLUMN HEADER — the third tracked-11 role, and the one §9.6 proves was
   * never "the eyebrow pair": it tracks 0.08em, wider than either label, because
   * a column header sits alone above a rule with nothing beside it to set the
   * rhythm. NEW — batch 1 told this role to borrow the eyebrow's tokens. */
  --tp-type-column-header-size: 11px;
  --tp-type-column-header-weight: 700;
  --tp-type-column-header-tracking: 0.08em;
  --tp-type-column-header-transform: uppercase;
  --tp-type-column-header-ink: var(--tp-muted);

  /* MONO BADGE — mono, 11/700, uppercase, 0.06em. NOT the mono IDENTIFIER: that
   * role borrows its size and is ALWAYS 500 (see --tp-type-mono-id-* below).
   * §9.6 keeps them apart in one sentence — *"a mono BADGE is not this role; it
   * is 11/700 like every other 11"* — and they are published apart here for the
   * same reason: a badge is a fixed 11px chip that must survive being small, an
   * identifier is a run of characters sitting level with the prose beside it. */
  --tp-type-mono-badge-size: 11px;
  --tp-type-mono-badge-weight: 700;
  --tp-type-mono-badge-tracking: 0.06em;
  --tp-type-mono-badge-transform: uppercase;
  --tp-type-mono-badge-ink: var(--tp-ink);
  /* THE FAMILY IS A COLUMN §9.6 STATES, and it was the one column this block
   * dropped. The matrix marks exactly three roles `mono` — count badge
   * `11/700 mono`, mono identifier `13/500 mono`, mono badge `11/700 mono` — and
   * the batch that wrote these roles published every other stated column (size,
   * weight, tracking, case, ink) and left the family to the `.tp-mono` class
   * (components.css:92). That is consistent with the shipped sheet and it is
   * still a hole in the LOOKUP: stripped of its family, `--tp-type-count-badge-*`
   * is 11/700/tabular with nothing in it that says mono, so a Stage-3 author who
   * reads the role tokens and trusts them gets Inter. A role that "publishes its
   * roles COMPLETE" has to carry the column the board states.
   * Guarded by test_cohesion_token_ladder.py::test_the_mono_column_is_exactly_
   * the_three_roles_the_matrix_marks — exactly three, and only ever the mono
   * face. It does NOT replace `.tp-mono`; nothing needs migrating for these to
   * be true. */
  --tp-type-mono-badge-family: var(--tp-font-mono);

  /* COUNT BADGE — mono, 11/700, TABULAR, sentence case, and NO ink token: the
   * matrix says *"tone ink"*, so the colour belongs to the badge tone the count
   * is painted in, not to the type role. Tabular because a count that ticks
   * 9 → 10 must not shift the pill's width. */
  --tp-type-count-badge-size: 11px;
  --tp-type-count-badge-weight: 700;
  --tp-type-count-badge-numeric: tabular-nums;
  --tp-type-count-badge-family: var(--tp-font-mono);

  /* PHONE TAB-BAR CAPTION — 11/600, sentence case, muted at rest and ink when
   * active. The ONE 11 that is not 700 and not uppercase: it is a NAME (of a
   * destination), which is the 600 job, and it is sentence case because it reads
   * as a word rather than as a label above something.
   *
   * ⚠ §9.6 CONTRADICTS ITSELF ON THIS ROW, THREE STATEMENTS TO ONE, AND THE
   * MATRIX IS THE READING TAKEN. Three places in §9.6 say the 11 rung carries no
   * 600: the opening ruling (*"'Label at 600' is struck. 11 carries ONE weight:
   * 700. There is no sentence-case label anywhere in the portal at 11px — every
   * 11 is uppercase and tracked."*), the rung-table amendment in the same
   * paragraph (*"§9.5's rung table row 11 loses its `600 · 700` and reads
   * `700`"*), and the Five-Weights table, whose 600 row lists its sizes as
   * `22 · 18 · 14 · 12` — no 11 — while the 700 row lists `30 · 15 · 13 · 11`.
   * ONE place draws an 11/600: this role's matrix row,
   * `11/600 · sentence · normal`.
   *
   * The matrix still wins, because §9.6 says which side wins in its own words —
   * *"the matrix is the source of truth"* — and a session does not overrule a
   * board on its own reading. The defence above is why the row is coherent
   * rather than merely tolerated: the struck 600 was a LABEL at 600, and a tab
   * caption is a NAME. But 3-to-1 is a lopsided enough split that the owner
   * should confirm the drawing was deliberate rather than a row left behind by
   * the strike.
   *
   * IT IS STILL A JUDGMENT CALL, AND IT IS LOAD-BEARING, so it goes to the owner
   * rather than living only in this comment. This 600 is the single reason
   * `RUNG_WEIGHTS["11px"]` reads `{600, 700}` instead of `{700}` in
   * tests/styleguide/test_cohesion_token_ladder.py, and a wider rung set is a
   * wider target: an 11/600 published under any other role name is a pair the
   * rung guard cannot refuse. (It is refused anyway, by the role-set guard added
   * with this note — but by the SET of roles, not by the data.) If the owner
   * rules that 11 truly carries one weight, THIS row is what moves: the caption
   * goes to 700, the rung narrows to `{700}`, and the hole closes twice over.
   * It is a ratification row on the pending cohesion decision entry (drafted as
   * D-216 — the number is NOT allocated yet; this register's tail is D-215 and
   * has renumbered at integration three times, so read the tail rather than this
   * line), beside the eyebrow and display trackings. */
  --tp-type-tab-caption-size: 11px;
  --tp-type-tab-caption-weight: 600;
  --tp-type-tab-caption-ink: var(--tp-muted);
  --tp-type-tab-caption-ink-active: var(--tp-ink);

  /* 12 — THE 12 RUNG: helper and error under a field · timestamp and meta line ·
   * chip and status pill · tab-bar caption. §9.5's rung table said "400 · 600"
   * and that WAS the menu: §9.6 splits it into THREE named roles — helper/error
   * 12/400, meta·timestamp 12/400, chip/status pill 12/600.
   * (§9.2's converged chip spec draws the pill at 11/600 and the count at 11/700;
   * §9.5's rung table — the later, settled board — puts chip and status pill on
   * 12, and the later board wins.)
   *
   * ⚠ --tp-type-meta-weight CHANGED, 600 → 400. It shipped at 600 as the RUNG's
   * weight when the rung offered two; §9.6 names the role and the role is a
   * SENTENCE (*"a timestamp is a sentence"* is the whole 400 job). Nothing reads
   * --tp-type-meta-weight directly, but --tp-type-label-weight was published as
   * an ALIAS of it and 23 selectors read THAT — so the alias is re-pointed at
   * --tp-type-chip-weight, which still holds the 600 those 23 render today. An
   * alias exists to hold a VALUE steady through a rename; re-pointing it at the
   * token that kept the value is the alias doing its job. */
  --tp-type-meta-size: 12px;
  --tp-type-meta-weight: 400;
  --tp-type-meta-ink: var(--tp-muted);

  /* HELPER / ERROR — the line under a field. Same pair as the meta line and a
   * different role, because it has a second ink: muted when it is helping,
   * danger when it is an error. The PAIR does not move between those two, which
   * is what makes this a two-ink role and not a two-pair one. */
  --tp-type-helper-size: 12px;
  --tp-type-helper-weight: 400;
  --tp-type-helper-ink: var(--tp-muted);
  --tp-type-helper-ink-error: var(--tp-danger);

  /* CHIP / STATUS PILL — 12/600. A chip carries the NAME of a state, which is the
   * 600 job. NO ink token: *"tone ink"* — the chip's tone owns its colour. */
  --tp-type-chip-size: 12px;
  --tp-type-chip-weight: 600;

  /* 13 — THE DENSE RUNG: dense body 13/400 · table-cell identifier 13/600.
   * §9.5 gave this rung "400 · 600" as a choice; §9.6 names both roles —
   * *"the first column of a table names the row, the rest describe it."* */
  --tp-type-body-dense-size: 13px;
  --tp-type-body-dense-weight: 400;
  --tp-type-body-dense-ink: var(--tp-muted);

  /* TABLE-CELL IDENTIFIER — 13/600, ink. RENAMED from --tp-type-body-strong-*,
   * which is now a deprecated alias at the foot of this block. "Body strong" is
   * a name §9.6 makes self-contradictory: *"a sentence is light, a name is
   * heavy"*, so there is no such thing as strong body copy — the role is the
   * cell that NAMES the row. The old name is why `.tp-card-title` (components.css)
   * reads it today and renders a card title at 13/600 instead of the ruled
   * 15/700. */
  --tp-type-table-cell-id-size: 13px;
  --tp-type-table-cell-id-weight: 600;
  --tp-type-table-cell-id-ink: var(--tp-ink);

  /* 14 — THE BUSIEST RUNG: SIX roles, and the one §9.5 left most open. It is
   * also the rung that kills 13.5 ("the last half-step still being defended, and
   * it was being defended by a component").
   *
   * ⚠ WHAT THIS COMMENT USED TO CLAIM, and why §9.6 struck it: *"CONTROL TEXT is
   * one role covering the input value, the select, the in-list search, the BUTTON
   * LABEL and the TAB LABEL — so it gets ONE size token and no weight: the weight
   * is the control's own business."* That sentence IS the menu. §9.6's answer:
   * *"14/400 and 14/600 are not two ways to set 14 — they are body copy and a
   * list-row primary, which are DIFFERENT THINGS that happen to share a rung."*
   * Control text, the button label, the tab label and the menu item are four
   * roles with four published pairs, not one role with a weight argument.
   *
   * The 38/44 height ladder is UNCHANGED — only the text stops pairing with the
   * box: 14 at both breakpoints. "14px inside a 38px box has 36px of inner room;
   * nothing reflows, nothing is redrawn, the number changes." */

  /* BODY COPY — 14/400, ink-2. A sentence, so it is 400; secondary ink, because
   * prose sits under the names around it. */
  --tp-type-body-size: 14px;
  --tp-type-body-weight: 400;
  --tp-type-body-ink: var(--tp-ink-2);

  /* LIST-ROW PRIMARY — 14/600, ink. The row's NAME, which is the 600 job. Same
   * rung as body copy and a heavier stroke: that pair of rows is the clearest
   * worked example of "weight is length, not importance". */
  --tp-type-list-primary-size: 14px;
  --tp-type-list-primary-weight: 600;
  --tp-type-list-primary-ink: var(--tp-ink);

  /* CONTROL TEXT — 14/500, ink; MUTED when the control is EMPTY (a placeholder is
   * not a value). One role for the input value, the select and the in-list
   * search: *"values in a box"*, which is the whole 500 job.
   * NEW: the weight. Batch 1 published the size alone on purpose and said so;
   * §9.6 closes it. */
  --tp-type-control-size: 14px;
  --tp-type-control-weight: 500;
  --tp-type-control-ink: var(--tp-ink);
  --tp-type-control-ink-empty: var(--tp-muted);

  /* BUTTON LABEL — 14/600. A button label NAMES AN ACTION ("Save changes", not
   * "Submit"), so it is 600 and not the 500 of the value beside it. NO ink token:
   * *"per tone"* — the button's kind owns its ink, and there is no on-solid token
   * in this file by design (see tests/styleguide/conftest.py on #FFFFFF). */
  --tp-type-button-label-size: 14px;
  --tp-type-button-label-weight: 600;

  /* TAB LABEL — THE ONE ROLE THAT HOLDS TWO PAIRS, published as TWO NAMED STATES.
   * *"A state, not a choice, and the only place in the matrix where one role
   * holds two pairs. Everything else resolves to one."*
   *
   * WHY TWO NAMES AND NOT ONE ROLE WITH AN ARGUMENT: an argument is something a
   * consumer chooses, and this is something the interface IS. A sheet writes
   * `.tp-tab { font-weight: var(--tp-type-tab-rest-weight) }` and
   * `.tp-tab[aria-selected="true"] { font-weight: var(--tp-type-tab-selected-weight) }`
   * — two rules, two names, no decision. The SIZE is identical across the two on
   * purpose and is guarded as such: a tab strip that re-measured on selection
   * would reflow every time the user changed tab. */
  --tp-type-tab-rest-size: 14px;
  --tp-type-tab-rest-weight: 500;
  --tp-type-tab-rest-ink: var(--tp-muted);
  --tp-type-tab-selected-size: 14px;
  --tp-type-tab-selected-weight: 700;
  --tp-type-tab-selected-ink: var(--tp-ink);

  /* MENU ITEM — 14/500, ink-2. A row in an anchored menu or a select list: the
   * user is reading choices back, which is the same job as a value in a box. */
  --tp-type-menu-item-size: 14px;
  --tp-type-menu-item-weight: 500;
  --tp-type-menu-item-ink: var(--tp-ink-2);

  /* 15 — CARD TITLE, and the two roles that are the same thing under another
   * word: the LIST / STATE-BLOCK HEADLINE and the OVERLAY TITLE ON PHONE. All
   * three are `15/700 · sentence · normal · ink` in §9.6, byte for byte, so they
   * share ONE token rather than three identical ones — *"it is never a 27th; if
   * it is not one of the 26 roles it is one of them under another word."*
   * NEW rung in batch 1: the anchor set had nothing between 14 and 18, which is
   * where 15.5 and 16.5 got in.
   *
   * ⚠ THE WEIGHT CHANGED, 600 → 700, and §9.6 is explicit about why the card
   * title is HEAVIER than the page title above it: *"15 → 600 · 700 becomes one
   * pair only: 15/700. The 600 was §9.2 · 07 drawn before the split; nothing uses
   * a 15/600."* Blast radius today: ZERO — no stylesheet reads either
   * --tp-type-card-title-* token yet. (`.tp-card-title` renders the 13 rung
   * through --tp-type-body-strong-*; that is Stage 3's to move.) */
  --tp-type-card-title-size: 15px;
  --tp-type-card-title-weight: 700;
  --tp-type-card-title-ink: var(--tp-ink);

  /* 18 — SECTION TITLE: the head of a pane, drawer, modal or settings group.
   * ONCE PER CONTAINER (§8.2 SECTION SCALE). Tracking is the shared heading value
   * below (--tp-type-heading-tracking), which §8.2 draws as -0.15px here. */
  --tp-type-section-title-size: 18px;
  --tp-type-section-title-weight: 600;
  --tp-type-section-title-ink: var(--tp-ink);

  /* 22 — PAGE TITLE: once per screen, top of the content column, ink tile beside
   * it (§8.2 PAGE SCALE).
   * The WEIGHT IS 600 and that row is NOT open: §8.2 draws it, the portal ships
   * it, and the §9.1 ladder row that said 800 is the one §9.5 corrected —
   * "the same failure mode as 19 and 24, a ladder row written without reading
   * the ruling beside it."
   *
   * ⚠ OPEN AT CLAUDE DESIGN (2026-08-14) — THE PHONE FORM OF THIS RUNG HAS NO
   * TOKEN, AND MUST NOT BE HAND-TYPED. §8.2 draws the page head at ≤767.98 as
   * `title 20 / 600` (page-head-ruling.html `pageSpec` "phone ≤767.98", and the
   * drawn 390 heads at :195 and :825). 20 is NOT on §9.5's closed ladder
   * (11·12·13·14·15·18·22·30) and is not in its retirement list either — the
   * ruling simply never reached it. This is PASS-2 §A2's exact shape ("the type
   * ladder excludes sizes the adopted page-head ruling depends on"), which §9.5
   * resolved for 22 and 18 and left unresolved for 20.
   *
   * ✅ CLOSED — D-224(2), 2026-08-16. This paragraph read: "The board must say
   * which: 20 is a NINTH rung held by the named role 'page title at ≤767.98', or
   * §8.2's phone head snaps to 18. Either answer is a token." The ruling took
   * NEITHER — a third outcome the guard anticipated but did not enumerate: the
   * phone head keeps the rung it already has, so THE ROW NEEDS NO TOKEN AT ALL.
   * There is still deliberately no phone page-title size here, and the ban on
   * hand-typing `font-size: 20px` into a page sheet is now a RULING rather than a
   * holding position — that is the hand-typed half-step this whole ladder exists
   * to stop. Guarded by
   * tests/test_design_tokens.py::test_the_phone_page_title_rung_stays_on_the_22_rung. */
  --tp-type-page-title-size: 22px;
  --tp-type-page-title-weight: 600;
  --tp-type-page-title-ink: var(--tp-ink);

  /* 30 — THE DISPLAY NUMERAL: dashboard KPI, scorecard figure, hero number.
   * NEVER PROSE. §9.5's rung table offered "700 · 800"; §9.6 resolves it to ONE
   * pair, 30/700 — the 800 is kiosk-only and illegal here.
   * TABULAR on purpose: a KPI that ticks 9 → 10 must not shift the tile.
   * VALUE CHANGED 2026-08-14: the size was 28px. 28 is not a rung — PASS-2 §A2
   * spells it out ("pass 1's type block also publishes body 14, dense body 13,
   * subtitle 18, display 28; of those, only 13 survives the new ladder") and §9.5
   * rescues 14 / 18 / 22 but not 28. Leaving it at 28 would have kept the one
   * legal home for a big number off the ladder, which is how a KPI ends up at 27
   * or 29 beside it. Live consumer: components.css:66-70 (`h1`). */
  --tp-type-display-size: 30px;
  --tp-type-display-weight: 700;
  --tp-type-display-numeric: tabular-nums;
  --tp-type-display-ink: var(--tp-ink);

  /* ---- THE TWO KIOSK ROLES — ON THE MATRIX, DELIBERATELY UNPUBLISHED --------
   * §9.6's matrix ends with KIOSK TITLE `32/700 · sentence · −0.02em` and KIOSK
   * DISPLAY `44/800 · sentence · −0.025em`. Both are roles; neither gets a token,
   * for the reason §9.5 already gives about the sizes: they are legal on
   * Launchpad and *"not legal in the portal"*, and a portal sheet that can reach
   * a token will eventually use it. The kiosk WEIGHT is published
   * (--tp-weight-kiosk) and guarded instead, because 800 has a spelling no token
   * can withhold — see the note there.
   * These two roles are the reason the matrix has 26 entries and this file
   * publishes 24. tests/styleguide/test_cohesion_token_ladder.py asserts the
   * absence, so "unpublished" is a checked state rather than an oversight. */

  /* ---- Letter-spacing is `em` ONLY (§9.1) --------------------------------
   * ~400 unitless `letterSpacing` values exist in the bundle. Unitless renders
   * as PX in CSS, so the intent was TRACKING and the result is a NO-OP that does
   * not scale with the size beside it. A px letter-spacing is drift even when it
   * looks right at one size.
   *
   * FOUR em values across the tracked roles, and nothing else (§9.6 supplies the
   * per-role column; this list used to say "0.12em mono eyebrow", which §9.6
   * struck — see the eyebrow note above):
   *   0.06em   field label · eyebrow · mono badge
   *   0.08em   TABLE COLUMN HEADER (its own value, not the eyebrow's)
   *   -0.01em  HEADING         (here — page title 22 and section title 18)
   *   -0.02em  DISPLAY NUMERAL (below — the 30 rung, never prose)
   *
   * ⚠ VALUE CORRECTED 2026-08-14, AFTER REVIEW. This row shipped as a single
   * -0.015em for "headings", sourced to §9.1's tracking row. -0.015em is the
   * DISPLAY tracking: it is the faithful em conversion of the old
   * --tp-type-display-tracking, which was -0.4px, at the display size
   * (-0.4/28 = -0.0143em) and of nothing else. Applied to a heading it is
   * 50-80% too tight, and §8.2 — the ADOPTED page-head ruling — draws both
   * heading rungs:
   *     page title    22 / 600 / -0.2px   → -0.2/22  = -0.0091em
   *     section title 18 / 600 / -0.15px  → -0.15/18 = -0.0083em
   * (page-head-ruling.html `pageSpec`/`sectionSpec`, and the drawn heads at
   * :132 and :171). The shipped portal already renders exactly that:
   * `.adm-head-title` (admin.css) and `.adm-set-head-title` (admin-settings.css)
   * both declare -0.01em. One em value serves both heading rungs to within
   * 0.03px, so the rung split §8.2 draws in px needs ONE token, not two.
   *
   * WHY THIS IS A READING OF THE BOARDS AND NOT A PREFERENCE. §9.1's own framing
   * is *"§8.2 and §8.3 are already adopted; this register does not re-open them,
   * it extends them"* (PASS-2-CONFLICTS.md). Where a §9.1 row contradicts a value
   * §8.2 drew, §9.1 says §8.2 governs — and §9.5 has already applied that rule
   * TWICE, to §9.1's 19/24 type rungs and to its 800 page-title weight, both
   * struck as *"a ladder row written without reading the ruling beside it."*
   * This is the third instance of the same failure and it is resolved the same
   * way. §9.1 is NOT committed (only §9.2 of the four pass-2 boards is), so if a
   * board ever states -0.015em for a HEADING specifically, that is a fourth
   * pass-2-vs-pass-1 contradiction for the owner — not an edit a session makes
   * from this comment. Guarded by
   * tests/test_design_tokens.py::test_the_heading_tracking_matches_the_adopted_page_head_ruling.
   *
   * Blast radius of the correction: ZERO today. Nothing in any stylesheet reads
   * --tp-type-heading-tracking; its only reach was through the display alias,
   * which is now a token of its own and holds its own value. */
  --tp-type-heading-tracking: -0.01em;

  /* The DISPLAY rung's tracking — a role, not a deprecated alias.
   * This was published on 2026-08-14 as `var(--tp-type-heading-tracking)`, i.e.
   * as one of the retired names awaiting a Stage-3 migration. It is not retired:
   * it is the second tracked heading-family role, and it is the ONLY consumer
   * the old -0.4px ever had (components.css `h1`, the global display rule).
   * Legal on the display numeral only — never on prose, never on a page or
   * section title. The two heading-family trackings are drawn far enough apart
   * that no single token can hold both, which is why the split exists.
   *
   * ⚠ VALUE CHANGED, -0.015em → -0.02em, RULED BY §9.6. The -0.015em was an
   * IMPLEMENTER'S CONVERSION, and this comment used to defend it as one: *"-0.4/28
   * = -0.0143em … -0.015em at the 30 rung is -0.45px, which is what that `h1` has
   * always rendered."* That reasoning was sound and it is spent — §9.6's matrix
   * states the display numeral's tracking DIRECTLY, `30/700 · tabular · −0.02em`,
   * and a board's stated value beats a conversion of a px value taken at a size
   * (28) the ladder no longer has. It also lands on a coherent curve with the two
   * kiosk rows §9.6 rules beside it (32 → −0.02em, 44 → −0.025em), which a
   * conversion pinned to the retired 28 could not.
   * Live blast radius: ONE rule, components.css `h1`, from -0.45px to -0.60px. */
  --tp-type-display-tracking: -0.02em;

  /* THE MONO IDENTIFIER — the one role with a WEIGHT and no SIZE.
   * It BORROWS the size of the text beside it (13 in a table cell, 12 in a meta
   * line, 11 in a badge) and is ALWAYS 500: *"mono reads a step heavier per
   * stroke, so 500 mono sits level with 400 Inter."* §9.5: *"a role that borrows
   * a rung never gets a size argument"* — publishing one would let it drift off
   * the very text it exists to sit level with.
   * DO NOT CONFUSE IT WITH THE MONO BADGE (11/700, above). The badge is a fixed
   * chip that must survive being small; the identifier is a run of characters
   * inside somebody else's line. Both are mono; they share nothing else, and the
   * guard asserts exactly that — one has a size and the other must not.
   *
   * ⚠ §9.6 CONTRADICTS ITSELF ON THIS ROW, AND "BORROWED" IS THE READING TAKEN.
   * The matrix's pair column says `13/500`; the SAME ROW's next column says
   * *"borrows the size beside it"*, the prose says *"a mono identifier takes the
   * size of the text beside it and is always 500"*, and the Five-Weights table
   * puts 500 at *"14 · borrowed"* — not 13. Three of the four say BORROWED, and
   * a literal 13 would contradict §9.5's *"a role that borrows a rung never gets
   * a size argument"*, so no size is published here. It is a judgment call
   * rather than a transcription, which is why it is written down: it belongs on
   * the pending cohesion entry's ratification list (drafted as D-216, a number
   * not yet allocated — see the tab-caption note) beside the eyebrow and the
   * display trackings. Live
   * consequence today: NONE — the deprecated alias --tp-type-mono-id-size
   * resolves to --tp-type-body-dense-size, i.e. 13px, so even the literal
   * reading renders what this row renders. */
  --tp-type-mono-id-weight: 500;
  --tp-type-mono-id-ink: var(--tp-ink);
  --tp-type-mono-id-family: var(--tp-font-mono);

  /* ---- DEPRECATED type names — ALIASES, not values (2026-08-14) ----------
   * Each points at the rung it always held, under the name that says which role
   * holds it. They exist so this batch cannot break a consumer; Stage 3 migrates
   * the reads and deletes these lines. A NEW read of any of them fails
   * tests/styleguide/test_cohesion_token_ladder.py.
   *
   *   --tp-type-title-*     was 22/600. "Title" is the ambiguous name that put a
   *                         KPI (claims.css:367 .dc-eqcost-value) and a section
   *                         head (claims.css:221 .dc-h2) on the PAGE-title rung.
   *   --tp-type-subtitle-*  was 18/600. §8.2 calls this rung the SECTION TITLE;
   *                         "subtitle" reads as the line UNDER a title, which is
   *                         the 13/muted role, not this one.
   *   --tp-type-label-*     was 12/600. The ruled field LABEL is 11 at 0.06em, so
   *                         a token called "label" holding 12 is precisely what
   *                         put `.tp-field-label` on the wrong rung.
   *   --tp-type-body-strong-*  was 13/600. §9.6 names that pair the TABLE-CELL
   *                         IDENTIFIER, and makes "strong body" a contradiction
   *                         in terms: *"a sentence is light, a name is heavy"*, so
   *                         there is no heavy body copy — a 13/600 run is the cell
   *                         that NAMES the row. The old name is why
   *                         `.tp-card-title` reads it and renders a card title at
   *                         13/600 instead of the ruled 15/700. NEW 2026-08-14
   *                         (§9.6 batch).
   *   --tp-type-mono-id-size  was 13px. RETIRED WITH NO REPLACEMENT: mono borrows
   *                         the rung beside it. The alias points at 13 because
   *                         that is the rung `.tp-mono` sits in today (a table
   *                         cell); a mono ID in a meta line or a badge must read
   *                         --tp-type-meta-size / --tp-type-eyebrow-size instead.
   *
   * --tp-type-display-tracking WAS LISTED HERE and is not: it was -0.4px, and
   * re-expressing a px tracking in em is a VALUE fix, not a rename, so there is
   * no successor name to migrate a read onto. It is published above as the
   * display rung's own role token.
   *
   * ⚠ --tp-type-label-weight IS RE-POINTED, 2026-08-14 (§9.6 batch): it was
   * `var(--tp-type-meta-weight)` and is now `var(--tp-type-chip-weight)`. Its job
   * is to hold the VALUE its 23 live readers render — 600 — steady through the
   * rename, and --tp-type-meta-weight stopped holding 600 when §9.6 named the
   * meta line a SENTENCE at 400. Following it there would have re-weighted 23
   * selectors across nine sheets from the token layer, in a batch that owns no
   * page sheet. An alias tracks a value, not a name. */
  /* ⚠ ALL NINE ALIASES ARE DELETED (2026-08-16, cohesion Stage 3). They were
   * published 2026-08-14 to hold their VALUES steady through the rename while
   * Stage 3 moved the call sites, and the call sites are moved. Kept as a note
   * because the names are quoted in prose across the tree, and because WHERE
   * their 61 reads landed is the whole argument for renaming them:
   *
   *   --tp-type-title-*        → page title (the claims record <h1>, the org
   *                              team banner) · section title (`.dc-h2`, and
   *                              the bare `<h2>`) · DISPLAY NUMERAL (the
   *                              equipment cost KPI). Three roles, one alias,
   *                              which is exactly what a name reading "any
   *                              title" buys you.
   *   --tp-type-subtitle-*     → section title. A position, renamed to a role.
   *   --tp-type-body-strong-*  → table-cell identifier · card title. §9.6 calls
   *                              the old name self-contradictory: "a sentence
   *                              is light, a name is heavy, so there is no
   *                              heavy body copy."
   *   --tp-type-label-*        → meta · helper · chip · field label · column
   *                              header · button label · tab label · display
   *                              numeral. EIGHT roles under one word, and the
   *                              word was the one role it never held: the
   *                              ruled field label is 11/700 uppercase and the
   *                              alias was 12/600.
   *   --tp-type-mono-id-size   → nothing. §9.5: a role that borrows a rung
   *                              never gets a size argument, so the migration
   *                              was deleting the declaration.
   *
   * Three published roles gained their FIRST consumer in the same pass — the
   * tab label's two states and the button label's several call sites — because
   * the token layer was deliberately ahead of its consumers and this is the
   * consumers arriving. */

  /* ---- Spacing (4-based rhythm) ------------------------------------------ */
  --tp-space-6: 6px;
  --tp-space-8: 8px;
  --tp-space-12: 12px;
  --tp-space-16: 16px;
  --tp-space-24: 24px;
  --tp-space-32: 32px;

  /* ---- Radii (§9.2 · 01 + §8.3) ------------------------------------------
   * 48 corner radii exist in the bundle. The rule that collapses them:
   *   CARD IS r12 — one radius for every container that sits on the page.
   *   r8  stays legal on CONTROLS and INNER BLOCKS.
   *   r14 is the OVERLAY radius — modals, and nothing that sits in the page.
   *   sheet 22 top · chip 6 · pill 999 · popover 12 (it reads --tp-radius-card).
   * "r8 r9 r10 r11 at card scale" are RETIRED HERE; the TILE radii belong to the
   * §8.3 icon-tile ladder (26·r7 · 34·r9 · 40·r11 · 56·r14 · 64·r16) and are
   * never "fixed" to 12 — that ladder arrives with the `.tp-tile` primitive and
   * has no token here yet. */
  --tp-radius-chip: 6px;        /* chips / kbd hints */
  --tp-radius-control: 8px;     /* buttons, inputs, nav items, inner blocks */
  --tp-radius-card: 12px;       /* EVERY card, panel and popover card */
  /* THE MODAL CARD — OVERLAY ONLY, never a card that sits in the page.
   * NOT the bottom sheet: a sheet is `--tp-radius-sheet` (22, TOP corners only),
   * which the block comment above says and eleven shipped sites already read.
   * NOT the phone confirm either: PASS-3 Ruling 1 gives that 16px top corners
   * only, and NO token holds 16 — the confirm batch must get that ruled rather
   * than reach for 14 or 22. (This comment read "modal / sheet card", which
   * contradicted both the line above it and §9.2 · 06's own header
   * "modal r14 · sheet r22 top · popover r12".) */
  --tp-radius-overlay: 14px;
  /* The 18px squircle of the Stacks app icon and the brand lockup. §8.3 retired
   * `--tp-radius-tile` AS A TILE RADIUS and kept the value under this name,
   * legal on the BRAND MARK ONLY. Its former tile consumers move onto the §8.3
   * ladder (56·r14 or 64·r16). */
  --tp-radius-app-icon: 18px;
  --tp-radius-pill: 999px;      /* badges / pills */

  /* DELETED 2026-08-16 (cohesion Stage 3) — two aliases published 2026-08-14
   * with the instruction "migrate the reads, then delete", and their 17 reads
   * are migrated. Kept as a note because both names still appear in prose:
   *   --tp-radius-card-lg  was 14px, "large cards (Mission Control)". There is
   *                        no large card: §9.2 · 01 collapses 14 radii and a
   *                        second name ("Panel") into ONE flat container at r12,
   *                        and 14 becomes the overlay radius. Its 11 reads
   *                        landed on THREE radii, not the two the retirement
   *                        note predicted — six cards and a popover on r12, two
   *                        overlays on r14, and one inner block on r8.
   *   --tp-radius-tile     was 18px "icon/module tiles", and the audit found it
   *                        consumed by essentially none of the 47 icon tiles it
   *                        was published for while six unrelated surfaces read
   *                        it as a big radius. One of the six was the brand mark
   *                        (kept, as --tp-radius-app-icon), four were controls
   *                        and inner blocks (r8), and the single real tile went
   *                        to the `.tp-tile` ladder, where size, radius and
   *                        glyph are one triple. */

  /* ---- THE SHADOW LADDER — four elevations, chosen by MEANING (§9.1) ------
   * 106 distinct shadow strings exist today. Four elevations replace them, and
   * you pick by what the surface IS, never by how heavy it should look:
   *
   *   e0  NONE      — CARDS. "Cards are flat: hairline only." No raised variant
   *                   exists and none is coming: §9.2 · 01 declines it, because
   *                   elevation means FLOATING and a card sits in the page.
   *                   Deliberately NOT published as a token — a surface at e0
   *                   declares no box-shadow at all, and `var(--tp-shadow-flat)`
   *                   would just be a noisier way to write nothing.
   *   e1  STICKY    — the topbar and the table header. Docked-BOTTOM bars use it
   *                   INVERTED (the cast goes up, toward the content it covers).
   *   e2  FLOATING  — popovers, anchored menus, toasts. NO SCRIM under any of
   *                   them (see --tp-scrim above).
   *   e3  BLOCKING  — modals, sheets, slide-overs. ALWAYS with the 0.45 scrim.
   *
   * Plus the two focus rings (a control concern, not an elevation). NOTHING ELSE.
   *
   * ⚠ e2 and e3 REVISE the two values §8.3 quoted — the popover was `.14` and the
   * sheet `0 -8px 30px .22`. Pass 2 is the later board and wins; the collision is
   * recorded at PASS-2-CONFLICTS.md §A4. Anything citing the §8.3 numbers is
   * stale. */
  --tp-shadow-sticky: 0 1px 2px rgba(15,23,34,0.05);
  --tp-shadow-docked: 0 -6px 20px rgba(15,23,34,0.06);   /* e1, inverted */
  --tp-shadow-floating: 0 12px 32px rgba(15,23,34,0.12); /* e2 */
  --tp-shadow-blocking: 0 24px 70px rgba(15,23,34,0.12); /* e3 */

  /* ---- THE SIX RETIRED ELEVATIONS ARE GONE (deleted 2026-08-16, Stage 3) --
   * This block declared `--tp-shadow-raised / -overlay / -lift / -hero /
   * -drawer / -sheet` from 2026-08-14, keeping their VALUES rather than
   * becoming aliases so that migrating one meant looking at the surface rather
   * than swapping a name. All 23 reads are migrated and the declarations are
   * deleted. Recorded here rather than dropped, because "these six names do not
   * resolve" is the fact a session reading old prose needs:
   *
   *   --tp-shadow-raised  → e0, DELETED. Nine resting shadows: four cards
   *                         (account · system-teams · backlinks panel ·
   *                         notifications results), four selected SEGMENT faces
   *                         (settings tier · claims pen tab · notifications seg
   *                         · team-levels sub-tab) and the onboarding keypad
   *                         key. The last five are the interesting half — none
   *                         is a card, and none is on the ladder either: a
   *                         segment sits in its own track and a key is a
   *                         button, so "floating above the page" describes
   *                         neither. Selection is carried by the surface fill,
   *                         the weight and the ink, which is what it was
   *                         already doing underneath the shadow.
   *   --tp-shadow-lift    → e0, DELETED. The system-teams card hover; §9.2 · 01
   *                         declines the lift with the rest.
   *   --tp-shadow-hero    → e0, DELETED. `.au-card`, `.se-card`, `.ob-card` —
   *                         standalone auth/onboarding cards are still cards.
   *   --tp-shadow-overlay → SPLIT per site, as predicted: e2 for the
   *                         notifications bell popover, e3 for the two admin
   *                         slide-overs and the impersonation modal.
   *   --tp-shadow-sheet   → e3 everywhere (5 reads).
   *   --tp-shadow-drawer  → e3. The nav drawer is a blocking overlay with a
   *                         scrim, and the ladder has no sideways cast.
   *
   * ⚠ THE BOTTOM SHEETS NOW CAST DOWNWARD, and that is the ruling, not a slip.
   * §8.3 drew the sheet at `0 -8px 30px .22` — a cast going UP, toward the
   * content it covers. §9.1 is the later board and collapses it into e3, whose
   * offset is +24y.
   *
   * THE GEOMETRY, because "it casts downward" is easy to read as "it is gone"
   * and that is not true. A 70px blur spreads about half its radius past the
   * offset shape, so e3 still reaches roughly 11px ABOVE a sheet's top edge
   * (24 − 35), against roughly 20px for the retired `0 -8px 24px`. The alpha
   * drops too, 0.18 → 0.12. So the sheet's top edge keeps a soft cast; it is
   * weaker, not absent, and what actually separates a sheet from the page is
   * the 0.45 scrim every one of them carries. *(The 2026-08-16 adversarial
   * review read this as "renders the entire shadow below the viewport" and
   * "the top edge now has none" — overstated for a blurred shadow, which is
   * why the numbers are here now instead of an adjective.)*
   *
   * IF /verify-ui JUDGES THE SHEETS UNDER-SEPARATED AT 390, the in-system fix
   * already has a precedent one line up: `--tp-shadow-docked` is e1 INVERTED,
   * so direction is an axis this ladder already models separately from rung. An
   * inverted e3 would be the same move and would NOT be a fifth rung. It is not
   * done here because nothing has measured it, and the ruling assigns the rung
   * rather than the direction.
   *
   * THE NAV DRAWER IS DIFFERENT AND IS NOT A CANDIDATE. Its old value was a
   * SIDEWAYS cast (`8px 0 30px .28`), and the retirement note above rules that
   * out in as many words: "the ladder has no sideways cast, so the value goes."
   * A reviewer proposing a horizontal e3 is proposing to undo that clause. */

  /* ---- LAYERING — the board's ORDER and NAMES, the app's existing numbers
   * ------------------------------------------------------------------------
   * Pass 2 drew a 10–70 scale. THE OWNER RULED (2026-08-14): adopt the ORDER and
   * the NAMES, keep the app's existing 300-based band. Renumbering changes
   * nothing a user can see and touches every overlay in the codebase; the ORDER
   * is what carries meaning.
   *
   * So these numbers are READ OFF THE SHIPPED LADDER (components.css + shell.css
   * + the four admin overlays Stage 4 placed on it), not invented. Every value
   * below is a z-index that exists in the tree today.
   *
   * THE DIVERGENCE FROM THE BOARD, ON THE RECORD so it is a decision and not a
   * drift — the board's order is
   *   sticky head 10 → docked bar 20 → drawer 30 → popover/menu 40 →
   *   modal/sheet/slide-over 50 → toast 60 → tooltip 70
   * and the app differs in six places (⚠ this read "in FOUR places" and
   * enumerated 1-4 until 2026-08-30, when `--tp-z-rotate-gate` was published
   * below without the count above being moved — a pinned enumeration going
   * stale in the same commit that extends it, which is exactly the rot the
   * anti-rot rules legislate against. The new rung's own comment was written;
   * this paragraph is the one that carries the COUNT):
   *   1. POPOVER SITS ABOVE MODAL, not below it (340 vs the modal scrim's 300).
   *      The board has modal 50 over popover 40. The app's inversion is DECIDED
   *      and documented at components.css:3866-3878 — a phone push and a
   *      bottom-sheet popover never coexist, so the stack cannot actually invert.
   *   2. THE BOTTOM SHEET SHARES THE POPOVER RUNG (340), because on phone the
   *      popover IS a bottom sheet — one primitive, one rung. The board files
   *      "sheet" with modal at 50.
   *   3. CONFIRM (360) IS A RUNG THE BOARD DOES NOT NAME. It has to out-layer
   *      whatever raised it, including an open popover — at 70 it painted BEHIND
   *      the row menu that raised it, live, on the TM Policies pane.
   *   4. THE SKIP LINK (500) is above the toast region. Chrome for keyboard
   *      users; the board's scale stops at the tooltip.
   *   5. THE LANDSCAPE-PHONE GATE (600) is a rung ABOVE the board's ceiling,
   *      and the board names nothing there at all — its scale stops at the
   *      tooltip, three rungs below. DERIVED like the docked rung rather than
   *      chosen: the gate's whole claim is that the portal is not presented
   *      sideways on a phone, so it has to out-paint everything that can be
   *      open when the phone turns — including the skip link at 500, which is
   *      the highest thing the app ships. This band steps by 100 (300 · 400 ·
   *      500), so 600 is the next step and not a new numbering scheme. Owner
   *      ruling 2026-08-30; see the rung's own comment below.
   *   6. THE SANDBOX ENVIRONMENT FRAME (380) is a rung the board does not name,
   *      published 2026-09-16 with its only consumer (Phase 6.22 Task 5). It sits
   *      between the confirm and the toast region for the reason on the rung
   *      itself. (⚠ The count above read "five" until this item was added —
   *      moved in the same edit, which is the point of the note on it.)
   * ⚠ ONE OF THOSE TWO RUNGS NOW HAS A CONSUMER, AND IS PUBLISHED BELOW. This
   * paragraph read "TWO BOARD RUNGS HAVE NO LIVE CONSUMER and are deliberately
   * NOT published: the DOCKED BAR (no fixed bottom bar ships yet;
   * --tp-shadow-docked is its elevation and arrives first) and the TOOLTIP".
   * That was a statement with an expiry date on it — the docked bar arrived
   * with cohesion Stage 2 (§8.3 · 08), and its elevation had indeed arrived
   * first, exactly as predicted. --tp-z-docked is published beside the sticky
   * rung it must out-layer. THE TOOLTIP RUNG IS STILL WITHHELD: the portal has
   * no tooltip primitive, `title=` is not one, and publishing it would still be
   * inventing a number.
   *
   * The docked value is DERIVED, not chosen: the board's order puts the docked
   * bar directly above the sticky head and below the drawer, the app's sticky
   * rung is 2, and 3 is the next integer in that same local band. It preserves
   * the ruled ORDER (sticky 2 < docked 3 < drawer 301) without renumbering
   * anything, which is the whole shape of the owner's ruling here. */
  --tp-z-sticky: 2;    /* rung 1 · a sticky head INSIDE its own scroll container.
                        * LOCAL, not a global rung: the live sites use 1 and 2, and
                        * 2 is the higher of the two shipped values — adopting it can
                        * only raise a head over the rows it passes, never lower it. */
  --tp-z-docked: 3;    /* rung 2 · the docked bottom bar (§8.3 · 08). ABOVE the
                        * sticky head and below everything that blocks; see the
                        * derivation note in the block comment above. */
  --tp-z-scrim: 300;   /* the blocking scrim: modal, nav drawer, impersonation */
  --tp-z-drawer: 301;  /* rung 3 · the nav-drawer PANEL, over its own scrim */
  --tp-z-push: 320;    /* rung 5 · the phone full-screen push (slide-over) */
  --tp-z-reading: 330; /* the Page Details explainer — over the modal band, under
                        * the popover, so the panel it reopens into paints on top */
  --tp-z-popover: 340; /* rung 4 · popover · anchored menu · select menu · bottom sheet */
  --tp-z-confirm: 360; /* above everything a confirm can be raised from */
  --tp-z-env-frame: 380; /* the SANDBOX environment frame (Phase 6.22 Task 5) — a
                        * 3px ink border around the viewport, `pointer-events:
                        * none`. DERIVED, not chosen: it must out-paint every
                        * overlay that can reach a viewport EDGE (scrim 300 …
                        * confirm 360), because the board draws the slide-over
                        * (3a) and the full-screen phone picker (3b) inside the
                        * frame; and nothing above the confirm needs to be under
                        * it — the toast region paints inside its own 12-16px
                        * gutter and the skip link 12px in, so neither ever meets
                        * a 3px edge. 380 is the midpoint of that gap, so a later
                        * rung on either side has room. Its consumer is
                        * `.sbx-env-frame` in sandbox-chrome.css — its OWN sheet,
                        * for the rotate gate's reason. Absent from every page of
                        * the real portal. */
  --tp-z-toast: 400;   /* rung 6 · the toast region */
  --tp-z-skip: 500;    /* the skip link, above the toasts */
  --tp-z-rotate-gate: 600; /* the landscape-phone gate (owner ruling 2026-08-30,
                        * "don't allow sideways on iPhone"). Above EVERYTHING,
                        * including the skip link: the gate's whole claim is
                        * that the portal is not presented sideways on a phone,
                        * and a rung that some overlay can out-paint would make
                        * that claim false on exactly the surfaces most likely
                        * to be open. DERIVED like the docked rung, not chosen:
                        * this band steps by 100 (300 · 400 · 500), the gate has
                        * to sit above the top of it, and 600 is the next step.
                        * Its consumer is `.tp-rotate-gate` in rotate-gate.css
                        * — its OWN sheet, because the gate must also reach the
                        * eight documents that own their own <head> and link
                        * neither components.css nor shell.css. */

  /* ---- Control heights ------------------------------------------------------ */
  --tp-control-h-sm: 34px;      /* compact buttons */
  --tp-control-h-md: 36px;      /* default buttons, topbar search */
  --tp-control-h-input: 38px;   /* inputs / selects */
  --tp-control-h-topbar: 56px;
  --tp-control-h-topbar-phone: 52px;  /* phone topbar (see Responsive system) */

  /* ---- Layout ----------------------------------------------------------------- */
  /* The single sidebar width. D-110 retired the 56px icon-rail tier: the
   * sidebar shows in full whenever it fits, otherwise it becomes an
   * off-canvas drawer (no intermediate icon-only state). */
  --tp-sidebar-w: 236px;

  /* ---- Responsive system (responsive-kit.jsx) ---------------------------------
   * Three breakpoints (phone < 768 / tablet 768-1023 / desktop >= 1024).
   * Below tablet the sidebar becomes an off-canvas drawer and overlays
   * become full-screen sheets / bottom sheets with >=44px touch targets.
   * The drawer scrim reads the ONE scrim, --tp-scrim. (This line said it
   * "reuses --tp-scrim-heavy (same rgba(15,23,34,0.5))" until 2026-08-14 —
   * true of the two-token system, false of the collapsed one, and the kind of
   * sentence that re-forks a value somebody has just merged.) */
  --tp-bp-tablet: 768px;
  --tp-bp-desktop: 1024px;
  --tp-drawer-w-phone: 86%;       /* off-canvas drawer width on phone... */
  --tp-drawer-w-phone-max: 300px; /* ...capped here */
  --tp-drawer-w-tablet: 280px;    /* fixed drawer width on tablet */
  --tp-radius-sheet: 22px;        /* bottom-sheet top corners (a --tp-radius-* value; lives here with the sheet system) */
  --tp-touch-min: 44px;           /* minimum interactive target below tablet */
  --tp-touch-cta: 48px;           /* phone sheet / docked-bar CTA height (the
                                     primary action sits a notch above the
                                     44px floor so it reads as the target).
                                     ✅ RULED — D-225, 2026-08-16, SUPERSEDING
                                     D-224(3) the same day.
                                     ⚠ THIS TOKEN WAS 46, AND THE REASONING FOR
                                     46 IS WHY IT IS NOW 48. D-224(3) held it at
                                     46 on PASS-3's principle — "before deleting
                                     a value, check whether it is UNIFORM;
                                     uniformity is the evidence that someone
                                     already ruled" — and added that moving
                                     "would re-tune every phone sheet CTA in the
                                     portal, or fork the docked bar off them."
                                     That cost claim does not survive its own
                                     evidence: every consumer reads THIS NAME
                                     and none hand-types a height, so the move
                                     was one declaration and all of them
                                     followed. The uniformity offered as the
                                     reason not to move is exactly what made
                                     moving free.
                                     On the value itself, 48 is drawn by six
                                     sources (§8.3 · 08; the register's
                                     button-height row; the Cohesion Primitives
                                     spec; the System Summary height table; the
                                     Screen Skeleton region table; §8.2's own
                                     390 head) and the independently-built
                                     Stacks Design System publishes it as
                                     `--stk-h-docked-cta: 48px` with "migrate to
                                     48". NOTHING draws 46 — it was the shipped
                                     incumbent, which is the class of value this
                                     remediation exists to converge. */
  /* A TABLE ROW's height on a coarse pointer — Chrome board 5a (2026-09-17),
   * owner-ruled PORTAL-WIDE ("Yes, portal-wide", the T9 build plan §J): "Table
   * rows go 44 → 48". A notch above the 44 floor so a finger lands between
   * hairlines. Its PRIMITIVE consumer is `.tp-table tbody tr` in components.css's
   * coarse block, which has NO phone twin (below 768 a table row is a stacked
   * card whose height the phone band releases to auto — test_table.py says why).
   * WHO ELSE READS IT IS A REGISTER, NOT A NUMBER WRITTEN HERE:
   * tests/styleguide/test_table.py's `_ROW_FLOORS` is the list of page rows that
   * take this floor, and it fails when a row is left out of every category.
   * (2026-09-23, Sandbox close-out re-shoot FND-6: this read "Its ONE consumer is
   * `.tp-table tbody tr`" while five page sheets already read the token — a
   * count that rotted the day the second consumer landed.)
   *
   * NOT AN ALIAS OF --tp-touch-cta, which is a CTA height that happens to share
   * the value: a row and a button are different roles, and aliasing would move
   * one when the owner re-rules the other. (The --tp-safe-tap → --tp-touch-min
   * alias exists because those ARE one value in two vocabularies.) */
  --tp-touch-row: 48px;
  /* THE TOUCH BAND'S *TEXT* FLOOR — the third member of this family, and the
   * only one measured in type. 44 and 48 are how big a finger's TARGET must be;
   * this is how big a focusable control's TEXT must be before the DEVICE stops
   * intervening. iOS Safari auto-zooms the page whenever a focused input /
   * select / textarea computes UNDER 16px, and the portal typed every control at
   * 14 (`--tp-type-control-size`), so every field in the app tripped it: the iOS
   * simulator sweep measured the page jumping to 1.144x on focus and pushing
   * ~47px of the form off-viewport (F-IOS-12, ui-audit-ios-2026-08-30).
   * Owner ruling 2026-08-30: "the zoom thing for tapping text … I don't like
   * that, don't zoom in on the text field."
   *
   * WHY THIS IS NOT A `--tp-type-*` TOKEN, and must never become one. 16 IS NOT
   * A RUNG — §9.5 ruling 2 closes the portal ladder at 11 · 12 · 13 · 14 · 15 ·
   * 18 · 22 · 30, and §9.6 closes the role set at 26 (both are asserted:
   * test_design_tokens.py::test_every_published_size_is_one_of_the_eight_rungs
   * and test_cohesion_token_ladder.py::test_the_published_role_set_is_exactly_
   * the_matrix_and_never_a_twenty_seventh). Redefining --tp-type-control-size to
   * 16 under a media query would fail the first of those AND would drag a
   * NON-focusable consumer with it — `.tp-typeahead--select .tp-typeahead-opt-title`
   * reads the control role for a dropdown ROW, which no device ever zooms and
   * which would simply grow. So this is a DEVICE THRESHOLD wearing the touch
   * family's name, not a typographic choice: the board still rules the portal's
   * control text at 14/500, and that is exactly what a fine pointer renders.
   *
   * WHY NOT `maximum-scale=1` ON THE VIEWPORT META, which is the usual fix.
   * That suppresses PINCH-ZOOM for every user on every browser — a real
   * accessibility cost, paid app-wide, to stop one device behaviour. Raising the
   * text past the threshold removes the browser's REASON to zoom instead of
   * removing the user's ability to, so the meta tag stays untouched. */
  --tp-touch-text-min: 16px;
  /* THE DOCKED BAR'S OWN HEIGHT (§8.3 · 08), published so "no page hard-codes
   * 106". Its parts are the bar's phone padding, its CTA and the device inset,
   * summed here so the reserve can never drift from the bar that casts it —
   * tests/styleguide/test_dock.py asserts this calc against the shipped rule's
   * actual declarations rather than trusting the arithmetic.
   *
   * A page reserves `padding-bottom: var(--tp-dock-h)`. It NEVER writes the
   * inset itself: "a page that writes 34px directly is the bug this token
   * exists to prevent." Note the bar ships `position: sticky`, which already
   * keeps its own flow box, so the reserve is belt-and-braces for scroll
   * containers rather than the only thing standing between the last field and
   * the bar. The value is the no-meta-line height; a bar carrying a progress
   * line is taller, and reserving for THAT is the placer's, per §8.3 · 08. */
  --tp-dock-h: calc(var(--tp-space-12) + var(--tp-touch-cta) + var(--tp-space-12) + var(--tp-safe-bottom));
  /* phone topbar height is --tp-control-h-topbar-phone, co-located with the other control heights above */

  /* ---- Motion ------------------------------------------------------------------ */
  --tp-spin-dur: 0.7s;            /* spinner sweep (.rk-spin / rk-spin keyframe, linear) */
  --tp-indet-dur: 1.1s;           /* indeterminate progress sweep (rk-indet keyframe) */
  --tp-disclosure-dur: 0.15s;     /* expand/collapse motion (R9): nav-folder height + phone
                                     search expand — pinned to the portal-nav chevron's 0.15s
                                     so the chevron rotation and the panel reveal stay in lockstep
                                     (owner-requested motion polish, Phase 1 sign-off) */
  --tp-spinner-track-on-fill: rgba(255, 255, 255, 0.35); /* spinner ring on a filled (ink/ok) BusyButton — the bundle's Spinner track on color===#fff */
  --tp-pop-in-dur: 140ms;         /* anchored-popover ENTER — fade + scale 0.97→1 out of the
                                     caret. The one designed timing in the Page Details motion
                                     spec with no shipped primitive to match, ruled onto the
                                     shared .tp-popover (build spec PD-A13) */
  --tp-pop-out-dur: 110ms;        /* anchored-popover EXIT — fade ONLY, no scale
                                     ("dismissal shouldn't animate"). Shorter than the enter
                                     on purpose: leaving should not cost the user time */

  /* ---- Device chrome / safe areas (Phase 4.6) --------------------------------
   * The ONLY place env(safe-area-inset-*) may appear — CI-guarded
   * (tests/test_device_chrome.py). Zero in an ordinary browser tab; non-zero
   * once base.html's viewport-fit=cover puts the shell under the Dynamic
   * Island / rounded corners / gesture bar in standalone display. Fixed or
   * edge-docked surfaces consume the token (topbar, drawer, bottom sheets,
   * docked bars, toasts) — never raw env(). The 0px fallback keeps every
   * calc() valid where env() is unsupported. */
  --tp-safe-top: env(safe-area-inset-top, 0px);
  --tp-safe-right: env(safe-area-inset-right, 0px);
  --tp-safe-bottom: env(safe-area-inset-bottom, 0px);
  --tp-safe-left: env(safe-area-inset-left, 0px);

  /* Orientation-INDEPENDENT device-chrome constants (2026-08-06 safe-area
   * audit). Plain lengths, NOT env() reads: the hardware geometry they describe
   * does not change when the device rotates — env() already reports the four
   * insets per orientation itself — so these never sit behind an orientation
   * media query, and being ordinary lengths they leave the CI guard above
   * untouched. */
  --tp-safe-corner: 16px;      /* min clearance from a display corner — nothing interactive inside it */
  --tp-safe-radius: 55px;      /* the true display corner radius that clearance is measured against */
  --tp-safe-gutter: 16px;      /* standard content gutter; the side rule is max(gutter, safe-side) — the
                                  LARGER of the two, never the two stacked */
  --tp-safe-tap: var(--tp-touch-min);  /* absolute tap-target floor — ALIAS of
                                   * --tp-touch-min (:190): one value, two
                                   * vocabularies (the safe-area handoff names
                                   * it this). Never diverge them. */
  --tp-safe-tap-field: 52px;   /* primary-action floor on field surfaces — wet/gloved hands.
                                  ⚠ This read "(consumers arrive with UCheck 6.3)" from the
                                  2026-08-06 safe-area audit until 2026-09-10, when they DID:
                                  UCheck Mobile's M0 token layer publishes --uc-safe-tap-field
                                  at the same 52px. It is not an alias of this one — that layer
                                  transcribes its values from its own design source, so an alias
                                  would no longer be the length that source publishes — it is
                                  BOUND to it instead, by
                                  tests/styleguide/test_ucheck_tokens.py::
                                  test_the_shared_hardware_constants_equal_the_portal_s, which
                                  parses this file. Four more constants are bound the same way:
                                  --tp-safe-corner / -radius / -gutter / -tap. Change one of the
                                  five here and that test names the other home. */

  /* ---- The Sandbox environment bar (Phase 6.22 Task 5, Ask 2) ------------------
   * TWO tokens, both 0px here, which between them are everything the shell does to
   * make room for the ink bar a SANDBOX instance fixes below the status bar.
   *
   * --tp-env-bar-h — the bar's own height. Its consumers, and what each does:
   *   - `.tp-shell` (shell.css) — its margin-top and height, together with the
   *     token below. The shell adds NO --tp-safe-top of its own.
   *   - the three surfaces docked BELOW the phone topbar — the phone toast stack
   *     (components.css `.tp-toast-region`), the phone search sheet (search.css
   *     `.gs-sheet`) and the typeahead takeover (`.tp-typeahead-screen-is-open`) —
   *     as a `margin-top`, beside the `top` expression that already carries
   *     --tp-safe-top and that tests/test_device_chrome.py pins byte-exact.
   *   - the standalone documents' top rows (login.css `.lg-hero` / `.lg-panel`,
   *     auth.css `.au-page` / `.au-brand` / `.se-card`, onboarding.css `.ob-top`),
   *     in the calc() that already adds --tp-safe-top.
   *   - `.adm-set` (admin-settings.css), whose min-height is "the screen less the
   *     chrome above the console".
   *   - the phone drill-down PUSH (components.css `.adm-drill`, <768 only), whose
   *     `top` is this token plus the one below — Chrome board 5d, 2026-09-22: the
   *     full-screen picker "stops at the strip's bottom edge". Every drill-down
   *     push in a Sandbox does the same, because the picker IS the drill-down.
   *   NOT the phone drawer: `.tp-rail` keeps `top: 0` in a Sandbox, because an
   *   overlay covers the bar (SBX-A50, argued at the rule in shell.css; board 5d
   *   draws the picker, not the drawer, so its drawer half stands).
   *   (⚠ This list read "`.tp-shell`, the phone drawer, the phone toast stack, the
   *   phone search sheet, the typeahead takeover and the standalone documents'
   *   top rows all add it where they already add --tp-safe-top" when the token was
   *   published — wrong twice in the commit that wrote it: the drawer never read
   *   it, and `.tp-shell` adds no inset at all. Caught by the Task 5 review.)
   *
   * --tp-env-safe-top — the TOP INSET the bar takes over from the shell's first
   * row (SBX-A56). The bar sits below the status bar, so in a Sandbox it is the
   * bar, not the shell's first row, that owns the band above it: `.tp-shell`
   * starts below bar + inset, and the three first rows that reserve the inset on
   * the real portal — `.tp-topbar-phone`, /offline's `.tp-main >
   * .tp-content:first-child`, and the phone drill-down push's header
   * (components.css `.adm-drill-panel > .adm-drill-header`, Chrome board 5d: "the
   * picker's header does not reserve the top inset — because it is not at the
   * top of anything") — subtract this token so they do not reserve it twice.
   * (This sentence named TWO first rows until 2026-09-22, when 5d moved the push
   * under the strip.) That is what lines up EVERY first row under the bar, including the ones
   * that never reserve the inset: the desktop `.tp-topbar`, the violet
   * `.tp-imp-bar` and the ≥1024 rail. (⚠ The first build shifted the shell by the
   * bar's height alone and relied on the first row to absorb the inset, which
   * only the phone topbar and /offline do — so with a top inset the bar sat on
   * top of the violet bar's "Logged in as" and its Return button, and the 390
   * merged violet bar carried SANDBOX inside the status-bar band.)
   *
   * ⚠ BOTH ARE 0px HERE AND MUST STAY 0px HERE. The real portal never renders the
   * chrome, so every consumer adds or subtracts zero and nothing moves
   * (tests/test_device_chrome.py's INSET_ZERO_METRICS + the flag-off geometry cell
   * in tests/functional/test_fn_sandbox.py). Only sandbox-chrome.css gives them a
   * value (the bar's heights and the inset — read that sheet for the numbers; this
   * line quoted "26px, 22px below 768" until the 2026-09-22 build of boards 5a/5b
   * made it 26 / 44 on a coarse pointer / 44 below 768), and only under
   * `:root:has(> body > .sbx-env)` — i.e. only while the chrome is in the
   * document, which is only inside a Sandbox (tests/test_sandbox_chrome.py). */
  --tp-env-bar-h: 0px;
  --tp-env-safe-top: 0px;

  /* ---- Damage Claims: the DC-A12 red pen -------------------------------
   * The pen is a DRAWING colour, not a theme colour: it is what the submitter
   * drew with and what the reviewer must see, so it does not track the palette.
   * Published here anyway because the app-wide rule is that no stylesheet
   * carries a raw hex — a value with a name is a value somebody can find. All
   * three are verbatim from the design bundle (`damage-claim-system.jsx`
   * MARKUP_STROKE + the portal pen's own chip colours).
   */
  --tp-pen-ink: #E11D48;
  --tp-pen-chip-bg: #FCE7EC;
  --tp-pen-chip-ink: #A8112E;
}
