A design system portfolio. One flagship screen in each of twenty-three systems, built so that a reader opening two of them back to back sees two different products — not one template wearing two palettes.
Open index.html. No build, no dependencies, no server needed.
What makes it different from a component gallery
Every colour is generated. Each systems/<slug>/tokens.css is the output of that system's own verified generator, run against the published package or specification the system ships — md3_palette.py against material-color-utilities, carbon_tokens.py against @carbon/themes, primer_tokens.py against @primer/primitives, and so on. Not one hex on any page was written from memory. The command that produced each file is recorded in that page's BRIEF.md and printed in the page's own spec panel.
Every page decided what it looks like before it was composed. Four lines, written first: the layout archetype, the data stance, the density, and the one place the boldness is spent. the 29 briefs holds them all. No two pages share both an archetype and a stance, which is what stops twenty-three correct screens from being one screen twenty-three times.
Every page shows its hard state. The empty table, the destructive confirmation, the validation error, the 40-character name that breaks a column. A screen that only ever renders the happy path is a screenshot, not a design.
No two pages are the same product. The logo-swap test measures a structural signature of every pair — 231 of them — and reports how similar two pages are. The mean is 0.34 and the closest pair sits exactly on the 0.75 ceiling, with a caveat that belongs next to the number: three of the six dimensions read literal values out of the source, this contract forbids writing any, and direction_check.py:548 scores two empty sets as a perfect match. So those three dimensions return 1.00 between any two compliant pages and the real working range is the other three. The test still earned its place — the one pair it flagged at 0.83 turned out to have a genuine unspent lever, and fixing it improved the page — but the mean is weaker evidence here than it would be over pages that hard-code their values.
Every page was measured. Contrast per pair in every theme the system publishes, target size under the SC 2.5.8 spacing exemption, one animation per verb with a reduced-motion path that shortens rather than deletes, and no horizontal scroll from 375px to 1920px.
The critique round
Passing the gate is the floor. the critique round is the round that asks the question no linter asks — is it any good to use — against interface-critique.md's rubric: the eight-item cognitive-load checklist, Nielsen's ten heuristics scored 0–4, and every finding severity-rated and sorted. Each page was reviewed by somebody who had not built it, working from six full-page renders (tools/review-shots.mjs) at 390px, 1440px and 1920px in both schemes — because until that round, everything past the first fold had only ever been read as source.
The ledger itself was reviewed the same way and did not clear four of the eight cognitive-load items: chunking, grouping, minimal choices and progressive disclosure. A flat list of equally weighted rows is that many simultaneous choices at one decision point, against a working-memory limit of about four (Cowan 2001), and the rubric says that finding outranks everything else in a review. So the screens are now grouped into five families named by who is at the screen — operators, teams at work, buyers and merchants, citizens, an audience — none holding more than six, with a contents page in front of them and each family's head as a sticky spine so the reader always knows which group they are in.
Layout
index.html the ledger — every screen, in five families
docs/ every document here, rendered and readable in a browser
index.html this file
contract.html the build contract
briefs.html every brief
critique.html the critique round
brief-<slug>.html each page's own record, x20
CONTRACT.md binding for every page
BRIEFS.md every brief, four direction lines each
CRITIQUE.md the round that asked whether the pages are any good
assets/theme.css the ledger's own palette (generated; see build-theme.sh)
assets/docs.css the reading surface for the documents
assets/accents.css each row's rule, resolved out of that system's tokens
assets/shots/ the ledger plates, one per appearance per page
assets/review/ full-page renders, 390/1440/1920 x light/dark
systems/<slug>/
index.html the screen
motion.html centauri only — the movement layer, live (see its BRIEF)
field.html centauri only — the same spine under the field profile
catalog.html centauri only — the same spine under the catalog profile
marketing.html centauri only — the same spine under the marketing profile
tokens.css GENERATED — never hand-edited
BRIEF.md the direction lines, the generator command, the measurements
copy-lint.ignore rules this page deliberately keeps, each with its reason
tools/check.sh the gate
tools/build-index.mjs the ledger and the accent map
tools/build-docs.mjs the documents · tools/md.mjs the renderer behind them
tools/shots.mjs the ledger plates, and the sideways-scroll check
tools/review-shots.mjs the six full-page renders per page
tools/a11y-probe.mjs computed contrast, target size, rendered SVG text
(--page <file> for a companion document beside a screen)
tools/state-probe.mjs hover and focus-visible, resolved in a browser
tools/drive-probe.mjs every control activated, watching for what breaks
tools/distinctness.sh the logo-swap test over every pair
tools/distinct-one.sh the same test scoped to one new page, 22 pairs not 253
Nothing here needs a build or a server. docs/ is generated from the Markdown beside it — node tools/build-docs.mjs, and --check fails if it is stale.
Running the gate
tools/check.sh # everything
tools/check.sh carbon-design uswds # two pages
It runs over every screen, the ledger and the documents beside them: audit_page.py, slop_scan.py, craft_lint.py, motion_lint.py, layout_lint.py, copy_lint.py, aria_lint.py and unsafe_patterns.py, plus that system's own lint where one applies (carbon_lint.py, bootstrap_lint.py, uswds_lint.py, enhancement_lint.py, lwc_lint.py, cloudscape_lint.py, ads_lint.py, catalog_lint.py, and for Centauri import_lint.py and nav_lint.py). A check that could not run is reported as an error and fails the gate; a missing script is not a pass.
A page may keep a copy_lint.py rule it deliberately disagrees with — the copy linter is heuristic, and every finding is a prompt to look rather than a verdict. The exception has to be written down: systems/<slug>/copy-lint.ignore holds one rule id per line, each with a # comment saying why, and tools/check.sh passes them to --ignore. An exception nobody wrote down is not an exception.
hig_lint.py is the one system lint that does not run here. It reads .swift files — its subject is a colour built from component literals in a Swift initializer — and the Apple HIG page is HTML. Running it would only ever report "nothing was read", which is a gate that cannot fail rather than a gate that passes.
It then measures the two things the source cannot: tools/shots.mjs opens each page at 1440px and 375px and fails on a document that scrolls sideways or an element that escapes its container, and tools/a11y-probe.mjs reads the computed colour and box of every element in both themes. That second one paints each colour into a canvas and reads the pixel rather than parsing what getComputedStyle serialized — a /[\d.]+/ over oklch(0.973 0.009 345) reads it as an RGB triple, which is how a checker invents a wall of failures on a page whose only crime is a modern colour space. It injects a deliberately failing control into every page and fails the run if it does not catch it, because a green from a checker that cannot fail is worth nothing.
Run over the whole set with no arguments, it finishes with the logo-swap test: direction_check.py --compare over all 231 pairs of pages (tools/distinctness.sh). That is the trap a portfolio this size is most exposed to, because every page was built to one contract — two pages scoring over 0.75 would be the same product with different logos.
It runs offline, on this machine. There is no CI configuration here and none is wanted.
Regenerating
assets/build-theme.sh # the index palette, and its 96/96 proof
node tools/build-index.mjs # the ledger rows and the accent map
A token file is regenerated by re-running its generator with a different flag or seed, never by editing the file. The generator commands are in each page's BRIEF.md.
What sweep.py reports that this gate does not
sweep.py . runs the whole skill library's detectors over the tree and reports more than tools/check.sh does. It found one real gap — unsafe_patterns.py was ungated, and its 24 xss-sink findings are now a gate. Everything else it reports was checked and is not a defect:
copy-lint4 errors. These are exactly the exceptions written down insystems/*/copy-lint.ignore.sweep.pydoes not read those files;tools/check.shdoes.SRC-INVISIBLE-CHARACTER, 142. All U+202F, the narrow no-break space, used deliberately between a figure and its unit and inside European thousands-grouped numbers. Correct typesetting, not a homoglyph.z-index-on-static, 17. Every one is insidesystems/bootstrap-5/bootstrap-5.3.8.min.css, which is the framework vendored verbatim and may not be edited.input-unlabelled, 4. The Apple HIG text-size radios, each wrapped in its own<label>inside a<fieldset>/<legend>. Implicit labelling, which the rule does not detect andaria_lint.pyconfirms is correct.shouty-caps/title-case/jargon, the bulk of the warnings. Mostlycopy_lint.pyreading a whole<tr>as one string, so country codes, Incoterms and column headers concatenate into what looks like a caps block.unsafe-patternseval, 2 errors intools/state-probe.mjs. Both are Playwright'spage.$$evalelement-handle API, which takes a function and evaluates no string at all. The rule matches those four letters followed by an opening parenthesis, which that method name happens to end with. Verified by reading both call sites; there is no dynamic evaluation in this repository.aria-lintH001, 14 warnings on the stone catalog. Redundant table roles written into the markup so the semantics survive adisplaychange at narrow widths. Correct under 48rem and redundant over it — being fixed the way the Polaris page solved it, with the roles restated only while the stacked layout applies.
Two deliberate lint warnings on the index
copy_lint.py flags Atlassian Design System, GOV.UK Design System and SAP Fiori (Horizon) as title case. They are the systems' own registered names; a proper noun keeps its capitals. It also flags the full stop in the headline Twenty-three systems. Twenty-three products. — that is two sentences used as a headline, and the stop is doing the work.