Foundations

Architecture

Not what the system looks like. What it is made of, what may read what, and what it takes to add to it. This page is the first foundation and not the last, because a system is read starting from its rules. The audit, the inventory and the coverage map are evidence about the system and they live in Checks; mixed into one group at the bottom, the rules would hide behind the reporting.

Source: docs/architecture.md.

Two ladders, and both of them use the word level

Confusing them is the most expensive mistake available here, because the cure is a cascade problem rather than a typo.

The token ladder: where a value came from

semantic · why is this colour here

--bg-page, --text-secondary, --line-control. Every role points at a primitive through var(). Colour is read by a component only through here.

primitive · which value

--warm-950, --space-4, --size-md. Raw values with no role. Geometry, type and space are read by components directly.

Geometry gets no second rung, and not because it matters less. A radius and a height have nothing to override: a theme and a rebrand change colour, not the height of a control. A third rung would be a third layer of renaming with no flexibility bought, and changing the colour of a button would cost three files instead of one.

A component token is founded one at a time, never in bulk, and only where a state lands on no role at all. The hover of a card and the hover of a list row are one --bg-hover and need nothing of their own. The hover of a dangerous button has to darken from the danger colour rather than from the ground, and there a component token is earned.

This system founded none, and the reason is a fact about the product rather than a preference. The textbook case for a component token is the dangerous button, and this product has no dangerous control anywhere: the button carries three modifiers, --primary, --quiet and the collapsed --primary-narrow, and not one of them is red. Rejecting Clerk is a first class action and design principle 3 says so, so it is drawn as an ordinary action and never as a destruction. Every state in the system lands on a shared role, and the one value that needed a new step of a ramp got a primitive rather than a component token, because a pressed accent is the accent and not the button.

The component ladder: markup nesting

The rule is level = 1 + the highest level of what it contains.

organisms · contain molecules, or are the shell of a screen

shell, z1, z4, z5, rows, dialog, door. 19 of them.

molecules · contain atoms

row, frow, banner, field, opt, pane-head, toast. 24 of them.

atoms · contain nothing else from the system

btn, chip, state, key, bars, navitem, link, the form primitives. 19 of them.

patterns · stage 09, and the criterion changes

A composition that repeated on three or more screens. The case pane and the fleet are both patterns of Z5, not variants of it.

The third level is the ceiling and the formula stops there. The sign-in dialog contains a form and a form is an organism too, so the formula would produce a fourth level that does not exist. Everything that contains an organism stays an organism, and nesting inside the level controls only the order: first the organisms that contain no other organism, then the ones that do.

Grouping by purpose is forbidden at the top level

By purpose a button and a sign-in dialog are both "forms", and they would sit side by side while one lives inside the other. Purpose is a secondary sort inside a level, and only once a level holds more than about eight. The price is paid three times when this is broken, and none of the three is cosmetic.

What a component may read

Kind of valueRead fromWhy
coloursemantic onlya theme and a rebrand override colour, and they do it by redefining roles. A component that reads a colour primitive is a hole the first theme finds
radius, spacing, sizeprimitive directlythere is nothing to override
a statea state token, in both themes:hover reads --bg-hover and --line-hover, :active reads --bg-active, :focus-visible reads --color-focus through base.css, out of reach reads --opacity-disabled. Five, not four: a bordered control here hovers by raising its boundary as well as its ground, in two rules that predate this stage

No new hex and no new number appears in a component file. A state written as a value is the same hole as a component reading a colour primitive, and it is paid for three times: in the theme, where it becomes forty edits instead of three lines; in consistency, where twenty components each get a hover of their own; and at stage 11, which would have to start by collecting the states back together.

How a role is named

Read out of the audit, never taken from another system. --color-primary and --surface-2 are somebody else's names. A role exists only where docs/tokens-audit.md shows it standing on screens, and its comment carries how many places it was read from. An empty role is not a reserve for the future; it is noise that real roles drown in.

Three axes decide whether a role gets a token, and they are three different questions.

The third axis catches the quietest colour defect there is. A fill role dropped onto a small bold label passes as a surface and fails as text, and a table of text and background pairs cannot see it, because nobody declared it as text. The cost of getting it wrong is not cosmetic either: if the text role exists, the cure is one word in a declaration; if it does not, the cure is the value, and the band moves along with the label.

The theme pair is a property of the level, not an event

Nothing on this stand is a picture of the product. A component page shows the component by linking the same index.css a screen links, and a state is produced by the reader rather than photographed for them: a bench of real controls in both themes at once, and a readout that asks the browser what each state resolves to. The alternative was a screenshot per state, and the first one taken here documented a focus ring that was not in it. A picture is a second copy, and a second copy is where drift starts.

Every role is written twice at the moment it is founded, once in :root and once in [data-theme="light"]. A role without a pair does not exist, and neither does a state without one: in the second theme the focus ring disappears, the product stops being keyboard passable, and the only way to notice is to switch themes, which is to say never.

The pair is not a mirror. Contrast is computed against the opposite ground, so the light theme takes a different step of the same ramp rather than the same primitive. Where a value genuinely clears its threshold from both directions it stays, and both measurements sit beside it so it can be seen that this was measured rather than skipped.

:root carries the dark theme, because that is what the product ships with nothing stored. The light theme is the pair, and since 2026-08-27 it is also a feature: the second control in the top bar reaches it, design/system/theme.js puts it on the root element before the first frame, and the stand and the product remember the same answer under one key. The pair stopped being a proof and became a surface, which is why the product now has a contrast sweep in light that renders light.

Two folders, and one of them can leave

design/ ├── system/ the CODE. This whole folder can be lifted │ ├── tokens.css into another project and it works there. │ ├── base.css │ ├── index.css the single entry point, @import BY LEVEL │ └── components/<name>.css └── kit/ the SHOWCASE. It stays here. ├── docs/*.md the sources ├── overview.html the hub ├── <foundation>.html architecture, colour, typography, geometry, icons ├── <component>.html one page per component ├── _nav.js _page.css the stand's registry and its own furniture └── kit.html shell.html frozen, from stage 07

A product class inside the showcase, or a showcase style inside a component file, is a defect either way.

Adding a component is five things

And the last two are the ones that get skipped.

  1. design/system/components/<name>.css
  2. design/kit/<name>.html, with its five blocks
  3. an entry in design/kit/_nav.js, in the group of its own level
  4. a row in design/kit/docs/inventory.md, with its level
  5. an @import in design/system/index.css, into the group of its own level and not at the end of the file

The card on the hub is not a sixth thing: the registry renders it from the same entry. Four and five matter most for a component added after the system is built, which is every component added by a reconciliation, by a new screen, or by the rollout. Appended at the end of the file it looks harmless, and that is exactly how the ladder falls apart.

Where a fix lives

What changedWhere it goes
a colour that carries a rolethe semantic level of tokens.css
a raw valuethe primitive level of tokens.css
how a component looksthat component's file
markupthe component's page, and every screen it stands on

A fix made on one screen is a desync, not a fix. A value approved on a coloured screen is never carried anywhere by hand: it goes into a variable, where it reaches every screen by itself, and then it is checked in a browser that it arrived on the second screen and on every repeat of the component.

Patterns, and why there are four

A pattern is a composition that already stands on three or more screens, and the counter runs on wireframes/: colour holds 52 pages of 62 until stage 12, so three occurrences there would be a statement about the sample wearing the name of a rule.

the definition this product produced

A pattern here is a FILLING. One container component, filled with a set of zones, where the container's other filling drops zones and grows different ones. The anatomy rule of stage 08 says a zone that disappears means a different thing rather than a variant; applied one level up, that rule produces exactly four compositions and no others.

PatternHostIts zonesGreyColour
queue-listz4scopebar, readout, banner, rows, qfoot3829
shift-briefz4readout, banner, brief, qfoot77
case-panez5pane-head, pane-body, pane-foot3829
fleetz5pane-head, frow, fleet-more1010

The two hosts have three other fillings between them and none is a pattern, because each is a single zone with its own class: frame on the five entry screens, outage on the three system states, and door on the five sign in states, which fills the shell rather than either half of the split. A filling with one wrapper is a component, and the wrapper is its name.

A pattern owns no paint and writes no new rule. Every declaration in design/system/patterns/ was cut from a component file, and each file names where its rules came from. What made a rule a candidate was mechanical rather than a judgement: a selector, written inside one component, that names another component and is conditioned on which filling the host is carrying. Fourteen rules matched, in three of them. .z4 > .banner did not: it places the banner whichever filling the column carries, so it belongs to the zone.

One rule was doing two jobs and neither was named. .z5{ display:none } at 900 hid the fleet at rest and the desk-only case pane of the log and shift screens, on one line, for two unrelated reasons. It is two rules now, one in each pattern, and 102 renderings say the result is identical.

Usage rules

Three classes of prohibition, and only the first has a home on a component page: substitution ("this needs a tag rather than a badge") lives as the rule and antirule block there and is not repeated here. Composition and context cannot be written as an antirule at all, because no other component is the right one to take. The component is correct and its count or its neighbour is not.

The Components column names patterns as well as components, because a rule about a count is most often a rule about the filling that does the counting.

Every rule below is also a function. design/kit/checks/rules.mjs is these thirteen in this order, measured at 1440 and at 360, on what renders rather than on what is in the markup. A prohibition written only in prose is a prohibition nobody runs, and three of these are true at one width and false at the other. The twelfth arrived at stage 12 and the thirteenth at stage 13, each because a rule had been written as a sentence in a stylesheet and had paid none of what section 11 says a new rule costs.

#The ruleClassWhere it came fromComponents
R1Not more than one .btn--primary per layer, and a foot does not compete with it. A second is allowed only as the viewport twin of the first, or in a modal layer over the screencompositionconventions.md, "exactly one primary action per screen", corrected by the counterbtn, dialog, scrim, pane-foot, qfoot, case-pane, queue-list, shift-brief
R2A dialog never stands without a scrim, and one of each at a timecompositioncounter: 11 dialogs on 11 screens, every one inside a scrimdialog, scrim
R3No overlay covers the detail panecontextdocs/decisions.md, node 4.4, design principle 5scrim, dialog, z6, toast, z5, z45, case-pane, fleet
R4Three notices at the desk, one at 360, and a failure takes the single slotcompositionconventions.md measured 408 of 760 pixels at 360; the counter agreesz6, toast
R5No shell before authentication, and no annunciator on a console that cannot read Clerkcontextcounter: 5 screens with no z1, two more with z1--out and no z2z1, z2, z45, navitem, annun
R6One readout per screencompositioncounter: 45 screens, maximum of one on every onereadout, queue-list, shift-brief
R7The detail pane never renders an emptycontextCLAUDE.md: the pane at rest must read as the fleet, not as emptyempty, z5, fleet, case-pane
R8A .only-desk element never renders at 360, a .only-narrow never at the deskcontextwireframes/docs/critique.md: 22 pages showed the 360 block at 1440btn, banner, hint, cons, prov, scrim, shift-brief
R9One selected row at a time, and none at 360composition, contextcounter: 23 screens, maximum of one; zero at 360 by node 3.1row, rows, queue-list
R10The fleet has no route and no navigation itemcontextdocs/decisions.md: a route would make the differentiator a place you gofleet, navitem, z5
R11One h1 per screencompositionvoice/docs/critique.md: two node specifications disagree on 19 screensreadout, pane-head, case-pane
R12An optlist holds one filling or the other, never both inside the same bordercompositionstage 12: optlist gained a second filling, keyrow, when node 0.5 was built. An opt is a link you answer and a keyrow is a row you readoptlist, opt, keyrow
R13An expansion that has a head is a details, and no head holds a linkcompositionstage 13: 21 screens carried div.expand with a chevron on it and no control behind it, while this stand described details.expand. The second half is why the handover note keeps no head: its head links to the case, and a link inside a summary cannot be reachedexpand, link

R11 is the one the corpus breaks, and it breaks it at one width only. On 16 coloured screens and 21 grey ones there was no h1 at all at 360: the heading is the queue readout, the readout lives in the list column, and at 360 a case screen hides that column. Stage 05 found the contradiction underneath it and could not see the consequence, because the consequence is a computed style at one viewport. The coloured half closed at stage 12 and node design/kit/checks/rules.mjs design prints 0 broken. The grey 21 stand, and the bare node design/kit/checks/rules.mjs still prints them, because wireframes/ is frozen: that number is the measured lag of a frozen corpus rather than a defect list. The screens are named in the backlog, and the lag itself is measured by design/kit/checks/diverge.mjs.

What a NEW screen does about it, because "open" is not an instruction: ship the same structure the sixteen have, add the screen to the backlog row, and do not invent a local cure. A heading promoted on one screen and nowhere else is the desync this system is built to prevent, and the fix is one decision about the document outline of the whole product rather than sixteen decisions plus yours.

R3 was broken on four grey screens, and building one of them in colour answered it. The escalate family draws a full width dialog at 360 over a pane still rendered behind it, where reject drops its scrim at that width and escalate cannot: escalating from a phone is the product's one mobile scenario. Escalate was the screen this stage built out of the system, the rule caught it on the first run, and the fix is one line in z45.css. The three grey states that remain uncoloured still report it, and they close by being built.

Contributing to the system

New appears in design/system/ first and on the screen second. Never the other way round.

That is the whole rule. Without it, styles are growing on screens again within a week and the system is another name for a folder somebody once tidied. Below is the address for each of the four kinds of new thing.

A new value

design/system/tokens.css, at its own level. A colour that carries a role goes in the semantic level with an origin comment; a raw value goes in the primitive level. A token of state is written in both themes at once or it does not exist: the pair is a property of the level rather than an event, and a role declared in one theme looks flawless in that theme and loses a focus ring in the other.

A new component, and it is five things

  1. design/system/components/<name>.css, with four states in both themes if it is interactive, and a header naming what it reads
  2. design/kit/<name>.html, with all five blocks: anatomy, variants and sizes, when to use it, rule and anti-rule, states
  3. an entry in design/kit/_nav.js in the group of its own level
  4. a row in docs/inventory.md with its level
  5. an @import in index.css in its own level group, not at the end of the file

Three and five are the two that get skipped. The system is already assembled and appending a file at the bottom looks harmless, and it is exactly how the ladder comes apart: an atom written at stage 12 would sit below every organism in the cascade, and the first contextual conflict gets cured with !important.

A new composition

Count it on wireframes/, where the whole product is. Three screens or more: a file in design/system/patterns/, a page in design/kit/, an entry in the Patterns group of the registry, an @import after the components, and a row in the inventory. The same five things one rung higher. Two screens: it stays markup and goes into the candidates table on Patterns, so the next round finds a list instead of starting the count again.

The page carries five blocks like a component's page, and the fifth is different: anatomy, variants, when to use it, rule and anti-rule, and where it stands, three or more screens named with links. There is no states block, and there should not be: every component in the composition carries its own states on its own page, so a states block here would either be a second copy or an invented state for the composition, which is the same defect as a pattern with its own paint.

A pattern that needs a declaration none of its parts has is not a pattern asking to be born. It is a component or a variant missing, and the component comes first.

A new usage rule

A row in the table above, with the source column filled in: counted on the screens, decided at an earlier stage, or caught by a critique. A rule with an empty source is an invention wearing the word rule. Then a Limits subsection on the page of every component the rule names, linking back here, and a function in design/kit/checks/rules.mjs. One author, three visible places, and the last one is what makes it run rather than be read.

A new rule about width

Four homes, and the fifth is forbidden. A value shared by more than one thing: tokens.css, at the primitive level, in rem. How one component behaves in its own place: its own file, through @container, with the local threshold listed in responsive.md. How a composition behaves: its pattern file, one query that reaches every screen the pattern stands on. How the shell behaves: the zone files, through @media, because the shell is the only thing that measures the viewport. In the file of a screen: never.

That last one is the rule with the highest price in this system, and it is not paid here. It is paid at stage 12, where twenty screens are built at once: twenty authors without this rule grow twenty media queries, and the adaptation of the product goes back to being scattered across twenty files, exactly as the inline CSS of stage 04 was.

Read the ladder top down: fluid, then container, then a point. A point is written only where the fluid answer physically cannot work, and the reason goes into the audit table. "It was easier to write" is not a reason. There is one point in this product, --bp-split-panes, and a second is a decision taken deliberately and written down, never a side effect. @media cannot read var(), so the literal stands in the query and the token is the register: no other number may appear in a query anywhere in the system. And a font-size is never switched at a point: type is fluid through clamp() with a rem addend, because a pure vw middle term takes the page out of the reader's zoom and fails WCAG 1.4.4.

A new place

design/system/places.css, and it is the fifth kind of thing rather than a fourth kind of component. The test is one question: does the rule say something about a THING, or about a GAP between things? A gap, an order or a width is a place. It gets no page, no registry entry and no inventory row, because it is not a thing anybody can be told to reach for: it is where a thing stands. The file itself carries the reason for every entry in it. A place that carries a colour, a line or a family is not a place, and stage 09 learned that by trying to move two of them into a pattern.

A new screen of the product

Flat in design/, beside the others, named after its node. It links system/index.css and nothing else, it carries the design panel through design/_nav.js, its shell comes from design/_shell.js, and it is registered in design/_nav.js so it appears on the coverage map of the screens page. There is no folder of examples: a screen assembled out of the system is a screen of the product, and the stages after this one adapt it, animate it and hand it over with the rest.

Before it is accepted, run node design/kit/checks/rules.mjs design <screen>.html and answer every rule by name. A rule that the screen breaks is fixed on the screen, because that is an assembly error rather than a gap in the system, and a rule that turns out to be wrong or too narrow comes back to the table above with a correction and a reason.

On a screen, with none of the above

Forbidden. A screen carries no style of its own, and it does not carry an inline declaration either. If a screen needs something the system does not have, the something is an order for the system. Three inline declarations survived stage 08 on three coloured screens; one of them found an existing home at stage 09 and the other two are rows in the backlog.