Outcome

This site had a design document. DESIGN.md, seventeen kilobytes, written with real conviction about typographic personality and colour philosophy. It was also wrong about the single thing it was most confident about.

It described the accent as “an electric blue-violet primary (hsl(250, 98%, 59%) / #3052FD)”, and went on for a paragraph about how, in dark mode, that primary “flips completely to near-white” because “the electric blue would feel garish on a dark background.” Good writing. A real argument.

The code says --primary: 153 100% 50%. Neon green. In both themes. The flip it describes does not happen, the hex it names appears nowhere in src/, and neither does #F3F2F2, the warm off-white it calls “the only decorative use of a non-semantic colour in the whole system.”

I don’t know exactly when that stopped being true. That’s the point. Nothing failed, no build broke, no test went red — the document just quietly became fiction while the site kept shipping.

So this replaced it. Everything below is measured out of the running page rather than written down about it.


Challenge

A design system that documents itself in prose has a drift problem with no detector. The document and the code start in agreement and diverge on the first token change, and the divergence is invisible because prose has no build step.

I’d already found the same failure twice in one week, both times by accident. The workshop pilot page still announced “Default Subframe theme — orange brand, Manrope” under buttons that had been painting cyan Satoshi since the theme sync landed months earlier. A case-study cover carried alt text describing a side-by-side component gallery; the image was a headshot.

Three instances of the same bug. The common factor is that all three were assertions about what the code does, stored somewhere the code can’t reach.

Solution

The specimens on this page are React islands that read the live document — getComputedStyle on :root, plus off-screen probe elements — and report what they find. They hold no values of their own. Resize the window or hit the theme toggle in the dock and the numbers move, because they were never numbers, they were measurements.

Colour

Six tokens carry every decision on this site. Here they are, resolved in whatever theme you are reading in:

    measuring…

    The real dark-mode mechanism is the inverse of what the document claimed, and more interesting than it. --primary doesn’t move: neon green, rgb(0, 255, 140), identical in both themes. What flips is --primary-foreground, the text on the accent — a deep indigo rgb(29, 29, 94) in light, near-black rgb(23, 23, 23) in dark.

    That is the better design, which is presumably why it won. The brand stays fixed, so a green button is the same green everywhere and I never have to remember two accents. Legibility on top of it is the variable, decided per theme by a token instead of by me.

    Typography

    Two variable faces from the Indian Type Foundry, doing opposite jobs. Satoshi carries the structural load — headings at weight 900, UI labels, buttons. Erode is for reading, and only reading: body copy in articles, at light weight, where a paperback feel beats a UI feel.

    The mapping survives as a rule I can state in one line: Satoshi for everything except reading. It’s enforced at the Tailwind config layer, which is what lets it survive a Subframe re-sync — the font tokens are overridden in project config, and project config beats the vendor preset.

    The scale is where it gets less obvious:

    StepSpecimenComputed× previous

    measuring…

    Every step is a clamp(), so nothing jumps at a breakpoint. But look at the last column at two window sizes. On a 390px phone, each step is about 1.14× the one below it. On a 1440px desktop, the same steps sit at 1.25×.

    The ratio is fluid, not just the sizes. The hierarchy is compressed on a phone, where a 1.25 ratio would push a display heading off the screen and leave body copy nowhere to go, and it opens up on a desktop, where the same headline gets to be genuinely large. text-6xl runs 47px on the phone and 114px on the desktop — not the same design scaled, a different shape of hierarchy at each end.

    I’d like to claim I planned that. I didn’t; it fell out of nine independently tuned clamp expressions. I only found it by measuring for this page, which is a reasonable argument for building the measuring thing.

    The measuring thing then caught a fourth instance of the same bug, in itself. The measurement columns above were set in Tailwind’s font-mono, which in this project does not resolve to a monospace font — the Subframe preset remaps that token to Satoshi, and inside an article body these cells inherit Erode instead. Both are proportional, so tabular-nums had nothing to align and a column of numbers was quietly ragged. On a page arguing for typographic precision. The component now carries an explicit stack and a comment explaining why it must not be “simplified” back to the utility class.

    Layout

    Article bodies sit in a three-zone CSS grid, so a block picks its width by saying what it is rather than by carrying a pixel value:

    • Content — 63ch. The default. Prose, lists, quotes.
    • Breakout — 80ch. Comparisons, wide tables, code diffs. The two specimens above are in this zone.
    • Full-width — edge to edge. Hero stills, dense image grids, tinted section breaks.

    The grid is Kevin Powell’s pattern: named grid lines on the container mean a child opts into a zone with one class and nothing else has to know. --padding-inline: 2rem holds the gutter at every size.

    The reason it’s worth having is the negative case. Before this, widths were per-component decisions, and every new article-shaped page re-litigated them slightly differently. Three names removed the argument.

    What I’d do differently

    Build the measuring version first. DESIGN.md took real effort to write, and the effort went into prose that was decaying from the day it was committed. The component on this page took an afternoon and cannot lie — if someone deletes --primary tomorrow, this page renders the gap instead of the old value.

    The lesson generalises past design docs: any claim about what a system does, stored where the system can’t reach it, is a claim with an expiry date you won’t be told about. The fix isn’t discipline about updating docs. It’s moving the claim to somewhere it gets evaluated.