Every ratio below is measured live from getComputedStyle against the shipped src/styles/tokens.css. Nothing is typed in. The same pairings are enforced non-interactively by npm run contrast.
AA text needs 4.5:1 (WCAG 2.2 SC 1.4.3). AA non-text and UI boundaries need 3:1 (SC 1.4.11).
| --text-primary | headings, readout numbers | |||
| --text-body | running prose | |||
| --text-secondary | supporting copy, captions | |||
| --text-label | eyebrows, stat keys | |||
| --line-control | interactive boundary | |||
| --line-hair | decorative divider | |||
| --mark-sun | sun marker · sun path · scrubber accent | |||
| --mark-sun-low | sun below the horizon | |||
| --mark-axis | street-axis spoke — LEGAL ONLY ON void/sunk/base | |||
| --mark-axis-text | prose about alignment |
| --text-primary | headings, readout numbers | |||
| --text-body | running prose | |||
| --text-secondary | supporting copy, captions | |||
| --text-label | eyebrows, stat keys | |||
| --line-control | interactive boundary | |||
| --line-hair | decorative divider | |||
| --mark-sun | sun marker · sun path · scrubber accent | |||
| --mark-sun-low | sun below the horizon | |||
| --mark-axis | street-axis spoke — LEGAL ONLY ON void/sunk/base | |||
| --mark-axis-text | prose about alignment |
| --text-primary | headings, readout numbers | |||
| --text-body | running prose | |||
| --text-secondary | supporting copy, captions | |||
| --text-label | eyebrows, stat keys | |||
| --line-control | interactive boundary | |||
| --line-hair | decorative divider | |||
| --mark-sun | sun marker · sun path · scrubber accent | |||
| --mark-sun-low | sun below the horizon | |||
| --mark-axis | street-axis spoke — LEGAL ONLY ON void/sunk/base | |||
| --mark-axis-text | prose about alignment |
| --text-primary | headings, readout numbers | |||
| --text-body | running prose | |||
| --text-secondary | supporting copy, captions | |||
| --text-label | eyebrows, stat keys | |||
| --line-control | interactive boundary | |||
| --line-hair | decorative divider | |||
| --mark-sun | sun marker · sun path · scrubber accent | |||
| --mark-sun-low | sun below the horizon | |||
| --mark-axis | street-axis spoke — LEGAL ONLY ON void/sunk/base | |||
| --mark-axis-text | prose about alignment |
| --text-primary | headings, readout numbers | |||
| --text-body | running prose | |||
| --text-secondary | supporting copy, captions | |||
| --text-label | eyebrows, stat keys | |||
| --line-control | interactive boundary | |||
| --line-hair | decorative divider | |||
| --mark-sun | sun marker · sun path · scrubber accent | |||
| --mark-sun-low | sun below the horizon | |||
| --mark-axis | street-axis spoke — LEGAL ONLY ON void/sunk/base | |||
| --mark-axis-text | prose about alignment |
The one light surface on the site: the print preview, a --linen mat inside the black rebate. Not a theme, not a toggle. No reserved mark is legal here — --mark-sun measures 2.34:1 on linen, which is the measurement that produced the dark-only ruling.
| --text-primary | headings, readout numbers | |||
| --text-body | running prose | |||
| --text-secondary | supporting copy, captions | |||
| --text-label | eyebrows, stat keys | |||
| --line-control | interactive boundary | |||
| --line-hair | decorative divider | |||
| --mark-sun | sun marker · sun path · scrubber accent | |||
| --mark-sun-low | sun below the horizon | |||
| --mark-axis | street-axis spoke — LEGAL ONLY ON void/sunk/base | |||
| --mark-axis-text | prose about alignment |
Two-tone: 1px --focus-inner (void) inside 2px --focus-outer (sage). Deliberately not amber — --sun is reserved, and an amber ring also measurably fails: against mid-grey #515151 it reaches only 2.65:1, below the 3:1 floor. Sage against void is 10.90:1, so over any photograph whichever ring the image swallows, the other survives.
SPEC §2 originally asserted ui-monospace resolves to SF Mono on macOS. MDN defines it only as "the default user interface monospace font" and does not say. Rather than assume, this measures the rendered advance width of 100 zeros and compares against known constants read from the font binaries: SF Mono 0.6182, Menlo 0.6021, IBM Plex Mono 0.6000, Inconsolata 0.5000. Since 2026-09-11 the keyword is no longer in the stack at all — this table stays because it is the measurement that explains why. One trap: the default proportional face also sets its digits at 0.5000 em, so a zero-width alone cannot tell Inconsolata from "name not recognised". Each row therefore also measures 100 i against 100 M; they are equal in a monospaced face and never in a proportional one.
What you are looking at: this site is a wall of tabular numbers. Until 2026-09-11 it rendered in two different mono faces depending on the browser — SF Mono in Safari on a Mac, IBM Plex Mono everywhere else — and size-adjust: 103.03% on Plex reconciled their widths (0.6182 vs 0.6000). Now the shipped webfont, Inconsolata, leads the stack for every browser, there is no size-adjust, and the only question is whether the webfont painted: the site's mono must measure 0.5000em per character (500/1000, read from the binary) and be monospaced. Menlo, the next real face, is 0.6021 — if you see that, the webfont did not load and every ch measure is 20% wider than designed.
In every browser, Safari included: the row marked var(--font-mono) should read 0.5000 and resolve to Inconsolata. There is no longer a Safari-only route — the stack does not start with ui-monospace, so SF Mono is never consulted. The ui-monospace alone row is kept as history: 0.6182 there means the keyword works in this browser (Safari), and a proportional result means it was skipped (everyone else). Neither affects the site any more.
Fraunces has no tnum feature and its digits are proportional — 1024 to 1461 units wide, a 43% spread. Both rows below are counting 0–9 in the same box. The mono row holds still; the Fraunces row breathes. On a scrubbing clock that is visible jitter.
font-variant-numeric: tabular-nums applied. It does nothing — that is the point.Note what is not here: an opsz value. Pinning the optical-size axis swings Fraunces' advance widths by 23%, and our display sizes are fluid, so a pinned opsz would be right at one window width and wrong at every other. font-optical-sizing: auto handles it. The lint fails the build if anyone pins it.
What you are looking at: every level in the system, at the size it is right now in this window. Resize the window and the three display levels move; the mono levels do not. That is deliberate — headlines want to fill their measure, but a column of numbers whose width drifts with the window is a column you cannot compare down.
ch, not pixels, so it is defined against whichever face actually painted — Inconsolata for everyone since 2026-09-11, Menlo only if the webfont fails — and the number of characters per line is the same either way.What you are looking at: the whole scale, drawn at real size. The rule is the number times 4px — --space-6 is 24px — so you never have to look one up. npm run lint:space fails the build on any hand-typed margin, padding or gap and tells you the nearest token.
Sections breathe on a large screen and compress on a phone, using the same clamp construction as the display sizes so headings and the space around them scale together instead of drifting apart. Resize the window and watch these move.
What you are looking at: a plate inside a black rebate, which is how every photograph, the globe and the dial will sit (SPEC §2). The rebate has a width and the plate has a corner radius, and those two numbers are not independent: if the outer radius equals the inner one, the black mat visibly pinches at each corner because the two arcs are not parallel. The rule is arithmetic — outer radius = inner radius + rebate width — so the --radius-rebate-* tokens are derived, never typed. This measures the real computed radii and fails if the arithmetic breaks.
What you are looking at: the npm run lint:type rule, running again in the browser against the rendered DOM. The static lint reads built HTML; this walks every text node that actually painted and reads its resolved font. Between them, a number set in Fraunces has nowhere to hide — including one that arrived from a template expression.