← Back to the screen

Design system portfolio

centauri — the record

The four direction lines behind this page, the generator command that produced its tokens, and the pairs it was measured against.

Brief #20. The house system: the firm's own internal console — the pipeline, the work list, the week and the ninety days behind them.

The four direction lines

LeverDecision
ArchetypeShell. A collapsible rail against one work area. The rail is 232px expanded, 56px collapsed, and a tab bar under 600px.
Data stanceTable-first. The pipeline table is the product; the stat strip, the sparklines, the bar list and the twelve-week line chart are all drawn to the table's grammar rather than a charting library's.
DensityThe console profile: 11 / 12 / 13 / 14 px, 28px controls, 32px rows, 10×6 cell padding.
The one bold thingThe four-step reading ladder used as intended, with datum (13px) reserved for the size a value is read at — and absence rendered as three visibly different marks.

Why this product suits this system: Centauri was written for exactly this screen. Its premise is reading numbers, its first-class object is the dense table that survives a phone, and the four-step ladder exists so the size a value is read at has a name. A pipeline of thirty-eight rows read at a desk and again at a kerb is the case the system was measured against.

The five declarations

  1. The ground — surface. The cool near-white of a drawing sheet. Rules divide it; a box appears only around a genuine container (the scrolling table box and the code block are the only two on the page).
  2. The hue budget — three, the firm's own. A navy neutral carries the ground, the rule, the edge and all three text steps. One blue carries the one action per screen, the sort tick and the current destination. One red carries what a reader must not miss. Status marks and person tints sit outside the budget and encode nothing a reader has to decode.
  3. The worst surface — surface, in both themes. strong and prose clear 7:1 there, muted and subtle clear 4.5:1, edge clears 3:1. Measured: muted on surface at 11px is 6.84:1 light and 8.43:1 dark; prose on surface at 13px is 12.17:1 light and 13.98:1 dark.
  4. How absence is rendered — three marks, never one:
  5. zero is written 0, with its dimension drawn at hairline length (City of Fairview: a pilot that carries no fee);
  6. data-absent="null" is an em dash — nobody gave a value (Parker County MUD 3's value; Fairview's next action);
  7. data-absent="pending" is a hollow dashed box — nobody has worked one out yet (Trinity Basin's value and its eight-week touches). Each carries a visually-hidden sentence, so the three are three states to a screen reader as well as to an eye. A date is read against today rather than on its own: the one row whose next action has run out carries a critical mark and the word, and the one that falls today carries the word alone.
  1. One signature object — the dimension line: a quantity drawn to a stated domain of 150,000 USD with a tick at each end. The same drawing device ends the sorted column head, marks the current navigation destination, and is the bar of the channels list.

No lit surface is declared. console+lit is refused by ADR-010 and this page is a table, a form and a list.

Generator

The token layers are generated output, linked in the mandated order and never edited:

node generate.mjs

Palette: preset ink, register deep, palette.py 1.0.0 — the shipped default since ADR-018, and the firm's own budget. The page carried quartz until 2026-09-16, when the vendored layers were refreshed from the system and the preset moved with them. Six links, in order: centauri.spine.css, centauri.console.css, centauri.base.css, centauri.nav.css, centauri.table.css, centauri.chart.css.

The three companion pages swap the profile and that system's layers and change nothing else: field.css + surface.css, catalog.css + shelf.css + table.css, marketing.css + page.css + expressive.css. The palette, the focus ring, the marks, the corners and the motion rungs are the same files in all four.

The second state

The work list under an active filter that matches nothing: the heading and the filter bar stay, the empty result names what the filter excludes and what clearing it would show, and the control that clears it is the primary action of that region and carries the same words as the one in the heading. Both states are live — the filter chip and the button swap, and focus lands on the one control that survives both.

The other hard case is in the product itself: a 51-character organization name (North Central Texas Municipal Water Authority Board) in the identity column, which wraps rather than widening the table, and is the record heading at 375px.

Deviations

A note for anything that measures state

Every interactive state in Centauri is one implementation: a ::after overlay at 8% hover, 10% focus, 10% pressed and 12% selected of currentColor. The element's own background-color and border-color therefore never move, in any state, by design. A probe that samples only the element's computed properties reads every control on this page as inert; sample ::after beside them. Measured under a real pointer at 1280px: brand, rail toggle, every destination, the primary action, the theme button, the three sort controls and both filter buttons go ::after opacity 0 to 0.08.

The two that genuinely had no state layer were fixed here rather than in the layers, because both were the page's class use: the primary action is a <button> on this screen (it opens the quick-add row rather than navigating), so the nav layer's .centauri-nav a::after could not reach it, and the sort controls carried no centauri-state. Both now do, and the sort control fills its head so the wash lands where the layer already puts the pointer's cursor.

Gates

import_lint.py systems/centauri/            clean (the spine, the declared profiles, base, nav,
                                           and only each system's own layers; every page declares
                                           @centauri-multi-profile, because a product picks one)
nav_lint.py    index/field/catalog/marketing  no findings on any of the four
parity_check.py --allow "Collapse navigation" aria-375 aria-1280   parity holds,
                                           23 controls at both widths
screen_probe.py systems/centauri/           clean — 872 focusable elements over 5 pages,
                                           7 viewports, reduced motion and forced colors
audit_page.py  index.html                2 info (touch icon, manifest)
slop_scan.py   index.html                clean
craft_lint.py  index.html                clean
motion_lint.py index.html                clean
layout_lint.py index.html                clean
copy_lint.py   index.html                15 findings: 13 warning, 2 info — title-case on
                                           the organization names in the table, and passive
                                           voice in the design notes
aria_lint.py   index.html                0 findings
a11y-probe                               clean in both themes, both the screen and its four
                                           companions
direction_check.py --ledger              1 warn, kept: hero-undecided over all five pages.
                                           A shell has no first screen, which the skill says
                                           in as many words; the tool reads five documents
                                           and asks each of them anyway
tools/check.sh centauri                  PASS — every gate ran and every gate is clean

Re-run 2026-09-16, after the vendored layers were refreshed from the system and the page took the account block, the ninety-day chart and the phone's way out.

Widths measured in Chromium at 320, 375, 600, 700, 760, 800, 860, 900, 950, 1000, 1024, 1280, 1440 and 1920: the document is never wider than the window, and no console errors at any width.

The companion page: motion.html

The screen shows what the system looks like at rest. Twelve of the twenty-one pages here have no keyframes at all, and a plate cannot carry a duration, so the one layer this portfolio could never show is the one every reviewer asks about. motion.html is that layer, in the same system, reached from the ledger row ("The motion, live") and from the console screen's masthead.

Its four direction lines

LeverDecision
ArchetypeDocument. A reference read top to bottom, with a specimen wherever a sentence would have to describe movement instead of showing it. The console screen beside it is a shell; this deliberately is not.
Data stanceProse-first, every duration drawn.
DensityThe console profile, unchanged.
The one bold thingThe dimension line — the system's own signature object — measuring time. A duration is a quantity against a stated domain, which is what that object draws.

What is on it

Six timing rungs on one 320 ms domain, grouped Arriving and Leaving; the seven verbs the Console layer ships, each with a specimen you can trigger; the frequency ladder with the same panel at three rungs; four counter-cases, each running the same event twice; a switch that applies the shortened reduced-motion path so it can be watched on a machine where the setting is off; and four systems' published timings side by side.

Why it is not a twenty-second entry

The contract reserves the spec panel as the only place a page's subject is the design system itself. This page's subject is motion throughout, so it is a companion to the console screen rather than a member of the set. It is not counted in the twenty-one, not in the similarity test, and has no ledger plate — it gets a second link on the console screen's row instead, off a generic companion field in build-index.mjs so the next system to ship one needs no code change.

Every timing on it was measured in the browser, not transcribed

VerbPrintedChromium
enter200 ms200 ms
exit160 ms160 ms
expand200 ms200 ms, on ::details-content
collapse160 ms160 ms
select120 ms120 ms
enter, shortened0.01 ms0.01 ms

The six dimension lines are drawn to scale, and the check is that each bar's width back-computes to its own printed label: at 1440 px the track is 375 px and the six bars return 120 / 200 / 320 / 90 / 160 / 240 exactly; at 375 px the track is 174 px and they return the same six numbers.

The four cross-system values came from the generators, never from recall:

carbon_tokens.py --motion      Carbon: 70/110/150/240/400/700 ms, six easings
primer_tokens.py --scales      Primer: enter 300 ms easeOut, exit 200 ms easeIn
md3_spring_css.py --table      M3 standard: spatial-default 317 ms, effects-default 231 ms
centauri_tokens.py --list       Centauri: three enter rungs, three exit rungs, three easings

The two values on the page that are not tokens

The last counter-case is this firm's own scroll reveal, and it is the only place the page leaves the token set. It runs 600 ms, longer than every rung here, and one half of it is linear, which check.mjs refuses inside a layer. Both are deliberate and both are read out of computingusa's shipped reveal.tsx (transition-opacity duration-600 ease-linear) and globals.css rather than invented: that specimen is a marketing surface's motion, not a console verb.

It earns its place because it is the one finding on the page that is a scar rather than doctrine. The firm shipped a moving entrance four times — 32, then 48, then 64 px of travel, at 1000, 800, 700 and 900 ms, on two curves — and every version was reported as arriving in place while measuring as running correctly. An element entering a page the reader is already scrolling competes with the scroll, which moves the same element further and faster in the same moment, and no duration or curve fixes that.

The demonstration proves its own claim: 60 ms into a 600 ms fade the eased pane measures 0.30 opacity and the linear pane 0.05, so the eased one is six times further along at the same instant. Both settle at 1.

The critique round, and what looking at it found

Six full-page renders (390 / 1440 / 1920, both schemes) after every source gate was already green. Seven defects, and only one of them is something a source gate reports:

  1. The ruler ran the full width of the document. At 1440 px the 320 ms line spanned the page and the set read as six rules rather than as a measurement. Bounded to 34 rem, and the two sets grouped under Arriving and Leaving labels instead of separated by colour — the accent is the product's one action per screen, and six exit bars is not that.
  2. The ladder rendered as a band of nothing. Its three panels sat at opacity: 0 until pressed, so the section read as a broken page. They are present now and the control replays the arrival.
  3. The stagger blocks were nearly invisible in both themes — the subject of the demonstration was a faint rule on a near-ground fill.
  4. The reveal specimen and the ladder were invisible at rest, for the same reason and with the same fix as (2) above — two labels over nothing.
  5. Neither replayed. Removing the attribute and putting it back inside two requestAnimationFrames replays no transition at all: the element transitions towards the rest state for about 30 ms and then back, so nothing visibly moves. Measured at 0 ms of travel. The start state has to be committed with the transition suppressed and a reflow forced — is-resetting, on for exactly one frame. The keyframe verbs (enter, exit) need the different remove-reflow-add form and already had it.
  6. The whole script was dead for one revision. The transition helper was named replay, which the keyframe demos already used, and SyntaxError: Identifier 'replay' has already been declared takes the entire <script> with it — every control on the page, silently. Every source gate stayed green. drive-probe.mjs is what catches this, and the lesson is that it has to run after the last change rather than once.
  7. The tab bar collided at 375 px. The brand kept its label (the rule said display: none, under a comment saying the opposite, and lost to the layer anyway) and Misapplied was cut over Four systems. Measured: a tab label gets 51 px at 375 and 42 px at 320, Misapplied needed 60, and the layer clips with text-overflow: clip so a word that does not fit loses a letter in silence. Fixed by the console screen's own icon-square brand rule, five one- and two-syllable labels, one step down the reading ladder below 360 px, and an ellipsis so the remaining 320 px overflow says so. Clean at 360, 375, 414 and 599.

Gates

audit_page.py      2 info (touch icon, manifest) — as every page here
slop_scan.py       2 warn — the string "scroll" in two event names, as index.html
craft_lint.py      2 warn (peers: two toggles, three tabs), 1 info (disabled styled by the layer)
motion_lint.py     clean          layout_lint.py   clean
type_lint.py       clean          ui_lint.py       clean
copy_lint.py       1 warning — "Parker County MUD 3", a proper noun
aria_lint.py       0 findings     unsafe_patterns.py clean
import_lint.py     clean          nav_lint.py      no findings
direction_check.py 1 warn — hero-undecided, which index.html reports identically;
                   the ledger reads literal values and this contract forbids writing any
screen_probe.py    clean — 275 focusable elements, 7 viewports, both themes,
                   reduced motion and forced colors
a11y-probe.mjs     clean at 375, 1280 and 1920 in both themes, control probe caught
drive-probe.mjs    27 controls driven; nothing thrown, emptied or focus-dropped
tools/check.sh centauri   PASS — every gate ran and every gate is clean

screen_probe.py found the one defect no source gate could: overflow: clip on the specimen stages cut 4 px off every focus ring inside them (SC 1.4.11), 108 findings across the viewports. The clip was bounding nothing — the enter and exit verbs travel inside the row, the rail's labels clip themselves — so it was removed rather than worked around. overflow-clip-margin was the first fix and is correct in the browser, but the probe compares rects and cannot see it, so a page relying on it would gate clean and read as unmeasured.

Three of the portfolio's own tools were hardcoded to index.html and so could measure no companion page at all. a11y-probe.mjs, drive-probe.mjs and review-shots.mjs each take --page <file> now; the default is unchanged.