Twenty-three design systems, twenty-three products. One page each. A reader opening two of them back to back must see two different products, not one template wearing two palettes — and must want to build in whichever one they just looked at.
Everything below is binding. Where it conflicts with taste, it wins; where it conflicts with the system's own published rules, the system wins and the deviation goes in the page's spec panel with its reason.
1. Where the values come from
systems/<slug>/tokens.css was emitted by that system's own verified generator (the command is recorded in systems/<slug>/BRIEF.md). It is generated output.
- Never hand-edit
tokens.css. If a value is wrong, the generator flag was wrong; say so rather than patching the file. - Never write a raw colour in the page. Every colour, and every radius, duration and easing the system tokenizes, is a
var(--…)from that file. Spacing and type may be authored where the system does not tokenize them, but must follow that system's published scale (GOV.UK is 5px-based, not 8px). - A token the page needs that the generator did not emit is a signal to run the generator again with another flag, not to invent the token.
2. One file, no build
systems/<slug>/index.html is a single self-contained document:
<link rel="stylesheet" href="tokens.css"> <!-- generated, first -->
<style> … the page's own composition … </style>
<script> … only where behaviour is the point … </script>
No bundler, no framework, no npm. Only two kinds of external resource are permitted: https://fonts.googleapis.com / fonts.gstatic.com for a typeface the system actually specifies, and inline SVG for every icon. No icon fonts, no image CDNs, no remote scripts. Every <img> is either an inline SVG or a data: URI, and carries explicit width and height.
3. The page has three parts, in this order
a. The product. A real screen from a real product, at real density, with real content. Not a component gallery. Not lorem. Someone should be able to believe a team ships this on Monday.
b. A second state, in the same page. Whatever this screen's hard state is — the empty table, the destructive confirmation, the validation error, the offline banner, the loading skeleton, the 40-character customer name that breaks the column. Pick the one the archetype makes hardest and show it honestly. This is the part that separates a portfolio from a screenshot.
c. The design-notes panel, last, styled in the system's own tokens: the four direction lines (shape of the page / what it leads with / spacing / the one bold decision), where the colour comes from, the measured contrast of the two pairs the page leans on hardest, and any deviation from §1 with its reason. Two sentences of prose maximum per line. This panel is the only place on the page where the subject is the design system itself.
It is written for the reader, not for whoever built it. No generator command, no flag, no file name, no token name, no WCAG clause number. A visitor to this portfolio is deciding whether to hire the people who made it, and a shell command in a panel headed Design notes is the writer never having changed audience from themselves. The command that produced tokens.css is recorded in systems/<slug>/BRIEF.md, which is where the build record belongs; copy_lint.py's operator-copy rule is what holds this.
Back to the index: one link, top-left or in the system's own nav idiom, reading Portfolio. Styled in-system. href="../../index.html".
4. What is forbidden on every page
These are the tells that make generated UI unmistakable, and they are what slop_scan.py and craft_lint.py are run to catch:
- A card around everything, and a stroke at every seam (
references/restraint.md). - Three or more controls in a row all forced to one width, so nothing is primary.
- A hover with no
:active, a click target with nocursor,outline: nonewith no ring put back, a disabled control with no disabled styling. - A native
title=tooltip; an unstyled<select>chevron; a raw<input type="date">whose picker is the browser's. - A default chart palette from any charting library. Series colour comes from the system's own chart tokens where it has them, and from
scripts/series.pywhere it does not. - Emoji as iconography. Gradients used as decoration rather than as meaning.
- Copy that explains the design ("we chose a flat list rather than a tree"), copy addressed to an operator (
localhost, "restart the server",TODO), or a rationale sentence beginning "because".
5. What every page must satisfy
- WCAG 2.2 AA. Text contrast measured, not assumed. Target size judged with the spacing exemption. Every interactive element reachable and visibly focused by keyboard. Headings in order, one
<h1>. Every control labelled. A table has a<caption>. - Both themes where the system publishes both — light and dark defined as tokens on
:root, redefined under@media (prefers-color-scheme: dark)guarded as:root:not([data-theme="light"]), and again under:root[data-theme="dark"]so an in-page toggle wins in both directions. A system that ships light only (Polaris) says so in the spec panel instead. - 375px to 1920px, no horizontal scroll at any width. Every grid child that can hold stored text carries
min-w-0's equivalent (min-width: 0), and anything rendering a name, URL or title carriesoverflow-wrap: anywhere. - Motion: one animation per verb, durations and easings from the system's own motion tokens, every exit under 250ms, nothing animating a value, and a
prefers-reduced-motion: reducepath that shortens rather than deletes (a baretransition: nonestrandstransitionend). lang, a real<title>, a<meta name="viewport">that does not block zoom, andfont-display: swapon any web font.
6. The gates, before the page is called done
Run all of these from the page's own directory and paste the output into your report. S=scripts.
$S/audit_page.py systems/<slug>/ # built-HTML defects
$S/slop_scan.py systems/<slug>/ # did anyone choose anything
$S/craft_lint.py systems/<slug>/ # did anyone decide anything
$S/motion_lint.py systems/<slug>/ # does the motion run
$S/layout_lint.py systems/<slug>/ # boxes in the right places
$S/copy_lint.py systems/<slug>/ # the words
$S/direction_check.py --ledger systems/<slug>/
$S/contrast.py "<fg>" "<bg>" --size <px> # the two pairs the page leans on
aria_lint.py systems/<slug>/
unsafe_patterns.py systems/<slug>/
Every page here builds markup in script, and CWE-79 is the one defect class a design gate would otherwise never look for. Nothing in this portfolio takes untrusted input, so a finding is about the code being exemplary rather than about a hole — which is the point, since a portfolio is code people copy.
Then the two measurements the source cannot make, both of which found real defects on pages whose source gates were already clean:
node tools/shots.mjs <slug> # 1440px
node tools/shots.mjs --width 375 <slug> # and 375px
node tools/a11y-probe.mjs <slug> # computed contrast and target size,
# both themes, in a real browser
Plus the system's own lint where it ships one: hig_lint.py, 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 + nav_lint.py.
Clear every error and every genuine warning. A warning you are deliberately keeping is named in your report with the reason. A gate that could not run is a failure, not a pass — say so.
7. Report back
- The four direction lines you chose and why this product suits this system.
- The generator command, verbatim, in
BRIEF.mdand not on the page. - Each gate's final output, and what you fixed to get there.
- Any deviation from this contract, with its reason.
- The one thing you are least sure about.