/* Tessella Health design tokens v3 — monochrome green system.
   THIS FILE IS THE SOURCE OF TRUTH. docs/brand-system.html links it and
   renders from it; the artifact at
   https://claude.ai/artifact/9ZqbbprZ5in29maRCdSRWC publishes that page with
   this file beside it. Anything that disagrees with this file is the bug.
   (Until 23 Sep 2026 this header said the opposite -- that the page was
   canonical and these values were copied from it. That was true when the
   page held its own literals, and it is what let the two drift.)
   Every value below carries its derivation at the line: a stated rule, a
   formula, or a named source. Where a value has none because it was chosen
   -- the three brand colours -- the line says so rather than inventing one.

   The only token file. The four-colour system it replaced was deleted
   3 Oct 2026 (in git history).

   Accessibility target: WCAG 2.2 AA. In practice that means 4.5:1 for text,
   3:1 for large text and for the boundaries of UI components, a visible
   :focus-visible indicator on every interactive element, and a 44px minimum
   target. Every colour pair in this file has been measured against it, and
   Seven non-exempt pairs fall short of the 3:1 a boundary is held to, and
   each says so at its own line: --input-border and --button-secondary-border
   at 2.36, --card-border and --progress-track at 1.48, --chart-gridline at
   1.19, --chart-200 at 2.20. Each is either not load-bearing (a card is
   identified by its content, a gridline is meant to recede) or carries a
   stated relief. An audit found the note here claiming "two".

   Rules carried from the system:
   - The whole colour palette beyond neutrals is THREE values: aura, mint,
     blush. There is no mid-tint; an element is full-strength aura or grey.
   - Every neutral is green-tinted, ANCHORED NEAR ink's hue (the ramp runs
     OKLCh h 164.9-172.2; ink itself is 163.8, just outside it) and
     near-zero chroma (0.002 rising to 0.019). White is the true canvas and
     is not tinted.
   - Aura is a FILL with ink content. It is never text on white and never
     marks up copy. The lockup is monochrome on every ground: the logo's job
     is legibility, aura's job is to be the one thing that never changes.
   - assets/lockup/lockup-horizontal-mono.svg carries no colour of its own
     (fill="currentColor"), so
     it takes the text colour of wherever it sits. It must be INLINED to do
     that — referenced through <img> it is an isolated document and falls
     back to black.
   - Primary buttons are ink. Aura is reserved for the single highest-intent
     CTA on a page. There is no aura wordmark.
   - No pills, no circles: two radii only, 6 for controls and 12 for every
     larger surface. A radius reads as a proportion, so one value cannot
     serve both ends: 12 on a 44px control is 27% of its height per corner
     and starts to read as a pill.
     The exemption is SHAPE THAT CARRIES MEANING -- a radio, a toggle, and
     a person's avatar. See --control-box and --avatar-person.
   - No dark mode, and no ink bands either. Ink is a FILL for small things
     -- the primary button, a checked control, a tooltip -- and a ground for
     the LOGO, because a dark-ground lockup is needed for places we do not
     control: a partner's site, a deck, dark browser chrome, merch. It is not
     a layout device. Measured: Ramp, Stripe and Mercury are light end to
     end, and Stripe's one dark section is a photograph with an overlay.
     A note here once called ink bands "a design element, not a mode". It had
     no source and it implied a spec nothing needs: there is no ink ground
     with a control inside it anywhere in this system, and a focus rim is
     drawn outside its element, so on a primary button it lands on white.

 */

/* ---- Google Fonts ----
   COMMENTED OUT DELIBERATELY. An @import blocks rendering until it
   resolves, so a consumer should load these with a <link rel=preconnect>
   plus a <link rel=stylesheet> in the document head instead -- which is
   what docs/brand-system.html does. Uncommenting this is a regression, not
   a fix. Without one or the other you get Georgia and the system sans.
   @import url('https://fonts.googleapis.com/css2?family=Lora:wght@600&family=Plus+Jakarta+Sans:wght@400;500;600&display=swap');
   ---- */

:root {

  /* ============================== COLOUR ============================== */

  /* ---- anchors ---- */
  --canvas: #FFFFFF;              /* the true base, untinted */
  --ink: #021A10;                 /* OKLCh h 163.8, aura's family (aura 158.4) */
  --ink-hover: #172C23;           /* +8.9 L*, interaction state only */
  --ink-press: #2E3F38;           /* +17.8 L*, interaction state only */

  /* ---- the three brand colours: aura, mint, blush ---- */
  /* PROVENANCE. Aura, mint and blush were CHOSEN, not computed -- they are
     the brand's, and a brand colour is a decision, not a derivation. That
     satisfies the house rule, which asks for a stated rule OR a stated
     source: the source is the palette lock, and the three are fixed. What
     IS derived from them is everything downstream -- the neutral ramp's
     hue, the status family's chroma ceiling, the chart ramp, aura's own
     interaction states -- and those carry their arithmetic where they sit. */
  --aura: #2BEF9C;                /* the brand colour; CTAs, pops, small accents */
  --aura-hover: #16D486;          /* -8.9 L*, interaction state only */
  --aura-press: #0FB972;          /* -17.8 L*, interaction state only */
  --mint: #E7FDF4;                /* one pale mint, soft wash */
  --blush: #FDE7F0;               /* one pale pink, atmosphere only */

  /* ---- dust: deeper mint and blush, for the hero's dust and nothing else ----
     By decision 2026-09-24 (Jefferson: the dust in mint and blush was too
     light to see; mint measures 1.06:1 on white, blush 1.18:1). The one
     exception to the three-colour rule, and it is scoped: these colour
     specks of dust, never a fill, border, text or surface.
     DERIVATION. A tint is its colour thinned with white, so a deeper tint is
     the same colour thinned less: hold OKLCh hue, hold the ratio of chroma
     to the distance from white, C / (1 - L), and move L. Mint: h 169.3,
     C/(1-L) 1.055. Blush: h 350.9, C/(1-L) 0.514. L is solved for 1.51:1
     on white, which is --aura's own contrast on white: as dark as the dust
     can go without outweighing the brand colour. (It was 1.40 until
     Jefferson asked for it a little darker; 1.60 was tried and puts mint
     outside the sRGB gamut and darker than aura.) Mint lands at L .843
     C .164, in gamut.
     Blush then takes MINT-DUST'S OKLCh LIGHTNESS, L .843, by the same
     chroma rule (C .081), so the two carry equal weight in the dust: at
     equal contrast blush read lighter, its chroma being a third of mint's.
     That landed it at 1.68:1 on white. (It was #F8C4DB, 1.51:1, until
     Jefferson asked for it a bit darker.) On 28 Sep he asked for it darker
     again, and it took the same step once more: that change was L .873 to
     .842, so L .842 to .811, C .097 by the chroma rule, h 350.9. It lands
     at #F2A8C9, 1.88:1 on white, in gamut. Blush now sits one step darker
     than mint-dust, not at its lightness. Holding chroma flat instead gave #C9DED6, a grey-green that
     sits beside --g6 and does not read as mint. */
  /* SHAPE. Dust specks are soft tessera diamonds, the brand's own motif,
     so the no-circles rule holds. (Soft round specks were tried first and
     would have needed an exception; diamonds were preferred, 2026-09-24.) */
  --mint-dust: #20EDBA;           /* 1.51:1 on white, = aura's */
  --blush-dust: #F2A8C9;          /* 1.88:1 on white; one step below mint-dust's lightness */

  /* ---- neutrals: 12-step green-tinted grey ramp ----
     1-2 backgrounds, 3-5 component fills, 6-8 borders, 9 solid, 10-12 text.
     Measured in OKLCh, because the "~155deg" this note used to claim was an
     HSL hue and matches nothing here. The ramp runs h 164.9 to 172.2, a 7.3
     degree band, sitting just above INK's own 163.8 and well clear of
     aura's 158.4. The ramp is anchored NEAR ink, not exactly at it. A neutral
     family anchored to ink is the right call; it just was not what was
     written.
     Chroma climbs 0.002 at g1 to 0.019 by g8 and then plateaus, so the pale
     end is all but pure grey and the tint only becomes visible as the steps
     darken. That is deliberate -- chroma reads less at high lightness -- and
     Radix's scales do the same.
     Lightness steps are NOT even, also deliberately: 0.017, 0.020, 0.018,
     0.029, 0.041, 0.058, 0.080, 0.158, 0.070, 0.074, 0.186. They are tuned
     per band rather than spaced evenly, which is how Radix builds a 12-step
     scale for this same background / fill / border / solid / text structure.
     The two large jumps, g8 to g9 and g11 to g12, are the gaps between
     bands. Material 3 would space these evenly; we do not.
     The text band starts at 10, not 11. g9 measures 4.36:1 on white, under
     the 4.5:1 AA floor for normal text, so it is a fill and an icon colour
     (it clears the 3:1 that graphics ask for) and never a text colour.
     g10 at 5.85:1 is the lightest step that can carry type. */
  --g1:  #FBFDFC;
  --g2:  #F4F8F6;
  --g3:  #ECF2EF;
  --g4:  #E4EDE9;
  --g5:  #D9E4DF;
  --g6:  #CBD7D2;
  --g7:  #B7C5BF;
  --g8:  #9DACA6;
  --g9:  #6E7C77;
  --g10: #5A6862;
  --g11: #46534E;
  --g12: #17231F;

  /* ---- alpha: ink at a few opacities, for use over any ground ---- */
  /* Unused. Three divider values already ship -- --border-hairline g6 for
     section rules and card edges, --table-divider g3 inside a table, and
     --menu-separator g6 -- and none of them is an alpha. This one predates
     them and has no job; it should go when something confirms it is not
     wanted for a divider over a photograph. */
  --alpha-divider: rgba(2, 26, 16, .08);
  --edge:          rgba(2, 26, 16, .14);   /* hairline over any ground */
  --alpha-scrim:   rgba(2, 26, 16, .40);   /* behind a modal. 40% is what
                                              Radix (rgba(0,0,0,.4)) and
                                              Primer (#C8D1DA66) both use.
                                              Was .55, from no source. */

  /* ---- semantic roles ---- */
  --bg: var(--canvas);
  --bg-subtle: var(--g1);
  --bg-muted: var(--g2);

  --fill-rest: var(--g3);
  --fill-hover: var(--g4);
  --fill-active: var(--g5);

  --border-hairline: var(--g6);   /* section rules, card edges, dividers */
  --border-control: var(--g7);    /* inputs, secondary buttons */
  --border-strong: var(--g8);

  --text-primary: var(--ink);     /* headings and primary copy */
  /* --text-strong is g12 at 16.20:1 against --text-primary's 18.14:1, one
     step under ink. Used by the stat cards' bold lines (Jefferson, 30 Sep:
     "one step below the black"). */
  --text-strong: var(--g12);
  --text-body: var(--g11);
  --text-muted: var(--g10);       /* 5.85:1. Was --g9 at 4.36:1, an AA
                                     failure wherever muted text appeared --
                                     which was every caption, eyebrow, table
                                     header and spec on the system page.
                                     --input-placeholder already sat at g10
                                     for the same measured reason. */
  --text-on-ink: var(--canvas);   /* #FFFFFF; the logo on ink uses this too */
  /* THE FOOTER'S INK BAND -- the one exception to "no ink bands", by
     decision, Jefferson 2026-09-25: the homepage footer is the live site's
     dark footer in the new colours, an ink ground under the page that
     anchors it. Nothing else takes an ink ground. */
  --footer-bg: var(--ink);
  --footer-text: var(--text-on-ink);
  --text-on-ink-muted: rgba(255, 255, 255, .72);   /* ~11:1 on ink; the slogan and legal line */
  --footer-rule: rgba(255, 255, 255, .16);         /* the live footer's 16% rule over the legal row */
  --text-on-aura: var(--ink);

  /* ---- gradient: the one brand moment, mint -> white -> blush ---- */
  --gradient-brand: linear-gradient(90deg, #E7FDF4 0%, #FFFFFF 56%, #FFFFFF 72%, #FDE7F0 100%);
  /* Rebalanced 2026-09-28 (Jefferson: "more green and less blush ... more
     of a white gradient in the middle"). Was mint 0-46, white 46-54, blush
     54-100: two equal ramps round an 8% white band. The white band doubles
     to 16%, and the remaining 84 splits two to one for mint, the inverse of
     the dust's two-to-one lean to blush: mint ramps 0-56, white holds
     56-72, blush ramps 72-100. The end colours are unchanged. Behind the
     hero (a layer 24% wider than the card) the visible edges now reach 83%
     mint on the left, from 79%, and 65% blush on the right, from 79%. */
  /* The homepage hero uses this at full strength. A 45%, then 60%, version
     (--gradient-brand-soft) was tried first and retired 2026-09-24: behind
     the hero it could not be seen at all (Jefferson: "no one can tell that
     it's there"). Mint and blush are the palest values in the system, so
     anything below full strength all but disappears on white. */

  /* ---- background texture: tessera diamonds, 1.8px on a 13px grid ----
     tile colour snapped from #C4CFCB (a step of the superseded 11-step ramp)
     to --g6 #CBD7D2, the nearest step of the shipped ramp.
     Then --g8 #9DACA6, 2026-09-25 (Jefferson: "way too light, I don't see
     the dots"). g8 is the darkest step of the border band (6-8) and stays
     below g9, the solid step, so the grid remains a graphic, never type.
     The tile's fill hex and --grid-dot must match.
     Then --g7 #B7C5BF, the same day (Jefferson: "less visible throughout"),
     one step lighter, the middle of the border band. The "too light" that
     prompted g8 was a stale tokens.css leaving the canvas with no colour,
     so g6 itself was never judged.
     Then back to --g6 #CBD7D2, the same day, now actually seen (Jefferson:
     "just a little bit too noticeable ... a little bit lighter"): one step
     lighter again, the lightest of the border band.
     Edges: the grid never stops on a hard line. It fades linearly from
     nothing at a section's top edge to full strength where the content
     begins (the top padding), and back to nothing across the bottom
     padding. The fade lengths ARE the section padding, so no new values.
     Jefferson, 2026-09-25: "not a hard boundary".
     Then, the same day ("a more gradual fade to white at the edges"): the
     fade runs twice the section rhythm (256, or 160 at or below --bp-sm)
     instead of the padding, and follows smoothstep, 3t^2 - 2t^3, sampled at
     quarters (0, .156, .5, .844, 1), so it eases out of white and into full
     strength with no visible start or end. No new lengths: the rhythm,
     doubled. */
  /* Motion: each diamond sways on two travelling sine waves, x on one and
     y on the other, on --ambient-cycle-a and -b. Asked for 2026-09-25 by
     Jefferson ("a subtle undulatory motion ... to add motion to the page").
     Amplitude is the most a diamond can move and stay inside its own
     cell's band: half a cell less its half-diagonal, 6.5 - 1.27 = 5.23,
     floored to 5. (The canvas draws in one-cell bands, so this is also the
     ceiling the drawing allows.) Wavelength is a third of the content
     width, so three swells cross the page and the crests can be seen
     travelling.
     Was a quarter cell (3.25) and the full content width (1312), until
     2026-09-25. Neighbouring diamonds then differed by 0.2px and a diamond
     moved about 1px a second; Jefferson: "I can't even tell if they're
     moving".
     Still under reduced motion (the static --grid-tile shows instead). */
  /* 2026-09-25, later (Jefferson: "move more noticeably but less
     amplitude"): half the sway, twice the speed. Sway is the diamond's own
     diagonal, 1.8 x sqrt2 = 2.55, floored to 2.5, so a diamond moves at
     most its own width. A diamond's peak speed is 2 pi x sway / cycle, so
     twice the speed at half the sway needs cycles four times shorter:
     the ambient 17 and 23 over 4, still coprime in ratio, so the two waves
     never line up. Peak goes from 1.85 to 3.70px a second per axis, and
     a crest crosses the page at 103px a second (was 26). The hero wash
     keeps the full ambient cycles. */
  --grid-sway: 2.5px;                                 /* the diamond's diagonal; was 5 */
  /* Then half again as fast, the same day (Jefferson: "a little bit more
     noticeable, i.e. move faster"): the ambient cycles over 6, not 4, so
     1.5x the speed at the same sway. Peak 5.55px a second per axis. */
  --grid-cycle-a: 2.83s;                              /* --ambient-cycle-a / 6 (17 / 6 = 2.833); was / 4 */
  --grid-cycle-b: 3.83s;                              /* --ambient-cycle-b / 6 (23 / 6 = 3.833); was / 4 */
  --grid-wavelength: calc(var(--content-max) / 3);    /* 437 */
  --grid-tile: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13'%3E%3Crect x='5.6' y='5.6' width='1.8' height='1.8' fill='%23CBD7D2' transform='rotate(45 6.5 6.5)'/%3E%3C/svg%3E");
  --grid-size: 13px;
  --grid-dot: var(--g6);    /* the diamond colour; the tile above hard-codes the same hex */

  /* ---- text glow: keeps the grid out of type set over it ----
     Approved 2026-09-24 (homepage v2 handoff, decision 6). Stacking order
     is grid, then glow, then text; the glow never sits on the letters.
     Each glowing element carries three aria-hidden copies of itself behind
     it, in --canvas, each with a text stroke and a blur. Stroke and blur
     are the handoff's stated values, not tuned here. The large set serves
     a section title (H1), the small set a subtitle or a step label.
     Not text-shadow: stacked shadows wash out the letter edges.
     Verified 2026-09-24 at 1440 by rendering with the text transparent:
     an open grid diamond reaches 52/255 below white; inside every glowing
     line box the darkest pixel is 4/255, and within 16px of the title 16.
     One stacking context per SECTION, not per element: isolating each
     element lets its glow paint over the neighbours before it. */
  --glow-lg-stroke-1: 28px;   --glow-lg-blur-1: 14px;
  --glow-lg-stroke-2: 90px;   --glow-lg-blur-2: 48px;
  --glow-lg-stroke-3: 170px;  --glow-lg-blur-3: 96px;
  --glow-sm-stroke-1: 20px;   --glow-sm-blur-1: 10px;
  --glow-sm-stroke-2: 60px;   --glow-sm-blur-2: 32px;
  --glow-sm-stroke-3: 120px;  --glow-sm-blur-3: 70px;
  /* The same large glow round a surface (beat 1's art, 30 Sep): as box
     shadows, a stroke of width w reaches w/2 past the edge (spread), and a
     filter blur(r) is a shadow blur of 2r (the shadow's radius is 2 sigma,
     the filter's is 1 sigma). */
  --glow-surface:
    0 0 calc(2 * var(--glow-lg-blur-1)) calc(var(--glow-lg-stroke-1) / 2) var(--canvas),
    0 0 calc(2 * var(--glow-lg-blur-2)) calc(var(--glow-lg-stroke-2) / 2) var(--canvas),
    0 0 calc(2 * var(--glow-lg-blur-3)) calc(var(--glow-lg-stroke-3) / 2) var(--canvas);

  /* ---- status: semantic states, not domain labels ----
     Named for the state rather than the claim ("Success", not "Recovered")
     so the same four serve any surface, product or otherwise.
     Tinted pill + matching label + the tessera diamond in currentColor -- the
     same rotated square as the section eyebrows, so the status marker and the
     brand's own motif are one shape. currentColor means it can never drift
     from the label. The diamond is a 4px square: its 5.66px diagonal matches
     the label's measured x-height of 5.53px (Plus Jakarta 600 at 12px). A
     6px square gives an 8.49px diagonal against a measured cap-height of
     8.33 -- 1.9% over it -- and reads
     oversized next to lowercase. 6px radius.
     Line-height is 16, stated rather than inherited, so the pill is exactly
     22px tall and fits a dense row's 38 - 8 - 8 content box without changing
     it. Inheriting the cell's 22px made the pill 28px and every table row
     46px against a 38px spec.
     The pill is also block-level (display:flex, width:max-content), not
     inline-flex. An inline-flex box takes its baseline from its first flex
     item, which here is the diamond, so the pill sat on the text baseline by
     the diamond's bottom edge, hung below the strut and still gave a 42.5px
     row. Taking it out of the line box is what finally renders 38.
     THE FAMILY IS BUILT IN OKLCh, AND HUE IS THE ONLY FREE VARIABLE.
       text lightness  L 0.520 for every status but error
       text chroma     C 0.106 -- not chosen, but the ceiling the whole
                       family can hold at that lightness in sRGB. Warning is
                       the binding hue: yellow maxes at 0.106 where green
                       reaches 0.118 and red 0.213, so 0.106 is the highest
                       value all of them can actually reach.
       tint lightness  L 0.965, tint chroma C 0.020
     Three stated exceptions, and no others:
       ERROR IS HEAVIER ON PURPOSE. It sits at L 0.429, the lightness of
       --text-body, where the rest sit at 0.520. An error has to carry more
       weight than the other four, so it is set to the weight of body copy
       while they stay a band lighter. That is the rule; it was true of the
       old value by accident (0.418) and is now exact.
       INFO takes aura's chroma, 0.187, not the family's 0.106, because it is
       the accent rather than a status -- see the note above.
       NEUTRAL is achromatic by role, C 0.020, tint C 0.004.
     Contrast on its own tint after the build: success 4.77, warning 4.96,
     error 7.69, info 5.19, neutral 4.92 -- all clear of the 4.5:1 AA floor,
     and error clearly heaviest, which is the point.
     Before this the family drifted: L ran 0.418 to 0.548 and C ran 0.020 to
     0.189 with nothing stating why.
     Info is the one blue in the system. It is the ACCENT blue, not an
     info-only colour: it also fills the progress bar, which is how Primer
     uses its own accent. Added deliberately.
     It is a true blue, not a violet, and the check is in OKLCh because HSL
     hue is no more perceptual than HSL lightness. #355DD3 measures h 265.9,
     against pure blue at 264.1 -- 1.8 degrees off. The peer set runs Primer
     257.5, Vercel 258.2, Ramp 264.5, ours 265.9, Stripe 278.3, Linear 278.8,
     so the two that are genuinely violet-leaning are Stripe and Linear. Ours
     is also the darkest of them at L 0.521, which is why it clears 3.87:1 on
     our track where Ramp's own bar measures 3.02:1.
     An earlier note here argued the same conclusion from HSL hue 225. The
     conclusion held; the method did not.

     Does it harmonise with aura? Yes, and on the axis that matters. Hue-wheel
     intervals -- complementary, triadic -- come off the RYB artists' wheel
     and have no perceptual basis; Radix, Material 3, Carbon and Leonardo all
     build harmony from shared lightness and chroma instead. Aura measures
     C 0.18742 and info C 0.18753: a difference of 0.0001. The blue sits at exactly
     the brand colour's intensity. (For completeness, the hue angle between
     them is 107.5 degrees, nearest a triad at 120 and 12.5 off it.)
     It does NOT match its status siblings, which run C 0.020 to 0.107, and
     that is the right way round: it is the accent, not a status, so it takes
     the brand's chroma rather than the family's. Pulled down to the family
     mean it would be #4A66AB, still passing at 3.75:1 on the track but with
     the tie to aura broken.

     Error was once called "open, and not yet settled" here, with its
     contrast given as 7.90, then 4.90, and the tint recipe given in HSL as
     sat 0.69 / light 0.949. All of that was wrong: error measures 7.692 on
     its own tint, no tint is at that HSL pair (they run s .517-1.000,
     l .935-.965), and the rule IS settled and stated above. The OKLCh
     recipe at the top of this block reproduces all five tints. */
  --status-success-bg: #E9F8EF;  --status-success-text: #1F7B53;   /* 4.77:1 */
  --status-warning-bg: #F8F3E5;  --status-warning-text: #806600;   /* 4.96:1 */
  --status-error-bg:   #FFEFEC;  --status-error-text:   #80342C;   /* 7.69:1 */
  --status-info-bg:    #EDF4FF;  --status-info-text:    #355DD3;   /* 5.19:1 */
  --status-neutral-bg: #F1F4F3;  --status-neutral-text: #5E6D67;   /* 4.92:1 */

  /* ============================ TYPOGRAPHY ============================ */

  /* ---- logo placement ----
     Clear space = 0.41 x the mark's height on all four sides. That is the gap
     the lockup already keeps between its own mark and wordmark (392 of 965
     units, measured), and the reason is that anything sitting closer than the
     wordmark begins reading as part of the logo.
     Minimum size = 100px wide for the horizontal lockup on screen, where the
     mark is 13.03px and the cap height 8.97px. The limit is "Health", the
     lighter of the two words -- its thin strokes alias first at 1x, well
     before the mark is in any trouble. Print needs a physical proof to set;
     do not guess one from the screen figure. */
  --logo-clear-space: 0.41;       /* x mark height */
  --logo-min-width: 100px;        /* horizontal lockup, screen */
  /* The closer's standalone mark: 44, the live site's closer mark,
     measured at 1728 on 2026-09-25. It is also --input-height and the
     control target, so it stays on a size the system already uses. */
  --closer-mark-size: 44px;
  /* THE CLOSER'S CARD (Jefferson, 2026-09-28: the dust ran over the words;
     "a lifted card that is frosted"). A second use of the header's frosted
     glass, by that decision: the same material, not a new one (80% white
     over a 10px blur), lifted at --elevation-2 with the card hairline and
     the surface radius. Its steps are CodaHx's closer card, measured at
     1440 the same day, whose heading is 34px as our H2 is: 50 of padding
     (to 48), heading to line 11 (to 12), line to button 22 (to 24). The
     card hugs the words (Jefferson, same day: "smaller, just around the
     area of the text"): its width is its content plus the 48 on each side,
     so the dust runs free either side of it. */
  /* More translucent (Jefferson, 2026-09-28: "maybe make it more
     translucent?"): half the header's white, 40% over the same blur. Body
     text (--g11) still clears AA over a solid speck behind it: 5.58:1 on
     blush-dust, 6.04 on mint-dust (4.3 with no white at all). */
  /* Then 10% (same day, Jefferson: "even more translucent ... I can't see
     any of it"): the least white that keeps body text AA over a solid
     blush speck, 4.59:1 (5% gives 4.45, none 4.30). */
  /* Then 5% (same day, "a little more translucent"): half. Under the card
     no speck is ever solid, since the 4px blur spreads it: a 2.5px speck
     peaks at 6% of its colour, so body text holds 7:1 or better over the
     dust. The solid-speck floor above (10%) is kept as the bound for any
     card that drops the blur. */
  /* Then 2.5%, with the blur halved to 2 (same day, "less blur and more
     translucence"): both halved again. A 2.5px speck under a 2px blur
     peaks at 22% of its colour, and body text holds 7.1:1 over it. */
  /* Then 3.75% (same day, "slightly less translucent"): halfway back to
     the 5% he had judged too opaque, keeping the 2px blur. */
  /* Then 5% at the 2px blur (same day, "still a little less translucent").
     The 5% judged too opaque earlier sat under a 4px blur; this is 5%
     under the lighter blur he asked for. */
  /* Then 6.25% (same day, "slightly less translucent on the last card"):
     one more of the 1.25-point steps it took from 2.5 to 3.75 to 5. */
  /* Then solid (2026-09-29, Jefferson: "fully opaque so there's no
     translucency"): the canvas, and no blur, since nothing shows through. */
  --closer-card-bg: var(--canvas);
  /* A very subtle gradient on the card (Jefferson, 2026-09-29). First the
     brand gradient verbatim; the same day, "more random and lighter ...
     this looks like italy's flag": three vertical bands. Now two soft
     pools on the diagonal the closer's dust takes, from its own spine
     points: mint where the dust comes in (14% across, 20% down) and blush
     where it leaves (78%, 70%). Each reaches half the card's width, so the
     middle stays white, as the brand gradient's white band does. Lighter:
     each tint at half strength, halfway from white (rgb 231 253 244 and
     253 231 240 at .5). */
  --closer-card-wash: none;   /* the pools were removed (Jefferson, 29 Sep); the grid stays */
  /* 4, not the header's 10 (same day): specks are 1.2-2.5px, so a 10px
     blur erased them whatever the white; 4 (--space-1, the smallest step)
     keeps them as soft specks behind the glass. */
  --closer-card-blur: calc(var(--space-1) / 2);   /* 2, half the smallest step (see above) */
  /* The page's own lifted card (Jefferson, same day: "consistent with the
     brand and all of the other cards"): banner 2's active step, hairline
     plus --card-shadow-hover. Was --elevation-2. */
  --closer-card-shadow: var(--card-shadow-hover);
  --closer-card-padding: var(--space-12);       /* 48 */
  /* Bigger (same day): the card spans the middle eight of the twelve
     columns, two left free each side for the dust, and its padding
     steps up one, to 64. */
  --closer-card-columns: 8;
  --closer-card-padding-lg: var(--space-16);    /* 64, from --bp-lg */

  /* ---- bento: banner 3, "Track every dollar from chart to check." ----
     From Jefferson's banner-3 handoff, 25 Sep 2026 (every value below is
     the handoff's; the peers were measured that day for reference). The
     pattern is Stripe's homepage bento, measured: 2/3 + 1/3, then three
     1/3 tiles (Stripe 816 + 400 over 1232 with 16 gaps; Ramp 1/2 + 1/2
     then thirds with 24 gaps, and 128 above and below its grid, as here).
     Gaps are the grid gutter, 32. The spans are 8 + 4, then 4 + 4 + 4 of
     the page's 12 columns, on the current 1480 grid (the handoff's pixel
     widths assumed 1312). Tiles are the card surface (24 padding, g6
     hairline, 12 radius) with no shadow: they are not links, so they get
     no hover lift. */
  /* Reworked the same day to a 2 x 2 (Jefferson: "do a 2x2"), six columns
     each, all at the handoff's lead-tile height. */
  --bento-tile-height: 480px;
  --bento-gap: var(--grid-gutter);          /* 32 */
  /* THE STAT CARDS (banner 2, 2026-09-29), after Ramp's platform cards,
     measured at 1440: 421 x 526 (4:5), 12 radius, a 36 x 36 arrow button
     at the card's padding, 6 radius, a 16px icon; on hover the arrow flies
     out up-right and back in from down-left over 0.5s ease-in-out, 8px
     each way. In the brand: the button is aura with an ink arrow
     (--button-cta-*), the icon Lucide's arrow-up-right at --icon-sm, the
     radius --radius-control (6, as Ramp's); the card takes the
     interactive-card rule, flat to e1 on hover (--card-shadow-hover). */
  /* Then (Jefferson, same day): the arrow nearer the corner, at Ramp's own
     inset (33, to --space-8); a light grey button (--g3, the lightest
     component fill) with a larger aura arrow, --icon-md (24) in a 44
     button (--button-height, the page's control height). */
  --stat-card-ratio: 4 / 5;
  --stat-arrow-inset: var(--space-8);                   /* 32 */
  --stat-arrow-size: var(--button-height);              /* 44 */
  /* was --button-cta-bg, aura (Jefferson, 29 Sep); see below */
  /* The button went aura with an ink arrow (Jefferson, same day: "in my
     brand color"); a green arrow on a grey button read 1.33:1 in aura. */
  /* was --button-cta-text, ink on aura */
  /* Then white with a hairline (Jefferson, 29 Sep: the green goes to a
     good or bad indicator instead): the secondary button, white with its
     g8 inset hairline and an ink arrow, as Ramp's corner button is. */
  --stat-arrow-bg: var(--button-secondary-bg);
  --stat-arrow-icon: var(--button-secondary-text);
  --stat-arrow-border: var(--button-secondary-border);
  /* The indicator (same day: "a red or green 'bad' or 'good' indicator to
     give it some visual priority"): the system's status pill, error for a
     problem and success for a saving, as the brand sheet draws it
     (Caption 12/16 at 600, radius 6, 3 x 12, a 4px diamond 8 from the
     label). It sits one --card-gap above the lead-in, at the copy's left
     edge, so the copy does not move (it sat centred on the arrow first). */
  /* Pill to lead-in (Jefferson, 29 Sep: first "closer to the top text",
     then "up slightly ... best in class spacing"). Proximity: the pill
     labels the whole stat, so it sits further from it than any gap inside
     it (number to line 16; the lead-in's 8 went when the word joined the number) and nearer than the card's
     48 padding: the next step past 16 on the scale, the page's own 24.
     Was 8 (--card-gap), which grouped it with the lead-in alone. */
  --stat-status-gap: var(--space-6);   /* 24 */
  --status-pill-marker: 4px;   /* its diagonal, 5.66, is the caption's x-height (brand sheet) */
  /* The hover, more pronounced (same day): the card lifts to e2, the level
     above hover, and rises one step (--space-1, 4px); behind it a glow in
     the dust's own colours, mint-dust to blush-dust corner to corner as
     the hero's streams run, blurred by --space-6 and shown at .625, the
     closer dust's strength. */
  /* halved (Jefferson, 29 Sep: "reduce how much it pops out on hover"):
     2px, half of --space-1; then halved again ("a little more"), 1px */
  --stat-card-lift: calc(-1 * var(--space-1) / 4);
  --stat-card-shadow-hover: var(--elevation-2);
  --stat-glow: linear-gradient(135deg, var(--blush-dust), var(--mint-dust));   /* blush upper left, mint lower right (Jefferson, 29 Sep) */
  --stat-glow-blur: var(--space-6);
  --stat-glow-opacity: .35;                   /* "just barely" less than his 40% (same day); was .625, .5, .4 */
  /* The cards' pop and count, a little further apart (Jefferson, 29 Sep:
     "a little more of a delay"): two entrance steps, 300ms, not one. */
  --stat-pop-stagger: calc(2 * var(--intro-stagger));
  --stat-arrow-icon-size: var(--icon-md);               /* 24 */
  /* The copy starts under the arrow, not level with it (Jefferson, same
     day): the arrow's inset and size, then the page's 24 step, 32 + 44 +
     24 = 100 from the card's top. */
  --stat-copy-top: calc(var(--stat-arrow-inset) + var(--stat-arrow-size) + var(--space-6));
  --stat-arrow-travel: var(--space-2);                  /* 8 */
  --stat-arrow-duration: 500ms;
  /* Stat card 2's image: Ramp's "200+ Integrations" globe, with the
     largest US provider networks for its apps (Jefferson, 29 Sep: "looks
     identically like this card from Ramp's website - just for healthcare
     systems logos"). Every value below is read from Ramp's own code
     (ramp.com, 29 Sep; the globe component beside
     integration-globe-sprite.webp): a 15 x 19 hex lattice at a 46 pitch,
     each tile 44 square, radius 12, filled white at 65%, a 20px logo in
     it 12 from its corner; tiles scale with a Gaussian of their
     distance from the centre (see the script). The ground is Ramp's
     #F4F2F0 (L* 95.6) on our ramp: --g3 (L* 95.0) is the nearest step,
     g2 (97.2) the next. */
  /* No tint (Jefferson, 29 Sep: "no green filter or fog"): no ground of
     its own, the card's white; the tiles solid white with the card
     hairline so they keep their shape on it (was --g3 under white at
     65%). Then, the same day, no frame, and Ramp's fade back ("do you
     see how ramp makes it small and fade out around the edges? it should
     be more obvious its a globe"): Ramp's mask and rim fade return, now
     fading to white, not grey. */
  --globe-tile-fill: var(--canvas);
  --globe-tile-border: var(--card-border);
  --globe-pitch: 46px;
  --globe-tile: 44px;
  --globe-tile-radius: var(--radius-surface);     /* 12, Ramp's exactly */
  --globe-logo: 20px;
  /* The logo box widens to 32 (the 44 tile less 6 a side) so wordmarks
     keep a readable size; every logo takes Ramp's 20 x 20 area inside
     it (scripts/build-provider-sprite.py), so all read at one size. */
  --globe-logo-w: 32px;
  --globe-logo-inset: var(--space-3);             /* 12 */
  /* Off centre and a touch bigger (Jefferson, 29 Sep: "shift the globe
     slightly to the right ... make it slightly bigger - just barely"):
     its centre moves right to the copy column's middle, half the card's
     48 padding, 24; and it is drawn one pop step larger, 1 / --pop-scale
     = 1.071, tiles, spacing and mask alike. */
  --globe-offset-x: var(--space-6);               /* 24 */
  /* Then 5% larger again (Jefferson, 29 Sep: "make it 5% larger"). */
  --globe-grow: calc(1.05 / var(--pop-scale));    /* 1.124 */
  --globe-mask: radial-gradient(ellipse calc(60% * var(--globe-grow)) calc(55% * var(--globe-grow)) at calc(50% + var(--globe-offset-x)) 50%, #000 50%, rgb(0 0 0 / .9) 50%, rgb(0 0 0 / .15) 80%, transparent 100%);
  /* Ramp's canvas is 526 tall (its 421 x 526 card); ours is the card below
     the copy, about 268. The lens is fitted to the box: its falloff
     (Ramp's 33,800 px2) and the centre's 32px drop scale by the box
     height over 526, squared for the falloff, so the tiles shrink to
     Ramp's rim size inside our box and the whole globe shows. Tile and
     logo sizes stay Ramp's, so the logos at the centre read. */
  --globe-ref-height: 526px;
  /* Slower, at rest and on hover (Jefferson, 29 Sep: "spin slightly
     slower on rest and slower on hover as well"): Ramp's speeds (.35 /
     .12px a frame idle, up to 5 on hover) times two pop steps,
     --pop-scale squared = .872; one step was "just barely", so two for
     "slightly". */
  --globe-speed: calc(var(--pop-scale) * var(--pop-scale));   /* .872 */
  /* The hover pan's top speed, halved again (same day: "if i hover on the
     card towards the edge it moves way too fast"), as his earlier
     "reduce" asks were halved; the pull is also capped to a circle so a
     corner is no faster than an edge. 5 x .872 x .5 = 2.18px a frame. */
  --globe-hover-speed: .5;
  /* At rest, half again (Jefferson, 30 Sep: "turn even slower"), as his
     "reduce" asks are halved: .35 x .872 x .5 = .15px a frame, about 9px
     a second. Hover is unchanged. */
  --globe-rest-speed: .5;
  /* Up (same day, "move it up slightly"): the globe centres on its box.
     Ramp drops its centre 32 (and its mask to 53%) to clear the title
     above it; ours has no title in its box, so the drop goes, lifting it
     32 x 241 / 526 = 15 at 1440. */
  /* Stat card 3's image: total savings, rising (Jefferson, 30 Sep). A
     browser window, after Ramp's "Cash & Treasury" card (ramp.com, 29
     Sep), holding a Robinhood-style line chart under a title and Ramp's
     tabs, with a win card popping over it in turn. The window's parts
     are Ramp's, measured from their card at 2x (421 wide) and set to our
     nearest spacing step: title bar 34 -> 32, dots 9.5 -> 8 on a 12.75
     -> 12 pitch, 7 -> 8 in from the edge, the panel 7 -> 8 inside the
     frame. The frame is Ramp's ground #F4F2F0 on our ramp, g3, as the
     globe's was; its dots are 1.24:1 on it, and g6 is the nearest step
     (1.30 on g3; g5 is 1.15). The window starts at the copy column, as
     Ramp's does, runs 24 past the card's right and bottom, and fades
     out over the card's 48 padding to both edges. */
  --savings-bar: var(--space-8);
  --savings-dot: var(--space-2);
  --savings-dot-pitch: var(--space-3);
  --savings-inset: var(--space-2);
  --savings-frame: var(--g3);
  --savings-dot-fill: var(--g6);
  --savings-bleed: var(--space-6);
  --savings-fade: var(--closer-card-padding);
  /* Ramp's tabs: the active one in ink over a 2px ink rule (Ramp's
     weight), the rest faded (Jefferson: g8, two steps under
     --text-muted), a g3 rule the full width, the grid's own grey. */
  --savings-tab: var(--g8);
  --savings-tab-rule: var(--g3);
  --savings-tab-active: 2px;
  /* The win cards: the house toast (hero-animation.html, deleted 3 Oct, in git history) one size down
     at every step (Jefferson: "smaller"): icon 24, caption type. They
     pop as the closer does but at half its speed (Jefferson: "slower"),
     2 x --pop-duration, and pop out on --ease-pop mirrored in time
     (1 - each coordinate, reversed). While up they float as the house
     cards do (6px a swing, 2200ms), softer and slower (Jefferson): a
     quarter of the swing, 1.5 each way, at twice the period, so each
     half swing is 2200. */
  --savings-toast-icon: var(--space-6);
  --savings-pop: calc(2 * var(--pop-duration));         /* 1000 */
  --savings-ease-unpop: cubic-bezier(.36, -.56, .66, 0);
  --savings-float: 1.5px;
  --savings-float-period: 2200ms;
  /* Stat card 1's image: every claim reviewed (Jefferson, 30 Sep). The
     same browser window as card 3 (--savings-*), holding Ramp's
     accounting table (ramp.com card: a search field and a filter chip
     over a table that runs off under the fade) as the claims audit, with
     CodaHx's per-claim verdicts (codahx.com, 30 Sep). It fades over twice
     card 3's distance (Jefferson: "more fade towards the edges"). The
     Claim and Audit columns fit their longest entry plus the cell's 16
     either side (set by the script); the search field spans the Claim
     column's text. Pills are the status pills: Reviewing in warning,
     Cleared in success, the error in error (Jefferson); a flagged row
     takes half the error pill's tint, so the pill keeps its shape. The
     filter's inner chip sits half the smallest step in, its radius the
     control radius less that inset. */
  --audit-fade: calc(2 * var(--closer-card-padding));   /* 96 */
  --audit-inset: calc(var(--space-1) / 2);              /* 2 */
  --audit-flag-row: color-mix(in srgb, var(--status-error-bg) 50%, var(--canvas));
  /* Beat 1's image, "Index your plan" (Jefferson, 30 Sep). After the
     competitors' step cards (CodaHx, Bluespine: one picture, a few
     words, grey bars for text): a stack of plan documents, three clauses
     highlighted, and a card listing the five documents indexed (plan
     document, SPD, plan amendments, TPA contract, pharmacy contract;
     the SPD and amendments per DOL disclosure rules, the contracts per
     the Product build). Placement and size are Jefferson's, set in
     review: the stack 48 in from the panel and 56% wide, the card at
     40% down, overlapping the page by 64.
     Motion, in the step's 10s (see the steps script's HOLD), one thing at
     a time: the five documents check in as the card shows, the ledger's
     --bento-rows-stagger apart over --duration-slow (done by .9s); then
     the three clauses fill in turn over the rest of the step (10s less
     the pop-in wait and the checks, 8.6s; was 7.1s, then 6s, Jefferson
     30 Sep: "slow down the highlighter", then 10s a step), each fill
     followed by a pause a quarter its length (3.5 fills = 8.6s, 2.46s a
     fill). */
  --beat-doc-width: 56%;
  --beat-card-top: 40%;
  --beat-overlap: var(--space-16);
  /* The image pops in once the old one has popped out (.what__view), so
     the beat waits that long before it plays, and the fills end with the
     step. */
  --beat-lead: var(--pop-duration);
  --beat-run: calc(10000ms - var(--beat-lead) - var(--beat-checks));   /* the steps script's HOLD, less the wait and the checks */
  --beat-sweep: calc(var(--beat-run) / 3.5);
  --beat-gap: calc(var(--beat-sweep) / 4);
  --beat-checks: calc(4 * var(--bento-rows-stagger) + var(--duration-slow));
  /* Beat 2's image, "Review every claim" (Jefferson, 30 Sep: "similar to
     the Review 99% card"). Stat card 1's claims table (--audit-*) in the
     same window (--savings-*), whole rather than bleeding: the panel less
     --space-12 either side (card 1's window starts at the card's 48
     padding), top level with the step card, fading out at the foot as
     beat 1's stack does. Six claims in view, each Reviewing until its
     verdict; the three errors the step names (duplicate charges, coding
     errors, overcharges) on every other row, so all three land in view;
     the claims under the fade stay Reviewing, the review running on.
     Motion, in the step's 10s (the steps script's HOLD): after the
     pop-in wait (--beat-lead), the six verdicts land top to bottom,
     evenly spaced, each popping in over --savings-pop as card 1's do,
     the last one landing with the step's end. */
  --beat2-inset: var(--space-12);
  --beat2-rows: 6;
  --beat2-run: calc(10000ms - var(--beat-lead) - var(--savings-pop));   /* HOLD, less the wait and the last pop: 8.5s */
  --beat2-every: calc(var(--beat2-run) / (var(--beat2-rows) - 1));    /* 1.7s */
  /* Beat 3's image, "Recover overpayments" (Jefferson, 30 Sep: after
     competitors and recovery firms; chargeback recovery, Chargeflow and
     Justt, is the one field that shows a single dispute worked to a win).
     Step 2's coding error, disputed: Stripe's dispute statuses (disputed,
     under review, won; docs.stripe.com/disputes/responding), the
     evidence packet, the letter, then the money back, split by the
     claim's cost share. Beat 1's layout and tokens (--beat-doc-width,
     --beat-card-top, --beat-overlap).
     Amounts: FAIR Health's 2022 average allowed amounts, CPT 99285 $317
     and 99284 $235 (FH Healthcare Indicators 2024, figure 29), so the
     upcode overpaid $82.00; split at the average coinsurance, 20% (KFF
     Employer Health Benefits Survey 2025): $16.40 to the employee,
     $65.60 to the plan.
     Motion, in the step's 10s (the steps script's HOLD): after the
     pop-in wait (--beat-lead), half the rest builds the case and half
     holds the return to be read. Six steps build it, each --beat3-every
     apart: disputed, the four evidence items, under review; on the
     sixth it is won and the return pops in. */
  --beat3-every: calc((10000ms - var(--beat-lead)) / 2 / 6);   /* 792ms; won at 5.25s */
  /* Beat 4's image, "Prevent overpayments" (Jefferson, 30 Sep: "a
     procedure at the center, and then, extending from that, a few
     different options for made-up facilities and how much they cost at
     each of them"). One knee MRI, CPT 73721, and four invented
     facilities on spokes round it. The prices are 2023 commercial
     negotiated rates from Aetna, BCBS, Cigna and United (Philips and
     Whaley, "Commercial price variation for common imaging studies",
     Health Affairs Scholar 2025, Table 3), professional plus facility
     fee at the same point of each: 25th percentile $563 (196 + 367),
     median $865 (283 + 582), mean $1,315 (375 + 940), 75th percentile
     $1,568 (442 + 1,126). The fair market price is the median: a
     facility at or under it is "Fair price", over it "Above market".
     Which facility takes which price is illustrative; the paper does not
     split by site of service.
     Motion, in the step's 10s (the steps script's HOLD), one facility at
     a time, clockwise from the top left: its spoke draws out from the
     procedure over --pop-duration, the facility pops in (the house pop),
     then its verdict pops (the same pop). After the pop-in wait
     (--beat-lead) the four are evenly spaced, the last verdict landing
     with the step's end. */
  --beat4-draw: var(--pop-duration);
  --beat4-run: calc(10000ms - var(--beat-lead) - var(--beat4-draw) - 2 * var(--pop-duration));   /* HOLD, less the wait and the last facility's draw and two pops: 8s */
  --beat4-every: calc(var(--beat4-run) / 3);   /* 2.67s */
  --bento-header-gap: var(--space-12);      /* 48, header block to grid */
  --bento-rows-stagger: 150ms;              /* ledger rows resolve to their pill... */
  --bento-rows-duration: var(--duration-slow);   /* ...300ms each, on --ease-out */

  /* Two families render, and only two. The wordmark's letterforms were cut
     from Source Serif 4 and ship as outlines inside the lockup SVG - that is
     asset provenance, not a typeface of the system: it is never loaded, never
     set as live text, and never extended (lockups ship verbatim). */
  --font-heading: 'Lora', Georgia, 'Times New Roman', serif;   /* 600 only */
  --font-body: 'Plus Jakarta Sans', 'Segoe UI', system-ui, sans-serif;
  /* No mono family. Plus Jakarta's figures are already monospaced — every
     digit measures the same width — so table columns align without one. */

  --weight-normal: 400;
  --weight-medium: 500;
  --weight-semibold: 600;

  /* headings: Lora 600, tight, tracking eases off as size drops.
     Steps sit ~1.30 apart so each is perceptibly different from the next;
     H5 17px was dropped because 20->17 is only 1.18. */
  /* DISPLAY XL, for the one headline a page leads with. 80 is Enzo
     Health's hero headline, measured 2026-09-24 at 1440 (80px, their
     own computed style), and the homepage wireframe's original size;
     Jefferson asked for it after seeing 60 in the hero card. It sits one
     ~1.30 step above Display (60 x 1.30 = 78), so the ladder's spacing
     rule holds. Line-height keeps Display's own ratio, 62/60, rounded to
     the pixel: 80 x 1.0333 = 82.67 -> 83. */
  --display-xl: 80px;   --display-xl-lh: 83px;
  --display: 60px;      --display-lh: 62px;   --display-track: -.02em;
  --h1: 44px;           --h1-lh: 48px;        --h1-track: -.02em;
  --h2: 34px;           --h2-lh: 40px;        --h2-track: -.015em;
  --h3: 26px;           --h3-lh: 32px;        --h3-track: -.01em;
  --h4: 20px;           --h4-lh: 28px;        --h4-track: -.01em;

  /* body: Plus Jakarta, generous line-height. Steps sit ~1.15 apart.
     Caption at 13px was dropped once because 14->13 is 1.08. At 12 it is
     1.167 and clears the same rule, so it is back -- and it is the size four
     of the six systems measured put a validation message at (Mercury, Ramp,
     Toast, Primer; Stripe sets 14). 12/18 is Ramp's body 12/18 and Mercury's
     12/17.
     LEAD 19 WAS REMOVED to pay for it, one in and one out. It sat 1.05 from
     H4 20 -- far worse than the 1.18 that got H5 17 dropped from the heading
     ladder -- and an intro paragraph does not need its own size; Stripe's
     own body copy is 16/22. Measured at 1440: Ramp's hero H1 is 64/64 and
     Stripe's is 56/58, which is why Display 60 stays. */
  --body: 16px;         --body-lh: 26px;
  --small: 14px;        --small-lh: 22px;
  --caption: 12px;      --caption-lh: 18px;

  /* LEAD, back by decision. Approved 2026-09-24 (homepage v2 handoff,
     decision 3): 16px Body under a 60px Display reads too small for a hero.
     20/32 in Plus Jakarta 400 and --text-body. Its size equals H4's, which
     the ratio rule above would bar within one family; it is allowed here
     because the family differs (sans against Lora) and the two never sit
     next to each other. 32 is 1.6x, Body's own 26/16 ratio rounded to the
     4px grid. It never scales: Lead is body copy, and body copy holds its
     size at every width.
     Measure 520px is Body's 65ch scaled to 20px: 416 x 20 / 16. */
  --lead: 20px;         --lead-lh: 32px;
  --lead-measure: 520px;

  /* eyebrow label. Line-height is 22, the same line Small sits on, so the
     whole annotation layer shares one rhythm and a table header row is
     exactly the 38px a body row is (8 + 22 + 8). It was unspecified, which
     meant every use of Label rendered at whatever the font's default gave --
     about 15.5px, a number the scale never states. */
  --label: 12px;        --label-lh: 22px;       --label-track: .12em;
  --label-weight: 600;

  /* ---- responsive type ----
     BODY COPY NEVER SCALES. Body, Small and Caption hold their size at every
     width. Measured: Ramp's body goes 14/20 to 15/21 between 1440 and 390,
     Stripe's 16/22 does not move at all. Only headings and the section
     rhythm scale, and they scale together.
     HEADINGS STEP DOWN TWO PLACES at or below --bp-sm. Display takes H2's
     size, H1 takes H3's, H2 takes H4's, and H3 and H4 floor at 20 because
     the ladder runs out. That is a factor of about 0.58, which is what both
     peers measure: Ramp's hero goes 64 to 40 and Stripe's 56 to 34, and 34
     is exactly where our Display lands.
     Three steps collapsing to 20 on a phone is the honest consequence and
     not a defect: Stripe's mobile page spans 34 down to 16 where its desktop
     spans 56 down to 16. You cannot hold five heading sizes in 390px. */
  --display-sm: 34px;   --display-sm-lh: 40px;
  --h1-sm: 26px;        --h1-sm-lh: 32px;
  --h2-sm: 20px;        --h2-sm-lh: 28px;
  --h3-sm: 20px;        --h3-sm-lh: 28px;
  --h4-sm: 20px;        --h4-sm-lh: 28px;

  /* ============================== LAYOUT ============================== */
  /* THE SCALE GOVERNS SPACE BETWEEN THINGS, NOT SPACE INSIDE THEM.
     Gaps, margins, section rhythm, gutters and page padding are steps of
     the 4px scale and nothing else. Space INSIDE a component is derived
     from that component's own rule and gets its own token, because a
     component's internals answer to its geometry, not to the scale: the
     status pill's 3px vertical padding is (22 - 16) / 2, where 22 is the
     content box a 38px table row leaves and 16 is the pill's line-height.
     Snapping that 3 to a 4 would make the pill 24 and the row 40.
     Horizontal padding inside a control follows the rule --button-px
     already states: the nearest spacing step to half the height.
     An internal value with no stated rule is still a defect -- the
     exemption is from the SCALE, not from having a derivation. */


  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;
  --space-20: 80px;
  --space-32: 128px;

  /* ---- section rhythm: the space between one page section and the next ----
     128 on desktop, 80 below 640. That is Ramp's exactly: their sections
     measure 128/128 at 1440 and 80/80 at 390. Stripe runs 96/96 and 56/56.
     Both scale the gap by about 0.6 between those widths, and both scale
     their headings by the same factor -- the rhythm and the type move
     together or the page stops looking like itself.
     The scale needed two new steps to carry this and they continue its
     existing pattern only partway: 48 to 64 is +16 and 64 to 80 is +16,
     but 80 to 128 is +48 and continues nothing -- the scale's deltas run
     4,4,4,8,8,8,8,16,16,48. 128 is here because it is the section rhythm
     (2 x 64), not because the scale reached it. */
  --section-rhythm: var(--space-32);      /* 128 */
  --section-rhythm-sm: var(--space-20);   /* 80, at or below --bp-sm */

  /* ---- grid ----
     Derived from our own measure, not copied. Plus Jakarta 400 at 16px has a
     measured average advance of 6.39px, so 65 characters -- the centre of the
     optimal 45-75 range -- is 416px. Twelve 80px columns with 32px gutters
     give exactly that at 4 columns (320 + 96 = 416 = 65ch), and total
     12(80) + 11(32) = 1312. 32 is on the spacing scale and the columns are
     whole pixels.
     Measured for reference at a 1600px viewport: Stripe 1232, Ramp 1312,
     Vercel 1400, and 12 columns confirmed at both Stripe and Vercel. Body
     copy sits in 4 columns; 5 would be 82.6ch, past the range.

     Two honest caveats. The GUTTER was a free choice, not a derivation: the
     maths fixes only that 4 columns must equal 416px, and 16 / 24 / 32 are
     all on the spacing scale and all satisfy it -- they only move the column
     and the total (16 -> 92px col, 1280; 24 -> 86px, 1296; 32 -> 80px,
     1312). Stripe uses 16, Vercel 24. 32 was chosen for the roundest column.
     24 is the live alternative, since it would equal --card-padding and let a
     card's internal padding and the gutter between cards agree.
     And 45-75 characters is typographic convention, not a measured fact.

     WIDENED 2026-09-25 (Jefferson: the second banner should "cover most
     of the screen on a regular desktop view"; he chose the whole page over
     that banner alone, so every edge stays aligned). The cap is Enzo
     Health's, measured that day: their section titles sit 220px in at a
     1920 window and 540px in at 2560, so their content stops at 1480.
     Twelve columns and 32 gutters over 1480 give 94px columns, whole
     pixels. That breaks the 4-column = 65ch derivation above (4 columns
     are now 472px, 74ch), which is why body copy is held by --measure
     and lead copy by --lead-measure, never by a column count. At 1920 the
     content now fills 77% of the window (was 68%); at 1728, 86% (was 76%).
     Peers, measured the same day at 1920 and 2560 windows (section-heading
     left edge; the cap is the window less twice that): Stripe 1232,
     Notion 1252, Linear 1284, Rippling 1320, Vercel 1400, Enzo 1480,
     Brex 1536, Mercury about 1620-1650. Oscar Health has no cap (160px
     in at any width). Ramp would not measure headless. The median of the
     eight capped sites is 1360, so 1480 sits on the wide side of the
     band. */
  --grid-columns: 12;
  --grid-column: 94px;           /* (1480 - 11 x 32) / 12; was 80 */
  --grid-gutter: 32px;
  --content-max: 1480px;         /* 12(94) + 11(32); was 1312 */
  --measure: 416px;              /* 65ch of --body */
  /* The lead headline wraps inside seven columns at Display XL,
     7(80) + 6(32) = 752px, stated in em so the wrap holds as the size
     steps down: 752 / 80 = 9.4em, which is 564px at 60 and 320px at 34.
     Measured in Lora 600 at -.02em: "Take control of your" is 9.17em and
     adding "healthcare" makes it 14.24em, so 9.4em breaks that headline
     after "your" at every size. (Was 10.667em, six columns at Display 60,
     before the hero moved to Display XL.) */
  --display-measure: 9.4em;
  --page-gutter: 32px;
  --page-gutter-sm: 16px;

  /* Breakpoints. These are Tailwind's defaults, adopted as a convention --
     not derived, and peers do not converge on any set: Stripe uses 640/940,
     Vercel 600/960, Ramp 768/992. Ours sit inside that spread. Only --bp-xl
     is computed: 1480 plus two 32px page gutters = 1544, the width at which
     the grid stops growing (was 1312 + 64 = 1376). */
  --bp-sm: 640px;
  --bp-md: 768px;
  --bp-lg: 1024px;
  --bp-xl: 1544px;

  /* ---- icons: Lucide, ISC licence ----
     Drawn on a 24 grid, fill none, round caps and joins. Stroke-width is 1.5,
     NOT Lucide's default 2. On a 24 grid the rendered stroke is size x 1.5/24,
     so 16 -> exactly 1.0px, 24 -> 1.5px, 32 -> 2.0px. The default 2 gives
     1.33px at 16px, which straddles two pixels and goes muddy. Sizes must be
     multiples of 8 for the same reason: 20px would render a 1.25px stroke.
     Measured: Vercel ships 1px at 16 and 1.5px at 24 -- the identical
     stroke-to-size ratio of 0.0625 -- and Ramp ships 1px at 16. Stripe's
     icons are filled rather than stroked, which suits a different register.
     Known trade-off: Plus Jakarta's stem at 16px measures 1.30px at weight
     400, so a 1px stroke reads slightly lighter than the text beside it.
     Matching the text would need stroke 2; both stroked peers choose
     crispness over that match, and so do we. */
  --icon-stroke: 1.5;             /* on the 24 grid, not the rendered px */
  --icon-sm: 16px;                /* renders a 1.0px stroke */
  --icon-md: 24px;                /* 1.5px */
  --icon-lg: 32px;                /* 2.0px */
  --icon-gap: 8px;                /* icon to label */

  --radius-control: 6px;   /* buttons, inputs, chips, status pills */
  --radius-xs: 2px;        /* controls under 24px: the checkbox. 6/44 x 16 */
  --radius-surface: 12px;  /* cards, panels, modals */
  /* App icons are NOT on this scale. The platform masks them (iOS uses a
     continuous superellipse at ~0.2237 x size), so the icon asset ships as
     a plain square and the rounding you see in mockups is a preview. */

  /* ============================ ELEVATION ============================= */
  /* Soft, large-blur, low-opacity, layered, tinted toward ink.
     STRUCTURE, stated so a fourth level could never be invented by eye:
     each level is two shadows. The CONTACT shadow doubles exactly across
     the three -- y 1/2/4, blur 2/4/8 -- and the AMBIENT shadow's blur runs
     a constant 1.87x, 16 -> 30 -> 56, with y 6/14/28 and spread
     -8/-10/-18. Alphas rise .05/.06/.08 on the contact and .10/.14/.22 on
     the ambient.
     Provenance, stated plainly rather than dressed as a derivation: the
     ambient values were picked against Stripe's own shadows, which are
     cited beside them, and then the contact shadow was made to double so
     the set has a spine. The ratios above describe the set; they did not
     generate it. Three levels is the whole scale -- rest, raised, modal --
     and there is no fourth. */

  /* Three levels, and flat is not one of them: flat is a 1px
     --border-hairline and no shadow, the default surface treatment. A card
     at rest is flat; level 1 is what it lifts TO. Tinted toward ink, not
     black -- Stripe tints indigo rgba(50,50,93,...) for the same reason.
     Measured on stripe.com: a card at rgba(50,50,93,.12) 0 16px 32px and an
     overlay at rgba(50,50,93,.25) 0 30px 60px -10px, beside --elevation-3. */
  --elevation-1: 0 1px 2px rgba(2, 26, 16, .05), 0 6px 16px -8px rgba(2, 26, 16, .10);
  --elevation-2: 0 2px 4px rgba(2, 26, 16, .06), 0 14px 30px -10px rgba(2, 26, 16, .14);
  --elevation-3: 0 4px 8px rgba(2, 26, 16, .08), 0 28px 56px -18px rgba(2, 26, 16, .22);

  /* ============================== MOTION ============================== */

  /* Two curves, two durations, every value measured on the peers.

     --ease carries STATE CHANGES -- hover, focus, a colour moving. It is
     Ramp's curve exactly (color/background .3s cubic-bezier(.4,0,.2,1)), and
     Mercury runs the identical curve at .15s.
     --ease-out carries things ENTERING -- an overlay, a toast, a message.
     It is Radix's overlay curve and the sharpest decelerate in the set.
     Every one of the six eases OUT for interface motion: Stripe
     cubic-bezier(.25,1,.5,1) on 144 elements, Mercury (0,0,.2,1) on 41,
     Linear (.25,.46,.45,.94), Primer easeOut (.3,.8,.6,1), Radix
     (.16,1,.3,1). None of them uses ease-in or linear for a state change.
     Linear is for spinners only, which is why --loader-easing is separate.

     300ms carries STATE CHANGES and larger transitions. That is Ramp's
     number exactly -- their color/background transition is .3s on this same
     curve -- and Stripe, Mercury and Toast all run 300 for the same job.
     150ms carries OVERLAY ENTRY only, which is Radix's: its dialog is .16
     and its tooltip .14. An overlay appearing should be quicker than a
     colour settling, and this is the one place the two diverge.
     The wider split is real and worth knowing: marketing motion measures
     240-300 across Stripe, Ramp, Mercury and Toast; product motion measures
     100-150 across Linear and Radix. We take the marketing number for state
     and the product number for overlays.

     Both curves are identical to a peer's, digit for digit, not approximated:
       --ease      is Ramp's (300ms), Toast's (box-shadow, 150ms) and
                   Mercury's (150ms) -- the same four numbers in all three
       --ease-out  is Radix's dialog and tooltip curve (140-160ms)
     The ones we did not take, for the record: Stripe cubic-bezier(.25,1,.5,1)
     on 144 elements, Mercury's second curve (0,0,.2,1) on 41, Linear
     (.25,.46,.45,.94), Primer easeOut (.3,.8,.6,1).
     Not adopted, deliberately: Linear ships eighteen Penner curves
     (in/out/in-out x quad/cubic/quart/quint/expo/circ) and Primer a full
     100ms ladder from 0 to 1000. Two and two covers everything this system
     animates.
     Honour prefers-reduced-motion wherever these are used. */
  --ease: cubic-bezier(.4, 0, .2, 1);      /* state changes */
  --ease-out: cubic-bezier(.16, 1, .3, 1); /* entrances */
  --duration-fast: 150ms;    /* overlay entry, and direct manipulation --
                                a toggle or anything you flip or drag.
                                Radix's switch thumb measures transform .14s;
                                a control should not lag the finger moving
                                it. Track and knob share the duration, since
                                a track fading over 300 while the knob slides
                                in 150 reads as broken. */
  --duration-slow: 300ms;    /* hover, focus, state, larger transitions */

  /* ---- ambient motion: backgrounds that move on their own ----
     Two cycles that never line up (17 and 23 are coprime, so the pattern
     repeats only every 391s). First set for the hero wash, 2026-09-24;
     lifted here 2026-09-25 when the grid began to move on the same clock,
     so the page's ambient motion keeps one tempo. */
  --ambient-cycle-a: 17s;
  --ambient-cycle-b: 23s;

  /* ---- page intro: the hero's first paint ----
     Jefferson, 2026-09-25: "like Enzo Health, where things slide up into
     place. No loading splash." Measured on enzo.health that day, frame by
     frame from first paint (inline GSAP styles on .text-intro): each
     piece -- headline line 1, line 2, paragraph, button -- goes from
     opacity 0 and 50px down to rest over 1.0s, 150ms apart. The curve
     fits GSAP power2.out, 1 - (1 - t)^3, to a squared error of 0.0001
     over 16 samples; in CSS that is easeOutCubic, (.33, 1, .68, 1).
     Enzo holds all of it until a video intro ends (about 3.3s); ours
     starts as soon as the fonts are in, and never waits longer than
     Enzo's 1.0s for them. The hero's wash and dust fade in over the
     same duration and curve, starting at once. None of it runs under
     reduced motion, or without JS. */
  --intro-duration: 1500ms;   /* the words; Enzo's 1000 x 1.5, since 2026-09-25 (Jefferson: "rise in a little slower too") */
  --intro-rise: 50px;
  --intro-stagger: 150ms;
  --ease-intro: cubic-bezier(.33, 1, .68, 1);   /* GSAP power2.out */
  /* The header, measured in the same recording: opacity 0 to 1 over
     300ms on CSS ease (a transition on their <header>), first, before any
     text moves. Ours starts with the wash, at the start of the intro.
     Added 2026-09-25 (Jefferson: "fade the header in too, like Enzo"). */
  /* Then both fades half again as long, the same day (Jefferson: "a
     little bit longer"; 1.5x is the step used for "a little" on the grid
     too). The words keep Enzo's 1.0s. */
  /* Then, again the same day, the words slowed by the same 1.5x
     (--intro-duration above), and the header given "a slow fade": it
     takes the wash's duration, so the header and the wash come in as
     one fade. */
  --intro-fade-duration: 1500ms;                        /* the wash and dust; Enzo's 1000 x 1.5 */
  --intro-header-duration: var(--intro-fade-duration);  /* 1500; was 450, Enzo's 300 */
  --ease-intro-header: ease;
  /* THE POP (the closer card, 2026-09-28; Jefferson: "I don't want this to
     fade in ... basically pop out"). GSAP's defaults for a pop: back.out
     at its default overshoot of 1.7, easings.net's easeOutBack,
     cubic-bezier(.34, 1.56, .64, 1), over GSAP's default 0.5s. The card
     grows from the size at which its edges sit one gutter (32) inside
     its own, 912/976 = .934, and is simply shown at the start (opacity over
     --duration-fast), so the motion is the scale, not a fade. */
  --ease-pop: cubic-bezier(.34, 1.56, .64, 1);
  --pop-duration: 500ms;
  --pop-scale: .934;
  /* The subtle pop (banner 3's steps, 2026-09-29; Jefferson: "a subtle pop
     in and pop out ... between 1 to 4"): the same pop, with the step
     card's edges one --space-1 (4px) inside its own rather than a gutter,
     1 - 8/534 = .985 at 1440 (the step card is 534 wide). */
  --step-pop-scale: .985;
  /* The border trace on the active step (Jefferson, 29 Sep, after Ramp's
     "See recommendations" box, read from their CSS): a 1px comet round the
     card's edge, one lap every 3.4s, linear, looping. Ramp's conic stops,
     verbatim: clear to 12deg, then 14% at 28, 34% at 44, 62% at 60, 84% at
     74, solid 86-92, clear from 94. Grey (his ask): --g9, the solid step,
     where Ramp uses black. */
  --step-trace-colour: var(--g9);
  --step-trace-duration: 3.4s;
  /* The closer card carries the same trace in aura, the brand green
     (Jefferson, 29 Sep: "the green outline animation"), always running. */
  --closer-trace-colour: var(--aura);
  /* ...and a second, in blush (--blush-dust, the dust's blush), running
     the other way round, so the two pass each other twice a lap
     (Jefferson, 29 Sep: "a green and blush outline that animates around
     each other"). Its comet is Ramp's mirrored, head first the other way. */
  --closer-trace-colour-2: var(--blush-dust);

  /* ============================== LAYERING ============================ */
  --z-dropdown: 1000;
  --z-sticky:   1100;
  --z-scrim:    1200;
  --z-modal:    1300;
  --z-toast:    1400;

  /* ============================ COMPONENTS ============================ */

  /* buttons: two sizes, weight 500, no pills. Padding-x is the spacing step
     nearest half the height; the label size is on the type scale.
     Measured: Linear's primary is 44 tall at px 20 and weight 510, which is
     our height exactly; Vercel 40 at px 14, weight 500, r8; Stripe 40/48 at
     weight 400, r4; Ramp 34/43/51 at 400, r6. 44 sits in the middle of that
     range and is also the WCAG 2.5.5 target size. Our px 24 is 4 wider than
     Linear at the same height. Linear and Mercury run pills; we do not. */
  --button-height: 44px;
  --button-px: 24px;              /* nearest spacing step to 44/2 */
  --button-text: 16px;            /* --body */
  /* Large, for the page's one lead CTA beside a Display XL headline, by
     decision 2026-09-24 (Jefferson: the 44px CTA read too small under an
     80px headline). Height continues the ladder's own ratio: 44 / 36 =
     1.222, and 44 x 1.222 = 53.8, snapped to the 4px grid -> 52. Padding
     follows the rule above, the step nearest half the height (26 -> 24).
     Label 20 is --lead, which keeps the label-to-height ratio of the small
     button (14/36 = .389; 20/52 = .385). Icon 24 is --icon-md, the next
     multiple of 8 above the default's 16, so its stroke stays 1.5px. */
  --button-lg-height: 52px;
  --button-lg-px: 24px;           /* nearest spacing step to 52/2 */
  --button-lg-text: 20px;         /* --lead */
  --button-lg-icon: 24px;         /* --icon-md */
  --button-sm-height: 36px;
  --button-sm-px: 16px;           /* nearest spacing step to 36/2 */
  --button-sm-text: 14px;         /* --small */
  --button-gap: 8px;
  --button-weight: 500;
  --button-radius: var(--radius-control);

  /* Primary is ink; aura is the one highest-intent CTA on a page. Ink cannot
     darken, so its states LIFT, by the same L* deltas aura falls by (-8.9
     and -17.8): 7.13 -> 16.03 -> 25.02. Those land in the gap between g12
     (12.5) and g11 (34.1), so they are their own values, exactly as
     --aura-hover and --aura-press are. Using --g12 for hover gave only
     +5.4 and left pressed identical to rest. */
  --button-primary-bg: var(--ink);
  --button-primary-text: var(--text-on-ink);
  --button-primary-hover: var(--ink-hover);
  --button-primary-press: var(--ink-press);
  --button-cta-bg: var(--aura);
  --button-cta-text: var(--text-on-aura);
  --button-disabled-bg: var(--g5);
  --button-disabled-text: var(--g11);   /* 6.18:1 */

  /* inputs */
  --input-height: 44px;
  --input-px: 12px;
  --input-text: 16px;             /* --body; was 15, which is on no scale */
  --input-radius: var(--radius-control);
  --input-border: var(--g8);      /* 2.36:1; --g7 was 1.79:1. Still under the
                                     3:1 WCAG asks of a boundary -- only --g9
                                     at 4.36:1 clears it. Accepted knowingly. */
  --button-secondary-border: var(--g8);  /* matches the input; was --g7 */
  /* Secondary, as the brand sheet draws it: white, ink label, the g8
     hairline inset. Hover follows the interaction scale (flat at rest,
     e1 on hover; see focus below), since the sheet gives it no hover.
     First use: the hero's second CTA (Jefferson, 2026-09-28). */
  --button-secondary-bg: var(--canvas);
  --button-secondary-text: var(--ink);
  --button-secondary-hover-shadow: var(--elevation-1);
  /* Between paired buttons: 12. Measured 2026-09-28 at 1440: Stripe's hero
     pair sits 8 apart (48 tall), Mercury's and Vercel's 12 (40 tall);
     ours are 44, nearer theirs. */
  --button-pair-gap: var(--space-3);
  /* Ghost: the quietest button, words only. The brand sheet's ghost
     (transparent, ink) with its padding removed, so the label sits on the
     text edge above it. Hover is the link's underline, not a fill: a fill
     would draw back the box it drops. First use: "Learn how" on the
     homepage's second banner (Jefferson, 2026-09-25). */
  --button-ghost-text: var(--text-primary);
  --button-ghost-underline: var(--link-underline-hover);        /* 2, on hover */
  --button-ghost-underline-offset: var(--link-underline-offset); /* 4 */
  --input-placeholder: var(--g10);   /* 5.85:1; --g9 was 4.36:1, under AA */
  /* ---- focus: ONE indicator for everything interactive ----
     A grey drop shadow from the elevation family: a 2px rim of ink at 55%
     with --elevation-2 under it. The elevation scale is the interaction
     scale -- a surface is flat at rest, lifts to e1 on hover, and sits at e2
     when focused.
     The rim is load-bearing, not trim. Rendered side by side, flat / e1 /
     e2 / e3 are indistinguishable on the ink button: a grey shadow under a
     near-black fill on a white page has nothing to cast onto. The rim is
     what carries the state there. Ink at 55% composites to rgb(116,129,124)
     on white, 4.06:1, over the 3:1 a focus indicator is asked for; 45% is
     2.98 and misses. Zero blur on the rim, as Stripe's band has none.
     Bind to :focus-visible on EVERYTHING, never :focus and never a list of
     selectors. A list is how five controls, and later a scrollable region,
     were left with no indicator at all.
     Measured live on the leaders, all of whom ring rather than lift:
       Mercury  outline 2px #707393, offset -1
       Radix    outline 2px accent,  offset -1
       Linear   outline 2px #7170FF, offset 2
       Primer   outline 4px accent,  offset 2
       Vercel   outline 2px #0070F3, offset 4
       Stripe   box-shadow 0 0 0 1px #A497FC, 0 0 0 4px #E0D9FB, zero blur
     We do not ring, and that is a deliberate departure: their accents are
     mid-dark violets and blues that carry a ring on white, and aura is a
     light green at 1.51:1 that cannot, so a ring here could only ever be
     grey or ink and read as a border rather than as focus.
     Rejected after rendering each: 2px g10 at offset 0 merged into the
     border; 3px opaque aura; a blurred aura glow (nobody ships a blurred
     coloured glow); a mint cushion, invisible on white; offset 4, which
     floats on a 16px checkbox.
     Known limit: a box-shadow is clipped by an overflow:hidden ancestor, so
     the lift is cut on a control inside a scrolling region. The rim sits on
     the element edge and survives. */
  --focus-rim: 0 0 0 2px rgba(2, 26, 16, .55);   /* 4.06:1 on white */
  --focus-shadow: var(--focus-rim), var(--elevation-2);
  /* On ink the rim above is ink on ink and vanishes; the footer's links
     take the same 2px rim in white instead, at 70%: composited on ink that
     is #B4BAB7-ish, 9.9:1 against the ground. */
  --focus-rim-on-ink: 0 0 0 2px rgba(255, 255, 255, .7);

  /* form controls. 16px box for checkbox and radio -- the consensus size:
     Primer ships 16x16 for both, Radix offers 14/16/20 with 16 the default.
     Checkbox radius comes from our own rule that radius scales with size,
     6/44 x 16 = 2.18 -> 2px. Primer uses 3px. A 6px radius on a 16px box is
     37% per corner and reads as a circle, colliding with the radio.
     Circles and pills are EXEMPT WHERE THE SHAPE CARRIES MEANING. A radio's
     round shape and a toggle's pill are how people tell one-of-many from
     many-of-many from on/off; a round avatar is how you tell a person from
     an organisation. The ban stands for decoration, where the diamond
     replaced the round dot. The exemption was once written as "functional
     controls", which named the two cases it had rather than the reason
     behind them.
     Checked is ink, never aura: aura is reserved for the single
     highest-intent CTA on a page and a checkbox is never that. */
  --control-box: 16px;              /* checkbox and radio, visual */
  --control-target: 24px;           /* hit area. WCAG 2.5.8 AA wants 24x24;
                                       a bare 16px control passes only via the
                                       spacing exception, which a dense table
                                       row will not satisfy. */
  --checkbox-radius: var(--radius-xs);
  --control-checked: var(--ink);
  --radio-checked-border: 5px;      /* donut; leaves a 6px centre */
  /* TOGGLE COLOUR. WCAG 1.4.11 asks 3:1 of two boundaries on a switch --
     the track against the page, and the thumb against the track -- and the
     old spec had neither: a --g5 track measures 1.303:1 on white and the
     white thumb on it measures the same 1.303:1. The control was close to
     invisible, and because disabled was ALSO --g5, an off toggle and a
     disabled toggle rendered identically.
     --g9 is the lightest step of the ramp that clears 3:1 on white, at
     4.364:1 -- g8 is 2.363 and misses. The white thumb on g9 is the same
     4.364:1, so one value fixes both boundaries. Disabled stays --g5,
     which 1.4.11 exempts as an inactive component and which now reads as
     distinctly lighter than off rather than identical to it.
     This is also --g9's first real job. It was carrying a stated purpose
     and no user, and it is barred from text at 4.36:1 -- but a track is
     not text, and 4.36 clears the 3:1 a boundary is held to. */
  --toggle-w: 36px;
  --toggle-h: 20px;
  --toggle-track-off: var(--g9);                    /* 4.364:1 on white */
  --toggle-track-on: var(--ink);                    /* 18.14:1 */
  --toggle-knob-color: var(--canvas);               /* 4.364:1 on the off track */
  --toggle-track-disabled: var(--button-disabled-bg);  /* g5, exempt */
  --toggle-knob: 16px;
  --toggle-radius: 10px;            /* half --toggle-h; derived, not a third
                                       radius. The radio and the knob are
                                       50%. These are the only rounded
                                       shapes outside 2 / 6 / 12. */
  --select-height: var(--input-height);
  --textarea-min-height: 88px;      /* 2 x --input-height */
  --textarea-py: 9px;               /* (44 - 26) / 2, so the first line sits
                                       where an input's single line sits */

  /* dense table. 38px row = 8px vertical padding + Small 14/22, both on the
     spacing and type scales. Primer measures 37px rows with 8/12 padding, so
     this sits at the same density. The header reuses the Label style already
     defined for eyebrows. Hairline dividers and no zebra: striping is a
     second signal for what the divider already says. Amounts right-align with
     tabular-nums -- Plus Jakarta's digits are already monospaced, so columns
     line up without a mono family. */
  --table-row-height: 38px;      /* holds only while --status-pill-lh is 16 */
  --status-pill-lh: 16px;
  --status-pill-py: 3px;          /* (22 - 16) / 2; 22 is the row's content box */
  --status-pill-px: 12px;         /* nearest spacing step to 22/2, as --button-px */
  --status-pill-gap: var(--icon-gap);   /* 8; marker to label, as icon to label */
  --table-pad-y: 8px;
  --table-pad-x: 12px;
  --table-text: var(--small);
  --table-divider: var(--g3);
  --table-header-border: var(--g6);
  --table-row-hover: var(--g2);

  /* cards. Flat at rest: a 1px hairline and no shadow, which is what this
     file already calls the default surface treatment.
     It carried --elevation-1 at rest for a while, on the strength of Primer
     shipping border plus shadow-resting-small. That was a misreading:
     Primer's resting shadow is 4% and 3% alpha at y1 and y2, a whisper, where
     our e1 is 10% at y6 with a 16px blur. Putting e1 under a resting card
     spent the first elevation level on a surface that is not raised, and
     left hover nothing to move to.
     24px of padding at a 12px radius is the pair Primer ships (padding
     spacious 1.5rem at borderRadius-large .75rem) and the pair Radix reaches
     at its largest size (12/8 at size-1, 16/8 at size-2, 24/12 at size-3).
     A card is 4 grid columns at its working width -- 416px, the body measure
     -- so copy inside a card obeys the same rule as copy anywhere else:
     416 minus 48 of padding is 368px, 57.6ch at Body.
     Title to body is 8px, which is also Primer's card content gap.

     An interactive card is a real <a> or <button>, never a div with a click
     handler, so :focus-visible reaches it. Hover LIFTS, flat to e1.
     Two quieter versions were rendered side by side and neither read: an
     edge step of g6 to g7 (dL* 6.6, which matches Radix's own hover delta of
     6.4 and is still invisible on a 1px line), and e1 to e2. Nothing to e1
     is the whole point of having a first level.
     There is no pressed state: the button owns that convention.
     The card edge does not meet the 3:1 WCAG asks of a component boundary
     (g6 1.48:1) and is not required to: a card is identified by its content,
     not its edge, and the one state that must be visible -- focus -- is
     carried by --focus-rim, ink at 55% -- #74817C composited, 4.06:1. */
  --card-radius: var(--radius-surface);
  --card-padding: var(--space-6);
  --card-shadow: none;                     /* flat at rest */
  --card-border: var(--border-hairline);   /* g6 */
  --card-shadow-hover: var(--elevation-1);
  --card-gap: var(--space-2);              /* card title to card body */
  /* The hero card is lifted (Jefferson, 2026-09-28, after asking whether its
     edges should fade): the page's lifted-card shadow, as banner 2's active
     step and the closer card carry, over its hairline. Cards at rest stay
     flat (--card-shadow); the hero is the page's one standing surface. */
  --hero-shadow: var(--card-shadow-hover);

  /* --diamond-size and --clip-corner were here and are gone. --clip-corner
     (a 14px 45deg corner cut) appeared in nothing and is on no scale.
     A 5px value once sat here, and it matches neither diamond the system
     actually draws: the status marker is 4px, sized against the label's
     measured x-height, and the section eyebrow is 6px. A motif that is
     sized per context does not get a single token. */

  /* ============================== OVERLAYS ============================ */
  /* Modal 480 wide: Primer's large dialog, and 480 - 64 of padding is 416,
     our body measure exactly. Padding 32 is Radix's dialog (space-6) at
     radius-5, which is our 12. Shadow is --elevation-3, already named for it.
     Menu: 4px content padding (Radix size-1 and shadcn both), 32px rows
     (Radix --base-menu-item-height, space-6; shadcn measures 27), 12px row
     padding-x (Radix space-3), 192 min-width (Primer --overlay-width-xsmall),
     4px off the trigger (Primer --overlay-offset). Peers nest the outer
     radius as inner + padding, which would be 10; our scale has 6 and 12
     only, and a menu is a surface, so it is 12 and 2px wider than the rule.
     Popover: 16 padding (Radix size-2, space-4), 320 wide (Primer small).
     Tooltip: dark fill and light text, as Radix (gray-12) and Primer
     (--bgColor-emphasis) both do; 4/8 padding and 12/16 text are Radix's
     exactly. Radius 2 follows our own rule that radius scales with size:
     6/44 x 24 = 3.3, and 2 is the nearest step. No shadow -- a small dark
     chip separates by value, and Radix gives its tooltip none either. */
  --modal-width: 480px;
  --modal-padding: var(--space-8);          /* 32 */
  --modal-radius: var(--radius-surface);
  --modal-shadow: var(--elevation-3);

  --menu-min-width: 192px;
  --menu-padding: var(--space-1);           /* 4 */
  --menu-radius: var(--radius-surface);
  --menu-shadow: var(--elevation-2);
  --menu-item-height: 32px;
  --menu-item-px: var(--space-3);           /* 12 */
  --menu-item-radius: var(--radius-control);
  --menu-item-hover: var(--fill-rest);
  --menu-separator: var(--border-hairline);
  --overlay-offset: var(--space-1);         /* 4, trigger to overlay */

  --popover-width: 320px;
  --popover-padding: var(--space-4);        /* 16 */
  --popover-radius: var(--radius-surface);
  --popover-shadow: var(--elevation-2);

  --tooltip-bg: var(--ink);
  --tooltip-text: var(--text-on-ink);
  --tooltip-py: var(--space-1);             /* 4 */
  --tooltip-px: var(--space-2);             /* 8 */
  --tooltip-radius: var(--radius-xs);       /* 2 */
  --tooltip-text-size: 12px;
  --tooltip-text-lh: 16px;

  /* ============================== FEEDBACK ============================ */
  /* Banner is Radix's Callout at its default size-2: padding 16, gap 12,
     16px icon. Radix pairs 16 with radius 8 and we have no 8, so a banner
     takes the surface 12 -- the same round-up the menu makes. No border:
     Radix draws none, Primer draws one in the status colour at 40%.
     Toast 320 is Primer's --overlay-width-small; Sonner's radius is 8, ours
     is 12 for the same reason.
     No empty state. Primer ships Blankslate, but every value in ours was
     already specified elsewhere -- 32px icon, H4, Body, the primary button,
     416 -- so it was assembly, not spec, and the removal test took it.
     The loader and the skeleton both run 1000ms, measured on Primer
     (--base-duration-1000 on its rotation, 1s on its shimmer). Primer's
     skeleton fill is a neutral at 10% alpha; --g3 lands in the same place on
     white. Progress is 8 tall, Primer's exactly.
     Skeleton and progress take radius 2 by our own rule that radius scales
     with size, 6/44 x 8 = 1.1. Primer uses 3 for both. */
  --banner-padding: var(--space-4);         /* 16 */
  --banner-gap: var(--space-3);             /* 12 */
  --banner-radius: var(--radius-surface);
  --banner-icon: var(--icon-sm);            /* 16 */
  /* A 16px icon is optically centred on the FIRST LINE of the text beside
     it, so its top offset is (line-height - 16) / 2. The banner sets 16/26
     and the toast 14/22, which gives 5 and 3. The sheet had these two
     swapped -- the banner carried the toast's 3 and the toast the
     banner's 5 -- so each icon sat 2px off its own first line. */
  --banner-icon-offset: 5px;                /* (26 - 16) / 2 */
  --toast-icon-offset: 3px;                 /* (22 - 16) / 2 */

  --toast-width: 320px;
  --toast-padding: var(--space-4);          /* 16 */
  --toast-radius: var(--radius-surface);
  --toast-shadow: var(--elevation-2);

  /* ONE loader, used everywhere: the four tesserae fading in turn, at the
     icon sizes. It replaces a geometric spinner entirely, and it is legible
     at 16. For the record, and not as a constraint: Primer rotates an arc,
     Radix fades eight leaves, and neither puts its mark in an inline
     spinner. We do, by decision. */
  --loader-duration: 1000ms;
  --loader-easing: linear;
  --loader-sizes: 16px 24px 32px;   /* --icon-sm / --icon-md / --icon-lg */

  --skeleton-fill: var(--fill-rest);        /* g3 */
  --skeleton-radius: var(--radius-xs);      /* 2 */
  --skeleton-shimmer: 1000ms;

  /* ---- step number: the active step's number on an Aura square ----
     Jefferson, 2026-09-25: the number turns green on its step's turn.
     Aura is barred from text on white (1.51:1), so it is the fill and the
     numeral is --text-on-aura, as on the CTA. Square at the label's own
     line-height, so the row keeps its height and nothing moves when a step
     opens. Radius --radius-xs: the rule for controls under 24px. The
     square is there, unfilled, on every step, so numbers never shift. */
  --step-num-size: var(--label-lh);         /* 22 */
  --step-num-radius: var(--radius-xs);      /* 2 */
  --step-num-bg-active: var(--aura);
  --step-num-text-active: var(--text-on-aura);

  /* Progress fills with the accent blue on a g6 track, 3.87:1 -- better
     separation than Ramp's own bar, which measures #6082CC at 3.02:1.
     No fourth colour: the blue is already here as --status-info-text, and
     this is exactly Primer's model, where --bgColor-accent-emphasis fills
     the progress bar AND tints the info banner. One accent, two jobs.
     Green cannot do it. Against a grey track aura is 1.02:1, aura-hover
     1.31, aura-press 1.73 -- the rule is lightness, not hue, which is why
     every peer's accent works and ours does not. */
  --progress-height: 8px;
  --progress-radius: var(--radius-xs);      /* 2 */
  --progress-track: var(--border-hairline); /* g6 */
  --progress-fill: var(--status-info-text);

  /* ================================ LINKS ============================= */
  /* The link colour is the accent blue -- the same value as
     --status-info-text and --progress-fill. That is Primer's model exactly:
     --fgColor-accent colours their links, fills their progress bar and tints
     their info banner. Aura cannot take this job; it is barred from text and
     measures 1.51:1 on white.

     INLINE LINKS ARE ALWAYS UNDERLINED, and it is not a preference. WCAG
     1.4.1 asks that a link identified by colour alone clear 3:1 against the
     text around it. Ours measures 1.40:1 against --text-body, so colour
     cannot carry it. Most peers fail the same test -- Stripe 2.06:1, Radix
     2.66 -- and Primer, the only one clearing it at 3.04, still ships an
     always-underline setting for exactly this reason.

     Offset 4 is derived, not copied: Plus Jakarta's descender measures
     3.50px below the baseline at 16px, and 4 is the first step on the
     spacing scale that clears it. Radix sets 2.4 and Primer 0.8, and both
     cut through g, j, p, q and y.
     Thickness 1, as Radix sets. Hover thickens to 2 rather than shifting the
     colour, because a darker accent would be a fourth colour.

     A STANDALONE link -- a nav item, a card title, anything not sitting in a
     run of text -- carries no underline, because 1.4.1 is about links
     embedded in text. It underlines on hover instead, as Primer's does. */
  --link-color: var(--status-info-text);
  --link-underline: 1px;
  --link-underline-hover: 2px;
  --link-underline-offset: 4px;        /* clears a 3.50px descender */

  /* ============================= VALIDATION =========================== */
  /* Measured live on all four: the message is ALWAYS below the field, never
     above and never inline. That part is unanimous.
     The field treatment is not, so it is a decision. Stripe and Ramp leave
     the field completely untouched; Mercury tints its border at 22% alpha;
     Toast and Primer recolour it outright. We recolour, at the same 1px the
     field already carries -- the same hairline, a different colour.
     Mercury's whisper is arithmetically closed to us: our error at 22%
     composites to #E3D2D1, 1.46:1, LIGHTER than the resting g8 border at
     2.36:1, so an invalid field would read as less bordered than a valid
     one. At 50% it is 2.53 and indistinguishable from resting.
     Message: Caption 12/18. Mercury, Ramp, Toast and Primer all set 12;
     Stripe sets 14. Gap 12, against Mercury's 13 and Ramp's 14.
     No icon in the message -- none of the four products puts one there.
     No success state -- Primer supports one and none of the four ships it.
     Required is marked, optional is not, and the mark is an asterisk in ink
     at weight 600. That is Primer's exactly: rgb(31,35,40), not red, because
     an asterisk is not an error.
     Fires on blur, which is what all four do.
     Semantics: aria-invalid="true" plus aria-describedby to the message.
     Mercury and Ramp both set aria-invalid; Stripe and Toast do not, which
     is a miss on their part.
     Peer error colours for the record, all measured live: Mercury #B0175F,
     Stripe #CD3D64, Ramp #B74018, Toast #D40023, Primer #D1242F. They sit at
     OKLCh L 0.50-0.58 and C 0.16-0.22. Ours is L 0.429, C 0.107 -- quieter
     than all of them, which is a direct consequence of the status family
     rule: error takes body-text lightness and the family chroma. */
  --field-error-border: var(--status-error-text);
  --field-error-text: var(--status-error-text);
  --field-message: var(--caption);
  --field-message-lh: var(--caption-lh);
  --field-message-gap: var(--space-3);      /* 12 */
  --field-required-mark: var(--text-primary);
  --field-required-weight: var(--weight-semibold);

  /* ================================ TABS ============================== */
  /* Both systems that publish a tab converge on a 2px underline: Radix
     draws it on a ::before at 2px in its accent, Primer on an ::after at 2px
     in a dedicated nav colour, #FD8C73, deliberately not its accent blue.
     Row height 40 is Radix's (Primer runs 32); padding-x 8 and text 14 are
     both of theirs exactly. A 1px hairline runs under the whole row, which
     is Radix's inset shadow.
     The underline is INK, and that is an application of an existing rule
     rather than a new one: ink is already what selected looks like here --
     a checked checkbox, a checked radio, a toggle that is on. Aura cannot do
     it at 1.51:1 on white, and the accent blue is already spoken for by
     links, progress and the info tint.
     Three things change on active, as at Radix: the underline appears, the
     label goes from muted to ink, and the weight goes 400 to 500. One would
     be enough for a sighted user; three is what makes it legible at a glance
     in a dense row, and the weight change carries it if colour is lost. */
  --tab-height: var(--space-10);          /* 40 */
  --tab-px: var(--space-2);               /* 8 */
  --tab-text: var(--small);
  --tab-rule: var(--border-hairline);
  --tab-indicator: 2px;
  --tab-indicator-color: var(--ink);
  --tab-inactive: var(--text-muted);
  --tab-active: var(--text-primary);
  --tab-active-weight: var(--weight-medium);

  /* ============================== DATA ============================== */
  /* Charts get no new colour. The peers we model do not give them one:
     Stripe draws trend as MONOCHROME sparklines and reserves colour for
     status (succeeded / failed / pending); Mercury and Ramp run an
     overwhelmingly monochrome surface with a single accent held back for
     the primary action. Carbon, which publishes the most complete chart
     spec of any system, ships four MONOCHROMATIC palettes of ten steps
     beside its categorical fourteen, and names trend and relationship
     charts as the monochrome case. That is the case we are in.

     THE RAMP IS AURA TO INK IN SEVEN EQUAL LIGHTNESS STEPS.
     Both endpoints are brand anchors already in this file, so the ramp
     adds nothing: step 100 IS --aura and step 700 IS --ink. The five
     between them are the straight line joining the two in OKLCh --
     L 0.843 to 0.194, C 0.187 to 0.038, h 158.4 to 163.8 -- cut into six
     equal parts. Every adjacent step is therefore exactly 0.108 L apart
     and the whole ramp spans 5 degrees of hue, which is what makes it
     read as one colour getting darker rather than as seven colours.

     SEQUENTIAL uses all seven. ORDINAL STARTS AT 200. The lightest step
     of an ordinal ramp has to stay visible as a mark, and aura measures
     1.51:1 on white -- it is a fill with ink on it, never a mark. Step
     200 measures 2.20:1 and clears the 2:1 ordinal floor. Aura appears
     in a chart only as the near-zero end of a continuous scale, where
     receding toward the surface is the job.

     DELTA-E HERE IS EUCLIDEAN DISTANCE IN OKLab, x100. Not CIE76 and not
     CIEDE2000 -- on this pair those give 31.5 and 23.7, so quoting a
     figure without naming the formula made the cap impossible to check.
     The floors of 8 (target) and 15 (normal vision) are the thresholds of
     the method this palette was built under, calibrated to the
     Machado-Oliveira-Fernandes simulation named below; they are that
     method's, not ours. Recomputed: 22.09 for the pair we ship, 13.02 for
     the three-step case we rejected.

     TWO SERIES, AND THE CAP IS MEASURED, NOT PREFERRED. Steps 200 and
     400 clear every gate on the harder all-pairs list: CVD delta-E 21.6
     (deuteranopia, Machado-Oliveira-Fernandes at severity 1.0), 22.1
     unsimulated, against floors of 8 and 15. THREE cannot be made to
     work. A mark has to sit inside OKLCh L 0.43-0.77 to be neither too
     pale to see nor too heavy to read as a series, and that band is only
     0.34 wide; three even steps inside it land 0.153 apart and measure
     delta-E 13.0, below the 15 floor, which means full-colour readers
     cannot reliably tell the middle one from its neighbours. Re-stepping
     does not help -- there is no room. A third series folds to Other in
     --g8, or becomes small multiples, or becomes its own chart.
     Step 200 at 2.20:1 is under the 3:1 mark floor, so a chart using it
     ships direct labels or a table view. With two series both are cheap.

     DIVERGING is success to error across a --g3 midpoint. Polarity is
     status semantics, not series identity, so this is the status family
     doing its own job rather than a status colour impersonating a
     series. Both arms are the status family's own recipe -- C 0.106,
     hue fixed, lightness stepped -- so no hex here is invented either.

     CHROME. A gridline sits INSIDE a card and must recede behind the
     card's own edge, so it is --g4 at 1.19:1 where --border-hairline
     measures 1.48:1. The axis and baseline are that hairline: a chart's
     baseline is a structural rule and takes the same weight as every
     other rule in the system. Axis and legend labels are --text-muted;
     a value or a label never wears the series colour, the mark beside
     it carries identity. */
  --chart-100: var(--aura);       /* #2BEF9C  1.51:1  sequential only */
  --chart-200: #1FC784;           /*          2.20:1  ordinal starts here */
  --chart-300: #13A16B;           /*          3.31:1 */
  --chart-400: #0A7C53;           /*          5.22:1 */
  --chart-500: #06593B;           /*          8.40:1 */
  --chart-600: #033825;           /*         13.16:1 */
  --chart-700: var(--ink);        /* #021A10 18.14:1 */

  --chart-series-1: var(--chart-200);
  --chart-series-2: var(--chart-400);
  --chart-series-other: var(--g8);
  --chart-series-max: 2;

  --chart-diverging-pos: var(--status-success-text);
  --chart-diverging-mid: var(--g3);
  --chart-diverging-neg: var(--status-error-text);

  --chart-gridline: var(--g4);
  --chart-axis: var(--border-hairline);
  --chart-label: var(--text-muted);
  --chart-line-width: 2px;
  --chart-mark-gap: 2px;          /* surface gap between adjacent fills */

  /* ============================= AVATAR ============================= */
  /* SHAPE CARRIES THE ENTITY, which is why a circle is allowed here at all.
     Round is a person; square is an organisation, team or bot. That is
     Primer's rule verbatim -- "Circle Avatars represent individual people.
     Square Avatars represent non-human entities" -- and it is the same
     exemption the radio and the toggle already use, applied for the same
     reason. Ramp, Stripe and Mercury all ship circular avatars for people,
     so round also reads as "person" by convention, not only by our say-so.
     Radix Themes is the counter-case worth recording: its Avatar has no
     default radius at all and inherits the theme's, so a circle there is
     something you ask for. We ask for it, and only for people.

     EVERY SIZE IS A TOKEN THIS FILE ALREADY HAS. 24 is --icon-md, which is
     also --control-target; 32 is --icon-lg; 44 is --input-height, so an
     avatar sits flush with the control beside it; 64 is --space-16.

     INITIALS ARE SIZED BY THE LOGO'S OWN CLEAR SPACE. --logo-clear-space
     is 0.41 x the mark's height on all four sides, measured off the
     lockup. Initials are a mark in a box, so they take the same constant,
     and the box is then wide enough for the WIDEST possible pair rather
     than an average one:
       W = pairWidth + 2 x 0.41 x capHeight
     Measured in Plus Jakarta Sans 500 across all 676 uppercase pairs, a
     pair runs 1.341 x font-size on average and 1.992 at its widest; cap
     height is 0.6616. Solving with the worst pair gives
     F <= W / (1.992 + 0.82 x 0.6616) = W / 2.5345, then snapped DOWN to a
     step of the type scale:
       32 -> 12.6 -> --caption 12      44 -> 17.4 -> --body 16
       64 -> 25.3 -> --h4 20
     At 24 the answer is 9.5 and no step qualifies, so a 24px avatar shows
     ONE letter, sized by the same formula against the widest single letter
     (0.996): 24 / (0.996 + 0.5425) = 15.6 -> --small 14.
     Nothing here was picked by eye; change the constant and the sizes move.

     FALLBACK is --fill-rest with --text-body initials, measured 7.10:1.
     Not ink, which is a fill reserved for small emphatic things, and a
     column of ink discs would out-shout every row it sits in. Not aura,
     which belongs to the one highest-intent CTA.

     THE RING IS --edge, the hairline built for any ground. A photograph
     with a light border bleeds into a light surface without it, which is
     why Primer draws one too. Inset, so it never changes the footprint. */
  --avatar-person: 50%;             /* circle -- the exemption, see above */
  --avatar-org: var(--radius-control);   /* 6; --radius-xs below 24px */
  --avatar-sm: var(--icon-md);      /* 24 -- one letter  */
  --avatar-md: var(--icon-lg);      /* 32 -- two letters */
  --avatar-lg: var(--input-height); /* 44 -- two letters */
  --avatar-xl: var(--space-16);     /* 64 -- two letters */
  --avatar-initials-sm: var(--small);    /* 14 on 24 */
  --avatar-initials-md: var(--caption);  /* 12 on 32 */
  --avatar-initials-lg: var(--body);     /* 16 on 44 */
  --avatar-initials-xl: var(--h4);       /* 20 on 64 */
  --avatar-initials-weight: var(--weight-medium);
  --avatar-fallback-bg: var(--fill-rest);
  --avatar-fallback-text: var(--text-body);   /* 7.10:1 on --fill-rest */
  --avatar-ring: var(--edge);

  /* ============================ PAGINATION ============================ */
  /* STRUCTURE IS PRIMER'S, because it is the only one of our references
     that publishes the counts: marginPageCount 1 and surroundingPageCount
     2, so the row is first, ellipsis, two either side of current, ellipsis,
     last -- nine slots at most, however many pages exist. Below --bp-sm the
     numbers drop and only Previous and Next remain, which is Primer's own
     responsive behaviour (its showPages prop takes narrow/regular/wide).
     The ellipsis drops WITH the numbers. It marks pages that exist but are
     not shown, so on a row that shows no pages at all it says nothing --
     the first build left it in and read "Previous ... ... Next".
     Stripe is the deliberate non-model here: its API is cursor-based, so
     its lists offer next and previous and no page numbers at all. Numbered
     pages are the right call for a table a person scans and links to -- a
     cursor is opaque, cannot be shared, and expires; page 4 of a claims
     table has to survive being pasted into an email.

     THE ITEM IS A SMALL BUTTON, so it is --button-sm-height at 36 with
     --radius-control and --small text, the same three values a small
     button already uses. 36 square holds FOUR digits, which is 9,999 pages
     of headroom, and the check is the avatar's: Plus Jakarta's figures are
     monospaced and measure exactly 7.00px at 14px -- identical at 400 and
     500, so the current page does not reflow when it takes weight -- and
     --logo-clear-space 0.41 against a 9.26px cap height wants 3.80px
     either side. 7 x 4 + 7.6 = 35.6, inside 36. A fifth digit needs 42.6
     and the item would have to grow by padding.

     CURRENT IS AN INK FILL. Ink is already what selected looks like here:
     a checked checkbox, a checked radio, a toggle that is on. A tab says
     it with an underline because a tab sits on a rule; a pagination item
     is a discrete button, so it says it the way a small button does.
     Weight goes 400 to 500 as well, so the state survives without colour.

     DISABLED IS --g8 at 2.36:1. WCAG 1.4.3 exempts inactive controls from
     the contrast minimum, and g8 is the lightest step this system already
     uses for anything non-text (--border-strong). The button's own
     disabled pair is a g5 FILL, which would put a heavy grey block at
     whichever end of the row you happen to be on. */
  --pagination-item: var(--button-sm-height);     /* 36, four digits */
  --pagination-gap: var(--space-1);               /* 4 */
  --pagination-radius: var(--radius-control);     /* 6 */
  --pagination-text: var(--small);                /* 14 */
  --pagination-rest: var(--text-body);
  --pagination-hover: var(--fill-rest);
  --pagination-current-bg: var(--ink);
  --pagination-current-text: var(--text-on-ink);
  --pagination-current-weight: var(--weight-medium);
  --pagination-disabled: var(--g8);               /* 2.36:1, WCAG 1.4.3 */
  --pagination-ellipsis: var(--text-muted);
  --pagination-count: var(--text-muted);
  --pagination-margin-pages: 1;                   /* Primer's default */
  --pagination-surrounding-pages: 2;              /* Primer's default */
  --pagination-numbers-hide: var(--bp-sm);        /* below this, prev/next */

  /* =============================== NAV =============================== */
  /* MEASURED, not read about. All four references were loaded at 1440 on
     2026-09-23 and their own computed styles read off the page:

       site      nav row   logo h   link      target   radius   action h
       Ramp        62        20     14 / 400    44       6         34
       Stripe      64        25     14 / 400    38       4         40
       Mercury     ~64       32     16 / 420    40      pill       40
       Toast       72        --     15 / 400    --      pill       40

     HEIGHT 64 is --space-16, and it is the middle of that column (62, 64,
     72). Ramp names its own value in a variable, --nav-height: calc(62px
     + 40px), the 40 being an announcement bar we do not have.
     LINK 14 at weight 400 is --small, and it is what the two references
     closest to us both use. Mercury's 16 comes with a 420 weight, pill
     links and a dark bar -- three things this system bans -- so it does
     not get a vote here.
     THE 44px HIT TARGET IS RAMP'S EXACTLY, and it is also --button-height,
     so the link is a full-size control inside a 64px bar: 44 with 10 above
     and below. Padding 12 and radius 6 are Ramp's figures too and are
     --space-3 and --radius-control unchanged. Ramp sets the gap between
     links to 0 and lets the padding do it, which puts 24px between labels.
     ACTIONS ARE SMALL BUTTONS at 36. The column runs 34 to 40 and
     --button-sm-height sits inside it; no new height is needed.
     LOGO 24 is --icon-md, inside the measured band of 20 to 32 and next to
     its median of 25. The lockup is 7.407:1, so height drives width and
     nothing is ever scaled by hand. Its clear space fits with room over:
     --logo-clear-space 0.41 on all four sides of a 24px lockup wants
     1.82 x 24 = 43.7 of the 64. (An earlier note here used 1.795, which
     back-solves to a mark that is 96.95% of the lockup's height and is
     stated nowhere. The mark measures 964.88 of the 1000-unit viewBox,
     so on the mark's own height the factor is 1.7912 and the answer is
     42.99. Either way it fits; 1.82 is the one the stated constant gives.)
     THE BAR IS WHITE WITH A 1px HAIRLINE, which three of the four have
     (Ramp rgba(33,33,33,.05), Mercury rgba(112,115,147,.22), Toast
     #F0F0F0; Stripe alone has none). Ramp's bar is also 50% white over a
     backdrop blur. We do not copy that: there is no blur anywhere in this
     system, and adding one here would be a new effect rather than a use
     of an existing one.
     CURRENT IS INK AND 500, the tab rule minus its underline. A tab draws
     an indicator because it sits ON the rule and interrupts it; a nav
     link sits 10px clear of the bar's edge, so the underline would float
     against nothing. */
  /* 80, 32 and 44 below replace 64, 24 and 36, by decision 2026-09-24:
     Jefferson asked for the header to sit closer to Enzo Health's and our
     own previous site's. Measured at 1440: Enzo's header is 82 tall with
     43px actions; the previous tessellahealth.com nav was 72 tall with a
     36px lockup and 40px buttons. 80 is --space-20, the step nearest 82;
     the lockup is --icon-lg, between the old site's 36 and the 24 it
     replaces; actions take the default button, 44, Enzo's 43 rounded to
     the size that already exists. Clear space still fits: 0.41 x the
     mark's 30.9px height is 12.7 a side, 57.3 of the 80. */
  --nav-height: var(--space-20);                /* 80 */
  --nav-bg: var(--canvas);
  /* SCROLLED: frosted glass, the one use of blur in the system. By
     decision, Jefferson 2026-09-25, copying Enzo Health's header (measured
     at 1440: rgba(255,255,255,.8), backdrop blur 0.694vw = 10px, shadow
     0 0.069vw 0.347vw rgba(0,0,0,.04) = 0 1px 5px). Their black shadow is
     tinted toward ink, as every shadow here is. This supersedes the note
     above that the bar carries no blur -- for the scrolled header only. */
  --nav-bg-scrolled: rgba(255, 255, 255, .8);
  --nav-blur: 10px;
  --nav-shadow-scrolled: 0 1px 5px rgba(2, 26, 16, .04);
  --nav-rule: var(--border-hairline);
  --nav-gutter: var(--page-gutter);             /* 32; wider past --bp-xl via --content-max */
  --nav-z: var(--z-sticky);
  --nav-logo-height: var(--icon-lg);            /* 32; width follows 7.407:1 */
  /* The link's label matches the action's label, and 32 separates the
     two, by measurement 2026-09-25 at 1440 (label size, link : button;
     label-to-button space): Stripe 14:14 ~28; Ramp 14:14 32; Mercury
     16:16 32; Linear 13:13 20; Enzo 16:16 47. Five of five match sizes;
     the space runs 20-47, typically 32. It was 14 against our 16 with 20
     between, which read small and cramped (Jefferson). 32 = the link's
     16 of padding + a 16 gap, both on the scale. */
  --nav-link-text: var(--button-text);          /* 16, = the action's label */
  --nav-link-weight: var(--weight-normal);      /* 400 */
  --nav-link-color: var(--text-body);
  --nav-link-height: var(--button-height);      /* 44 */
  --nav-link-px: var(--space-4);                /* 16 */
  --nav-action-gap: var(--space-4);             /* 16: with the padding, 32 label to button */
  --nav-link-gap: 0;                            /* padding does the spacing */
  --nav-link-radius: var(--radius-control);     /* 6 */
  --nav-link-hover: var(--fill-rest);
  --nav-link-current: var(--text-primary);
  --nav-link-current-weight: var(--weight-medium);
  --nav-action-height: var(--button-height);    /* 44 */
  --nav-collapse: var(--bp-sm);                 /* below this, a menu button */
  --nav-menu-target: var(--button-height);      /* 44 square */

  /* ================================ DEMO PAGE ========================= */
  /* /demo on the new brand (Jefferson, 29 Sep: "flat white background,
     with the subtle pop and glow on hover, and the ambient background on
     the card itself like we have on the 2nd banner ... match the home page
     exactly"). The cards ARE banner 2's cards: hairline, --card-radius,
     canvas, e1 at rest, e2 and a 1px lift with the --stat-glow on hover,
     the pop in at --stat-pop-stagger. The diamonds came off the cards the
     same day; the section carries banner 3's swaying grid instead. Only
     what a form needs is new, and each value is named here. */
  /* Narrower and left-aligned (Jefferson, 29 Sep: "i liked it a lot
     better when it was left-justified and narrower"). The old page ran
     1120 (the old system's --content-max); nine grid columns is the nearest the
     grid gives, 9 x 94 + 8 x 32 = 1134. Header, content and footer share
     that one column, so the logo sits over the title as on the home page. */
  --demo-columns: 9;
  --demo-max: calc(var(--demo-columns) * var(--grid-column) + (var(--demo-columns) - 1) * var(--grid-gutter));   /* 1134 */
  --demo-card-padding: var(--closer-card-padding);   /* 48, the stat card's */
  /* Taller (Jefferson, 29 Sep: "extend it vertically more"), then a
     little less ("it should all fit on the standard screen in 1 viewing
     window"): from --bp-lg the cards end one rim above the window's foot,
     the hero card's rule, so at 1440 x 900 the page to the cards' bottom
     is one screen: 900 - 336 - 24 = 540 tall. The stat cards' proportion,
     421 : 567.75 (.platform__stat, about 720 at 1440), caps them on a tall
     window; content taller than either grows them. The calendar's rows and
     the note's box share out the height. */
  --demo-card-ratio: calc(567.75 / 421);
  /* the cards' top, less the rim: header, the 64 step, a one-line title
     and subtitle 24 apart, and the 64 to the cards (80 + 64 + 48 + 24 +
     32 + 64 = 312; 336 with the rim) */
  --demo-cards-top: calc(var(--nav-height) + var(--demo-top) + var(--h1-lh) + var(--space-6) + var(--lead-lh) + var(--space-16));
  --demo-card-gap: var(--bento-gap);                 /* 32, as the stat row */
  /* Header block, then the stat row's own title-to-cards step: rim + 80 +
     64 from the top of the window to the title. */
  --demo-top: var(--space-16);                        /* 64 */
  /* A form label, as the brand sheet draws it (docs/brand-system.html,
     .field label): Small 14/22 at 500 in ink, 4 above the field. */
  --field-label: var(--small);
  --field-label-lh: var(--small-lh);
  --field-label-weight: var(--weight-medium);
  --field-label-color: var(--text-primary);
  --field-label-gap: var(--space-1);                  /* 4 */
  --field-gap: var(--space-6);                        /* 24, the page's one step */
  /* The calendar. A day with times is a component fill (--fill-rest, g3,
     the lightest), hovering to --fill-hover; a day without is not a
     control, so its number drops to --g8, the step under --text-muted
     that the ramp keeps for inactive states (WCAG 1.4.3 exempts inactive
     components). Today is underlined at the link's offset. Cells are 44
     tall on a phone (--button-height, the page's control height). */
  --calendar-day-free: var(--fill-rest);
  --calendar-day-hover: var(--fill-hover);
  --calendar-day-off: var(--g8);
  --calendar-day-radius: var(--radius-control);
  --calendar-gap: var(--space-1);                     /* 4 */
  /* Six time slots show, then the list scrolls: 6 x 44 + 5 x 8 = 304. */
  --calendar-times-max: calc(6 * var(--button-height) + 5 * var(--space-2));
}
