Colophon · this site
Every value on this site is on one of these lists.
This is the system the portfolio is made of, not the one at Sovren. It is here because the smallest system a designer owns is the one you can check. Every number below was measured from assets/css/site.css rather than remembered, and the specimens are painted by the tokens they document.
Eight declarations, applied in the order the chips land. Replay →
Four promises, and what it costs to break one.
A system is not a set of values. It is a set of values plus a promise about what happens when someone wants one that is not in the set. These four are written at the top of the stylesheet, above the tokens, so they are read before anything can be copied.
- 01Every value below the token block resolves to a token. A rule needing a size, space, color, duration or tracking the ramps do not name is a signal to extend a ramp, never to type a number. Four exceptions are allowed and all four are enumerated.
- 02Hairlines, never shadows. No radius, except the fluency pills and the gate’s disc. One border token exists, so there is no decision to get wrong.
- 03A color is declared only where it changes. Nothing restates what it already inherits, which is why a diff on this file is readable.
- 04One ground, one ink, two accents. Every tint composes from four channel tokens, so recoloring the site is a four-line edit and cannot be done partially.
Five channels, one ground, and an accent that no longer needs a second shade to speak.
The ground is a soft, warm black rather than a hard one. A true #000 behind warm off‑white text halos at every letter’s edge and reads as a screen rather than a page, so the ground sits a few steps up from it and warm rather than neutral, which is also why it is never retyped as #000 anywhere on the site. Salmon is the accent, and for the first time it is the accent for marks and for text: the primitive itself clears 7:1, so it does not need a second, darkened shade before a word can stand on it the way the old teal did. A lifted step still exists for small sizes, for comfort rather than to pass a line it was already past. Teal is new too, a second accent that exists to be a different participant from salmon: the gate’s second cursor, and nothing else. It never carries body copy, because a system with two hues that can each carry running text has no way to say which claim matters more.
Ink · contrast measured against the ground, #1b1917
- --inkHeadings and values. The only ink at full strength.15.31 : 1
- --ink-80Body copy. The default color on <body>, so most of the site is this.10.11 : 1
- --ink-72Secondary labels. The lightest ink the system still sets as running text, and it clears the AA line with room left over.8.30 : 1
- --ink-30The standing underline on a prose link, and the dim key in the Pay key grid. Decorative: never read as a word, so it owes nothing.Decorative · 30%
- --ink-28A dashed edge. Dashed means unfinished, everywhere on this site.Decorative · 28%
- --ink-22A border on hover. The only state change a well has.Decorative · 22%
- --ink-10The hairline, composed once into --hair. 12% on a dark ground reads as 10% did on paper, which is why the number moved and the token did not get renamed.Decorative · 12%
- --ink-02The only fill on the site. Used on a hovered target and behind an unfinished slot.Decorative · 3.5%
- --ink-015Fainter still, under a placeholder’s crossed diagonals.Decorative · 2.5%
Salmon · --acc, the accent that clears text contrast on its own
- --accMarks and graphics: rules, dots, carets, the focus ring, the drawn underline. The primitive at full strength, already past the 4.5:1 a word owes.7.07 : 1
- --acc-inkAccent text: eyebrows, live links, unit labels. Lifted one step for small sizes, not to pass a line --acc had already cleared.8.15 : 1
- --acc-70The rule a lede link draws under itself once the fold has settled.Decorative · 70%
- --acc-40A live underline in the ask band.Decorative · 40%
- --acc-10Selection ground, and the fill behind a working note.Decorative · 14%
Teal · --acc2, the second accent, never body text
- --acc2Teal marks: the peer cursor, a second series, anything that has to be told apart from salmon at a glance.9.42 : 1
- --acc2-inkTeal text. Rare on purpose: one short label at most, never a sentence of running copy.11.85 : 1
- --acc2-40A teal rule at partial strength, the same job --acc-40 does for salmon.Decorative · 40%
- --acc2-10A teal fill at partial strength, the same job --acc-10 does for salmon.Decorative · 12%
Two roles neither channel names · both on the gate
- --peerThe visitor’s own cursor tag. Set to teal rather than salmon, because salmon already carries the selected frame and its handles, and a cursor that matched would stop reading as a second participant.9.42 : 1 (= --acc2)
- --warnA refused password. The one color on the site not composed from a channel token, because a warning is not a tint of anything else.7.68 : 1
The ground’s plan grid
- --grid-lineA very faint plan grid behind the whole page, faded out toward the footer. Texture, not layout: nothing on the site positions itself against it.Decorative · 4.5%
- --grid-cellThe grid’s repeat: two linear gradients, one horizontal and one vertical, tiled at this size.96px
Where the tints stop being decorative
Everything below the ink, salmon and teal text rows is a divider, a fill or a decorative rule, which WCAG 2.2 exempts from a contrast requirement, because nothing there is read as a word. Selection is the pairing that looks like it should be checked anyway: accent text sits on a 14% accent tint rather than the ground itself, and composited, that pairing still clears roughly six to one, past the 4.5:1 a highlighted sentence owes.
Every semantic token, one row each, measured this second
The ratio column below is not typed. A script composites the token’s real computed color over the ground’s real computed color, the moment this page renders, and prints whatever comes out: the only way a contrast claim can survive someone editing section 2 without also editing this paragraph. A token with no readable role (a border, a fill, a grid line) shows “decorative” instead of a ratio, because WCAG has nothing to say about a rule that is never read as a word.
- --bgThe ground itself. body{background:var(--bg)}Every page’s <body>, and the gate’s plate.
- --inkrgb(var(--ink-rgb)), full strength
h1,h2,h3{color:var(--ink)}· every heading and value. - --ink-80ink at 80%
body{color:var(--ink-80)}· the site’s default running copy. - --ink-72ink at 71.5%
.rail .y{color:var(--ink-72)}· the milestone year, secondary labels. - --ink-30ink at 30%
.tie{border-bottom:1px solid var(--ink-30)}· the standing underline on a prose link. - --ink-28ink at 28%
.slot{border:1px dashed var(--ink-28)}· every unfinished edge. - --ink-22ink at 22%
.card:hover .ph{border-color:var(--ink-22)}· the only state a well’s hairline has. - --ink-10ink at 12%, composed into --hair
--hair:1px solid var(--ink-10)· the one border token, everywhere. - --ink-02ink at 3.5%
.nxt:hover{background:var(--ink-02)}· the only fill on the site. - --ink-015ink at 2.5%
.ghost{background:var(--ink-015)}· under a placeholder’s crossed diagonals. - --accrgb(var(--acc-rgb))
.pull{border-left:2px solid var(--acc)}· rules, carets, the focus ring, and text. - --acc-inkrgb(var(--acc-ink-rgb)), lifted a step
.eyebrow{color:var(--acc-ink)}· every eyebrow, live link, unit label. - --acc-70acc at 70%
.lede a::after{background:var(--acc-70)}· a lede link’s drawn underline. - --acc-40acc at 40%
.ask .links a.hot{border-bottom-color:var(--acc-40)} - --acc-10acc at 14%
::selection{background:var(--acc-10)} - --acc2rgb(var(--acc2-rgb))
.peer{color:var(--bg);background:var(--acc2)}· the gate’s second cursor, a filled key. - --acc2-ink#5eead4Reserved for teal text; not yet spent on a live selector.
- --acc2-40acc2 at 40%The teal rule at partial strength, --acc-40’s counterpart.
- --acc2-10acc2 at 12%The teal fill at partial strength, --acc-10’s counterpart.
- --hair1px solid var(--ink-10)The one border. Every card, band, nav and case rail.
- --grid-lineink at 4.5%
body::before{background:linear-gradient(…var(--grid-line)…)} - --peer= --acc2
.me b{background:var(--peer)}· the visitor’s own cursor tag on the gate. - --warn#ff8a80, not composed from a channel
.di-plate.bad .di-lab{color:var(--warn)}· a refused password.
What salmon may do
- ✓Carry text, at any size, because the primitive itself clears 4.5:1 on the ground.
- ✓Draw a mark: a rule, a caret, a focus ring, an underline.
- ✓Mark the ask band’s live link and the eyebrow that opens every section: the site’s one recurring “pay attention here.”
What salmon may not do
- ✕Fill a large area. It is a mark and a text color, never a background wider than a hairline’s worth of tint.
- ✕Sit next to teal doing the same job. The two never label the same kind of thing on one screen.
What teal may do
- ✓Mark a state that changed: the gate’s second cursor, a key filling in inside the Pay figure.
- ✓Stand apart from a salmon selection deliberately, when two things need to read as different participants.
What teal may not do
- ✕Carry body copy. --acc2-ink is reserved for one short label at most, never a sentence.
- ✕Open a section or mark a link the way the salmon eyebrow does; that is the accent’s job, and giving teal the same one erases the distinction the whole redesign exists to draw.
Why there is no light mode
The whole color system is five channels composed against one warm dark ground; a light variant is not a media query away, because every semantic token would need a second composition rather than an inverted lightness, and nobody has asked for one.
Three families with three jobs, and a ramp that has stopped being one.
Serif carries every heading and every claim set large, sans carries running copy, mono carries anything that is a label, a year, a unit or a number being compared. The family tells you what kind of statement you are reading before you read it. The ramp below is the weakest thing here, and every step is drawn at its real size.
The type ramp · 25 steps, live
- AaThe micro label. Rail note, meta key, callout number, band kind, artifact tag.--fs-01 · 10px
- AaThe gate: its one label, and the line along the bottom.--fs-02 · 10.5px
- AaThe eyebrow that opens a section. Fact keys, back link, case footer, the orienting line.--fs-03 · 11px
- AaThe fluency pill, and only the pill.--fs-04 · 11.5px
- AaAn unconfirmed fact, rendered as a visible hole.--fs-05 · 12px
- AaCard meta, placeholder brief, artifact brief, figure caption.--fs-06 · 12.5px
- AaThe milestone year, the unit beside a numeral, a fact's mechanism, the ask links.--fs-07 · 13px
- AaThe contents rail, and a case study's meta values.--fs-08 · 13.5px
- AaCard deck, domain-band description, callout text.--fs-09 · 14px
- AaA belief cell on the About page.--fs-10 · 14.5px
- AaBody. The default size on <body>, so most of the site is this one step.--fs-11 · 15px
- Aah3, and the gate's field on a phone.--fs-12 · 17px
- AaA subheading inside a case section.--fs-13 · 17.5px
- AaThe gate's field.--fs-14 · 18px
- AaThe lede, and a section deck.--fs-15 · 19px
- AaA case study's standfirst.--fs-16 · 20px
- AaThe pull quote, and the turn on the About seam.--fs-17 · 21px
- AaThe quiet card, the unit in the contact sheet with no photograph.--fs-18 · 23px
- AaA fact value, on a phone.--fs-19 · 24px
- AaThe next-case title.--fs-20 · 26px
- AaA case section heading.--fs-21 · 27px
- AaA fact value. The largest number on the home page.--fs-22 · 28px
- Aah2.--fs-23 · 32px
- AaA case study's h1. Fluid.--fs-24 · 34–46 fluid
- Aah1. The largest type on the site. Fluid: clamp(38px, 4.4vw, 56px).--fs-25 · 38–56 fluid
Eleven of those steps live inside five pixels
Between 10px and 15px the ramp goes 10, 10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15. The largest ratio between neighbors is about 1.05. A closed ramp exists to force a choice; this one forces nothing, so nobody adding a caption can tell whether 12.5 or 13 is correct.
What the documentation says
Fourteen steps, with a ratio rationale: 1.07 to 1.13 through the reading sizes, 1.14 to 1.17 above them.
What the stylesheet has
Twenty-five. The documented fourteen, plus eleven added between them, and a rationale that no longer describes anything.
The audit raises nothing, because check 1 asks whether every font-size is a token and has no opinion about how many tokens there are. That is the most useful thing on this page: a guardrail that cannot see the defect it was built to prevent.
The specimen sheet: all three families, every step
Below is what the ramp looks like from a family’s point of view rather than a selector’s: the same 25 sizes, set in serif, sans and mono at once, each carrying the tracking and line height that family actually uses on this site (serif tightens to --ls-tight, mono opens to --ls-06 and goes uppercase, sans runs at rest). No row above is a repeat of the earlier ramp; that one shows which single family a step is actually spent on, this one shows what the other two would look like if it were spent there instead.
Ten text roles, and the recipe under each
Every role below is the real element, wearing the real class, not a redrawn copy: the h1 in the first row is an actual <h1>, and it inherits site.css section 4 exactly like the one at the top of index.html does. The line beside it names the declarations that make it that role rather than plain text.
A headline
h1 { font-family:var(--serif); font-size:var(--fs-25); line-height:var(--lh-01); letter-spacing:var(--ls-tight) }A section head
h2 { font-family:var(--serif); font-size:var(--fs-23); line-height:var(--lh-03); letter-spacing:var(--ls-tight) }A subheading
h3 { font-family:var(--serif); font-size:var(--fs-12); line-height:var(--lh-04); letter-spacing:var(--ls-tight) }A lede paragraph, set in the serif at reading-plus size, capped at the tight measure.
.lede { font-family:var(--serif); font-size:var(--fs-15); line-height:var(--lh-07); letter-spacing:var(--ls-snug); max-width:var(--measure-tight) }Body copy: the sans, at reading size, the default on <body> so most of the site never has to declare it again.
body { font-family:var(--sans); font-size:var(--fs-11); line-height:var(--lh-07); color:var(--ink-80) }A card’s deck line, one step down from body, tabular where a number sits in it.
.deck { font-size:var(--fs-09); margin-top:var(--s-01) } .deck b { font-variant-numeric:tabular-nums }The two axes most systems leave as literals.
A type system that tokenizes size and nothing else has done a quarter of the job. Both of these are closed ramps, which is the part worth copying.
Line height · nine steps, tightening as the type grows
- --lh-01h1. Display serif, set solid.1.08
- --lh-02A fact value. A single line of tabular numerals.1.15
- --lh-03h2, and the next-case title.1.2
- --lh-04h3, the contents rail, a milestone line on a phone.1.35
- --lh-05The pull quote, the turn, the quiet card. Serif at reading-plus size.1.4
- --lh-06A case standfirst, a fact’s mechanism, meta values.1.45
- --lh-07Body. The default on <body>, and the lede.1.5
- --lh-08The placeholder brief and the artifact brief. Small sans, set open.1.55
- --lh-09A belief cell, and a working note. The most open setting on the site.1.62
Tracking · serif tightens, mono opens, nothing in between
- --ls-tightEvery serif heading, and every claim set large.−.02em
- --ls-snugSerif at reading size: the lede, the standfirst, a section deck.−.01em
- --ls-01The unit beside a numeral..04em
- --ls-02The fluency pill..08em
- --ls-03The case footer..1em
- --ls-04The rail note, and an unconfirmed fact..12em
- --ls-05The back link, the sheet note, a figure caption’s key..14em
- --ls-06The eyebrow, and every mono micro label. The workhorse..16em
- --ls-07The gate’s label, and its field on a phone..2em
- --ls-08The gate’s field. A passphrase, spaced like one..34em
Fourteen steps, every one a multiple of four, and three that are not.
Nothing on this site is 7px or 22px. Each bar below is drawn at the width of the step it names, so the ramp is showing you itself.
The spacing ramp · drawn at true width
- The tightest gap. A caption to the line above it.--s-01 · 4px
- A label to the thing it labels.--s-02 · 8px
- Inside a row: a year to its line, a number to its unit.--s-03 · 12px
- Between the parts of a heading block.--s-04 · 16px
- The nav’s vertical padding. A lede to the headline above it.--s-05 · 20px
- The work grid’s gutter. The gap between four facts.--s-06 · 24px
- Between cards in a column, and inside the next-case target.--s-07 · 28px
- Between nav links. Above a section’s meta strip.--s-08 · 32px
- The gate plate’s horizontal inset.--s-09 · 40px
- The hero’s column gap, and the space above the ask band.--s-10 · 48px
- Between a case study’s sections. The rail to the reading column.--s-11 · 56px
- The About seam, and the space before the next case.--s-12 · 72px
- The bottom of a case study. The only step used once.--s-13 · 96px
- The top of the fold. The largest space on the site.--s-14 · 112px
The three that leave the ramp, and why
Each is declared as its own token with its reason on the line. The documentation does not name them at all.
Off-ramp values
- --pad-pillA pill’s padding is typographic inset rather than layout, and matching the reference exactly mattered more than matching the ramp.6px 14px
- --gap-unitBetween a numeral and the unit beside it. An optical gap, not a spatial one.6px
- --s-08bThe contact sheet’s one off-ramp step, between 32 and 40.36px
Two columns, and three aspect ratios that are the whole height system.
Height rhythm comes from per-card ratios, never from a multi-column span grid, because a span grid makes the reading order argue with the visual order. Adding a card means choosing one of three, not inventing a fourth.
The ratio set
- .a169A wide artifact: a console, a flow, a running prototype.16 : 9
- .a45A portrait: a person, a phone, a tall document.4 : 5
One hairline, no fill, no radius, no shadow. The crossed diagonals are the printer’s mark for “picture goes here”. Delete the ghost, drop the artifact into the same well, and nothing else about the card changes.
Live component, rendered hereWhy the ratios survive the phone
At 760px the grid gives up its second column and every well keeps its ratio. Flattening a portrait to 16:9 crops the head off whatever is inside it, so the breakpoint deliberately does not touch aspect-ratio, and the comment above it says why.
Where the well is reused
Three places, one definition: the work grid, the About contact sheet, the About portrait. The portrait gets no private treatment, because a second visual language for one photograph is how a system forks.
The measure, the wrap and the gutter
Three layout tokens govern every non-full-bleed section on the site. Values below are read live from :root, the same way the space ramp is; nobody typed a pixel here.
Layout tokens
- .wrap’s cap. 1180 lines the nav up with the case column’s right edge.Every section
- .wrap’s side padding. Fluid, so it never pins the same value to a 400px screen and a 2000px one.Every section
- Prose’s cap. 66 characters is where a line stops being read and starts being skimmed.Case body, this page
- A serif lede’s cap, shorter because it is set larger..lede, .chead .sub
- The case sidebar’s fixed column..case
Five breakpoints, largest first, one collapse each
Section 15 is the only responsive section on the site; the audit’s check 10 exists to keep it that way. Each rule fires when the thing it governs runs out of room, not on a shared schedule, which is why there are five queries and not two.
| Breaks at | What collapses | What stays |
|---|---|---|
| 1040px | The four facts drop to a 2×2 grid; nav links tighten their gap. | Everything else, unchanged. |
| 900px | The case sidebar loses position:sticky and the contents rail hides; the two-up .two grid stacks to one column. | The reading column keeps its 66ch cap. |
| 860px | The hero and the About intro give up their second column; .pairs stacks to one column, top to bottom. | Row order inside .pairs, which the placement map preserves. |
| 760px | The work grid gives up its second .gcol; the lanes table drops to stacked rows with printed labels. | Every well’s aspect-ratio, deliberately never flattened here. |
| 620px | The case diagram swaps for its .dia-alt text list; the colophon’s three-column .brow drops to one column; the platform row’s pill moves to its own line; the nav subtitle hides. | The gate’s inspector (.ginsp) simply disappears rather than reflowing: there is no room to shrink it into. |
One curve, four durations, and a stagger that cannot re-time itself.
Motion tokens
- --easeThe only curve on the site. Decelerating: it starts fast and lands soft, so a 0.22s move and a 0.72s move read as the same gesture at two speeds.cubic-bezier(.16,1,.3,1)
- --dur-fastA dot sliding across the gate’s plate..22s
- --durColor and border on hover. The default..3s
- --dur-slowAn element arriving..5s
- --dur-exitThe gate dissolving, and the rule a lede link draws under itself..72s
- --dur-sigThe signature writing itself, on the gate. The slowest named duration on the site.1.6s
Arrival delay is a token on the element rather than an nth-child rule, because an nth-child stagger silently re-times itself the moment anything is inserted above it. Three delay steps exist and no more. Under a reduced-motion preference the arrival, the drawn underline and every transition are removed rather than shortened.
The curve, traveling
One easing function carries every move on the site, from a 0.22s hover to the 1.6s signature. Click to send the dot across at --dur-sig on --ease itself, read live from :root rather than approximated: fast off the mark, then spending most of the distance settling into the landing, which is why nothing here ever feels like it slams.
Ten moves, one older than the rest
Sections 16 and 17 layer nine new interactions onto the components above; the tenth, the .rv rise, predates the redesign but both 16.6 and 16.7 had to account for it, so a motion inventory that skipped it would be incomplete. None invents a new duration or a second curve; each cites the token it spends and what is left when motion is switched off.
| Move | Trigger | Duration | Easing | Reduced motion | Lives in |
|---|---|---|---|---|---|
| Skim or read | Hover, focus or the Read toggle over the hero lede | --dur-slow | --ease | Shown clear; no blur ever applied | site.css §16.1 |
| Spotlight well | Pointer over a .card .ph | --dur | --ease | Glow and ring never render (no pointer to spotlight, and no motion to spend) | site.css §16.2 |
| Split-flap rail | Milestone rail entering view | --dur | --ease | .flap-host never built; plain text throughout | site.css §16.3 |
| Platform marquee | Runs on load, pauses on hover or offscreen | 90s (mq-scroll) | linear | Track frozen; edge mask removed | site.css §16.4 |
| Inspect mode | The Inspect toggle in the footer | --dur | --ease | Overlay still toggles; only the color transition is skipped | site.css §16.5 |
| Signature wipe | Footer entering view | --dur-sig | --ease | Mark shown fully drawn, clip-path unset | site.css §11, §16.7 |
| View transitions | Any same-origin navigation between a card and its case | --dur-vt-root / --dur-vt-group | --ease | Cross-fade and group morph both disabled; hard cut | site.css §16.6 |
| Wire sweep scrubber | Drag, arrow keys, or auto-play on a case figure entering view | --dur-slow | --ease | Entry auto-play skipped; the drag itself still works, since dragging is the visitor’s own input, not decoration | site.css §17.2 |
| One in five hundred | The Pay figure entering view | --dur-fast (8ms/row stagger) | --ease | Grid renders filled, no per-key stagger | site.css §17.3 |
| The .rv rise | Any .rv element entering view, gated on document.fonts.ready | --dur-slow | --ease | Shown at rest: opacity 1, no translateY | site.css §13, §16.7 |
A hundred selectors, and the ones deliberately missing.
Sections 5 through 13 of the stylesheet are the components, and they reference the semantic tokens and nothing else: 109 class selectors, 244 rules, fifteen views. A component earns its place by appearing on more than one page, or by holding a rule that would otherwise be retyped.
The load-bearing ones
- .wrapThe page measure. Everything that is not full-bleed sits in one.Every page
- .phThe well. One hairline, no fill, no radius, no shadow. Whatever goes in fills it edge to edge.3 uses
- .ghostA well carrying its own brief until the artifact exists.Placeholder
- .band / .browHairline rows: name, description, kind. Every specimen table on this page.This page
- .factsFour numbers, each wearing its mechanism underneath. Never five.Home
- .case / .side / .tocA sticky contents rail and one reading column capped at 66 characters.6 pages
- .slotThe brief for a figure that does not exist yet, sitting in the space that figure will fill.Placeholder
- .needsAn unconfirmed fact, rendered as a visible hole. A gap is honest; a plausible guess is not.Temporary
- .platA platform row on the home page. A native <details>: the name and the role chip stay on, only the copy folds, and no script is involved. Built from .pill and .deck.Home
- .nxtThe next case as a full-width target rather than a text link.6 pages
- .loopThe six steps of the work, find through hear back, as one segmented rule. A step is .on only when Kyle did it and the page proves it; .off steps stay visible and muted, with screen-reader text saying so. Compact under a case’s .meta; .lg is How I work on the home page. Rides the parent’s .rv, no motion of its own.Home, cases
Two of those, the placeholder and the artifact slot, exist to make unfinished work look unfinished, and both are designed to be deleted rather than filled in. A third, the working note, was deleted from the stylesheet the day the last page carrying one was finished.
Shaped into the stylesheet and deliberately not shipped, because a rule no page uses is dead code and the audit would catch it: the decision block, the hairline data table, the two-column list, the artifact strip, the figure-with-caption. Each gets added when the case it serves is written.
Five patterns · none in the stylesheetEvery state, rendered rather than described
Each row below is the real class, not a picture of it. Hover, focus, click and open the ones that take input; the rest render every state they have because CSS gives them no other way to reach it.
The glow and the lit hairline both key off pointer position inside the well, the same script the home page runs.
- FindProof line, eight words at most
- SpecProof line
- DesignProof line
- BuildProof line
- ShipProof line
- Hear backNot my part on this project
Closed by default
Design lead
Hover the summary row to see the title take --acc-ink; the mark itself flips glyph on [open].
Open on load
Design lead
A native <details>, so this state needs no script and keeps its keyboard behavior for free.
Sixteen mechanical checks, run before every commit.
The contract is enforced by a script, not by discipline. It exits with a list rather than an opinion: it will not say whether the design is good, only whether the system is still intact.
tools/audit.py
- 01Every font-size is a token.Type
- 02Every spacing value above 3px is a token.Space
- 03No color literal outside the token block.Color
- 04No selector that no page uses, including classes JavaScript adds at runtime.Dead code
- 05No token nothing references.Dead code
- 06No var() pointing at a token that does not exist.Integrity
- 07Every local link resolves to a file that is there.Links
- 08Every in-page anchor resolves to an id on that page.Links
- 09Exactly one top-level :root, so there is one place to look.Structure
- 10Breakpoints declared in descending order, in one section.Structure
- 11Head furniture present on every page: viewport, theme color, stylesheet, favicon.Pages
- 12The italic budget: no page renders more than two italics per audience.Type
- 13Every rank in the deal order points at a case that exists in the pool.Content
- 14Variant blocks are paired: no block that only one audience gets.Content
- 15Every page carrying variant copy declares a default on the html element.Content
- 16Every tint composited over the ground, measured, and compared to the role written on its token line.Color
Two are unusual enough to point at. Check 12 counts italics per audience, stripping the variant not being rendered, because both audiences’ copy lives in the DOM at once. Check 4 reads classList calls, so a selector only JavaScript applies does not read as dead. Both were written after the check produced a false positive once.
What it still cannot see
It has no opinion about how many tokens there are, and it cannot compare the stylesheet against the documentation. Both of those are the first item on the next list.
Four things. One is now fixed, and the fix is in the file.
Each one names its fix, because a list of complaints is not a critique. Two items the 2026 color redesign carried away on its own, a lighter ground with more headroom in it than the old one had, so they are not listed here as fixed; they are simply gone.
01 · The documentation and the stylesheet disagree
DESIGN-SYSTEM.md describes fourteen type steps and twelve checks, the README says fourteen checks, and the file has twenty-five steps and sixteen checks. Fix: generate the document from the stylesheet. Every fact in it is already in the file the audit parses.
02 · Twenty-five type steps, eleven inside five pixels
A closed ramp exists to force a choice; at ratios of 1.05 there is none to force. Fix: collapse the reading sizes back to the documented six and pull the half-steps one at a time, letting the dead-token check say when each is genuinely unused. The only one of the four that would change how the site looks.
03 · A three-column hairline row does not fit a phone
The About page’s domain band reserves 320px of fixed track and gutter before the description column gets a pixel; a 375px phone has about 319px. Fix: one rule inside the existing 620px block. This page carries it, scoped to itself, so you can see it working.
04 · The audit could not measure the thing the color system is about
The whole argument for two accents is a contrast threshold, and the guardrail never checked one. Fixed: check 16 ships as tools/audit-contrast.py. It composites each tint over the ground, measures the ratio, and compares it to the role written on the token line, so the next token that claims a number it does not clear fails a build rather than a review.
A system is not the values. It is the argument about what happens when someone wants a value that is not in the set, and the argument is only real if a script can lose it.