Geometry
Four radii, two control sizes, one elevation, and an 8px spacing grid with 4, 2 and 0 under it. The grid was adopted at the founder's review of the foundations on 2026-08-11 and it replaces 27 loose values that had no variable behind any of them. Geometry gets no semantic level. A radius and a spacing have nothing to override: no theme and no rebrand moves them, they simply repeat. So a component reads them straight from primitive, and colour stays the only thing in this system that carries a role.
Why there is no role level here
The comment above the geometry block in design/system/tokens.css states the rule in the file itself: “GEOMETRY gets no semantic level. A radius and a spacing have nothing to override: no theme and no rebrand moves them, they simply repeat. Components read them straight from primitive.”
The test is whether a value could ever need to point somewhere else. --bg-action and --text-action hold the same petrol today and part company the moment the theme changes, so each one earns a role. A 14px corner is 14px in both themes and on every rebrand of this product. Wrapping it in --radius-container: var(--radius-14) would add a level of indirection that nothing will ever use, and a level nobody uses is a level nobody keeps correct.
Until 2026-08-11 one half of this page was named and the other was not: the radii and the two sizes were tokens and the spacing was 27 literals. The spacing is now eleven primitives beside them, and it did not gain a role level on the way in. Naming is not the same as adding a level, and the whole of geometry still sits at primitive, where a component reads it directly.
Radii, shown as themselves
Four values, four names, and each square below actually carries the corner it is labelled with. Three of them sit on a 4px step (6, 10, 14). The fourth, the 12px wash, sits off that step because it was measured off one object rather than picked off a ladder.
The 8px grid does not apply here, and nobody should fold these into it later. This is a corner scale, not a spacing scale: a radius answers to the size of the object it wraps, and rounding 14 to 16 or 6 to 8 would change how a card and a chip read while fixing nothing. The founder's review named the radii among the things it does not touch.
Where each one came from, in its own words
| Token | Value | Origin, quoted from tokens.css | Consumers | The objects |
|---|---|---|---|---|
--radius |
14px | “containers: the list, cards, the sheet” | 7 | .door, .plan-opt, .card in both of its forms, .sharecard, .panel, .sheet, the 52px hero logo |
--radius-wash |
12px | “origin: the alert banner. Washes and callouts” | 1 | .wash, one rule serving all four tones. It was two rules, .notice and .attention, until the fold |
--radius-sm |
10px | “controls: buttons, fields, rows, tiles” | 10 | .btn, .input, .select and its picker surface, .tile, .range button, .cand .btn, .logo, .chart, .rgroup li |
--radius-xs |
6px | “origin: the chip and the skeleton, written inline twice in kit.css before it was named” | 4 | .chip, .skel, the select's option rows, the 22px logo inside a radio row |
The step 1 audit recorded 8 / 9 / 3 / 2 in docs/tokens-audit.md, and the counts above are re-measured in design/system/components/ after step 6: 7 / 10 / 4 / 1. Not one radius moved. What moved is how many rules ask for each: the card's two names became one, the two washes became one, and the select's picker surface and option rows arrived with two rules of their own. Note what the list does not contain: a list row has no radius of its own. Rows stack inside one 14px container and the first and last inherit its corners, so a list reads as one object instead of a stack of cards.
Carried, not fixed here. The Shapes section of the root DESIGN.md still says “Three radii and no more”. The token block higher up the same file already lists rounded.wash: 12px, and the system carries four. The prose is one decision behind; correcting it belongs to DESIGN.md, not to this page. The 12px itself does not move: it was named by a decision on 2026-08-10, and a value changes only by another one.
Control sizes, at true size
Two values, both tokens, both drawn below at the size they actually are, and neither of them moved. --tap: 44px is “the floor a pointer target may not go under”. --row: 56px is “the list row, comfortably over the floor”. The 12px gap between them is not decoration: it is what a finger gets on a row that also has to carry a logo, a name, an amount and a date.
44 is a WCAG floor and it is not ours to round. 56 happens to be a multiple of eight and 44 does not, and that is not an argument for touching it: the number comes from WCAG 2.5.5 rather than from this product's rhythm, and a grid that reached it would be a grid overruling an accessibility minimum. The founder's review named both sizes among the things it does not change.
kit.css wrote min-height: 44px twelve times and min-height: 56px four times. The four are the objects a person actually aims at down a list: .row, .navrow, .tile and a tab in the tab bar. On the desktop rail the tab drops to 48px, which is under the row and still clear of the floor.
The floor has one exemption, and it was found by breaking something. The 44px was first put on the base link atom, which turned every link inside a sentence into a 44px inline-flex box: the comment at kit.css line 227 recorded a four-line paragraph on the trends screen measuring 100px instead of 75, with the lines drifting apart. The paragraphs carrying inline links are the privacy and trust sentences, so the copy whose only job is to reassure was the copy that looked broken. A link inside a block of text is exempt, precisely because enforcing the minimum deforms the text it lives in. Every standalone control in the product is a button, a row or a tab, and all of those clear 44px measured.
The one elevation
--shadow is the only elevation in the system and it has exactly one consumer: .app .sheet, which was kit.css line 978 and is now dialog-sheet.css. Even there it does not work alone, it arrives with a background change, a hairline and a 14px radius. The rule it obeys is named in DESIGN.md: “The Flat Paper Rule. A new surface earns a hairline and a background change, never a shadow. The moment two elevations exist inside one screen, the eye starts ranking them, and ranking is the work this product takes away from the person.”
Carried, not fixed here. The Shadow Vocabulary in DESIGN.md names the consumer as “the mobile app frame only”. That frame was deleted on 2026-08-05 and the block that styled it went at stage 07 step 5 (kit.css line 1027). tokens.css already says the dialog sheet alone, which is what the code does. The DESIGN.md line is stale.
Spacing, an 8px grid
Eleven steps, and every one of them a primitive in tokens.css. The grid is 8, with 4 and 2 below it for the places where 8 is too much, and 0 as a real value rather than the absence of a value. Above 32 the steps stay multiples of 8, because that is where the page frame lives. Each bar below is drawn at the width of its own value.
The first row has no bar, and that is the point: --space-0 is a value a component can ask for, not a gap somebody forgot to write. The rest carry the purpose tokens.css gives them next to the value: --space-2 is “a hair: the gap under a row's second line”, --space-8 is “the unit”, --space-16 is “the screen's side padding, the common block gap”, --space-24 is “between blocks” and --space-32 is “between sections”. The four above 32 carry no note, because above the section gap a number is page frame rather than rhythm.
Every old value, and what it becomes
The rule, and it was applied mechanically: every old value goes to the nearest step, and a tie goes to the larger. Nobody decided value by value, which is the same rule the type scale took, so the two halves of the foundations were folded the same way rather than each to its own taste.
| Was | Becomes | Move | Token |
|---|---|---|---|
| 0 | 0 | none | --space-0 |
| 1px | 2px | +1px | --space-2 |
| 2px | 2px | none | --space-2 |
| 3px | 4px | +1px | --space-4, a tie taken upward |
| 4px | 4px | none | --space-4 |
| 5px | 4px | -1px | --space-4 |
| 6px | 8px | +2px | --space-8, a tie taken upward |
| 7px | 8px | +1px | --space-8 |
| 8px | 8px | none | --space-8 |
| 9px | 8px | -1px | --space-8 |
| 10px | 8px | -2px | --space-8 |
| 12px | 16px | +4px | --space-16, a tie taken upward |
| 14px | 16px | +2px | --space-16 |
| 16px | 16px | none | --space-16 |
| 18px | 16px | -2px | --space-16 |
| 20px | 24px | +4px | --space-24, a tie taken upward |
| 22px | 24px | +2px | --space-24 |
| 24px | 24px | none | --space-24 |
| 26px | 24px | -2px | --space-24 |
| 28px | 32px | +4px | --space-32, a tie taken upward |
| 30px | 32px | +2px | --space-32 |
| 32px | 32px | none | --space-32 |
| 36px | 40px | +4px | --space-40, a tie taken upward |
| 48px | 48px | none | --space-48 |
| 56px | 56px | none | --space-56 |
| 64px | 64px | none | --space-64 |
| 104px | calc(var(--row) + var(--space-48)) | none | the screen foot, and it is not a step |
No value in the product moves by more than 4px, and eight of the twenty-seven do not move at all. The four-pixel moves are all the same thing: a value sitting exactly halfway between two steps, taken upward by the rule rather than argued over one at a time.
The 104px foot is now written as what it is. kit.css line 765 gave every screen padding: 18px 16px 104px with a comment saying the foot clears the fixed tab bar, and the number itself never said so: 104 is the 56px tab bar plus 48px of air. It is now calc(var(--row) + var(--space-48)), which computes to the same 104 and cannot silently break when the row height changes. A foot is clearance, not rhythm, so it stays out of the grid rather than being folded into 104's nearest multiple of eight.
Twenty-seven values is the honest count, and it is larger than the twenty-two this page showed before. The old ladder counted only the px numbers written in a margin or a padding in kit.css, with the reviewer's chrome excluded and gap counted as a separate scale. The adopted mapping covers every spacing value the product wrote, gaps included, plus 0. Same file, wider basis.
What the missing tokens had already cost is visible in the docs. The root DESIGN.md records the rhythm as 16px screen padding, 18px between blocks and 20px before a new section, and lists spacing.row-y: 11px, a value written nowhere in kit.css. The gap that actually carried a block was 22px and the one before a section was 26px. A scale with no token cannot be checked, so it drifted from its own documentation and nothing complained. There are eleven names now, and correcting DESIGN.md against them belongs to DESIGN.md.
One piece of geometry here can never become a token, and it says so. The three container steps, 760, 900 and 1340, are documented rather than tokenized because a custom property is not valid inside a @container condition. That is a language limit, not an omission, and it is the one place where a number staying a literal is the correct answer.
The page frame
Before a component draws anything, design/system/base.css has set the ground. The test for that file is narrow: a rule earns a place in it only if removing it would break every component at once. This is what it sets, and the geometry inside it is small: a zero margin, a 1.5 line height, a 2px ring at a 2px offset, and a ceiling of 100% on an image.
| What | Value | Why, from base.css |
|---|---|---|
| box sizing | border-box | the first line of the file, and it was missing from the first draft: the same .btn measured 44px on a product screen and 46px on a component page, one pixel of border on each side, and nothing about the file looked wrong |
| body margin | 0 | the page owns its own gutters, the browser gives none. It is a literal zero today, and the grid now has --space-0 for the components that need to say zero on purpose |
| body background | var(--bg-page) | the canvas, so a screen never has to paint the ground it sits on |
| body colour | var(--text-primary) | the reading colour, inherited rather than declared per block |
| font family | Inter, then the system stack | one family for the whole product and no second one. A display face and a mono face for numbers are forbidden by name: a second family is how a calm interface starts sounding like two products |
| line height | 1.5 | the reading default, and the only ratio in this file. Line heights were out of scope for the founder's review, so this one is where it always was |
| text size adjust | 100% | the type does not resize itself when a phone turns |
| tabular numerals | table, .amt, .total | numerals line up in a column or this is not a money product. Applied where figures stack rather than globally, so prose keeps proportional digits |
| accent colour | var(--bg-action) | the native marks of a control, the checkbox tick and the progress fill, take petrol rather than the browser's blue |
| focus ring | 2px, offset 2px | one ring for the whole system, on :focus-visible rather than :focus: the ring is for the person arriving by keyboard and is noise for the person who just clicked. It is measured as a line at 3:1, not as text, and it holds 6.2:1 light and 6.8:1 dark. Written inside :where() so specificity stays at 0 and any component can override it without !important |
| images | max-width 100% | an image never overflows its slot, and one with no dimensions never collapses the layout while it loads |
| the one keyframe | pulse | two components carry it, the skeleton bar and the logo placeholder, so it belongs to neither. Deleting one cannot stop the other from animating, silently and with no error, which is not hypothetical: it happened at stage 07 |
| reduced motion | 0.01ms | at the same specificity as the rule it guards. Written as a bare class once, it lost the cascade and every skeleton pulsed forever for a person who had asked the operating system for no motion. A person who asks for no motion asks the whole product, not one component |
The focus ring's 2px width and 2px offset are literals, and they stay literals: there is one ring, in one place, and a token for a value with a single consumer is a name to keep correct with nothing to gain. That is the test the whole page runs on. A value that repeats needs a name, a value that happens once needs a comment. The radii passed it from the start, the spacing failed it for one stage and passes it now, and this ring is the one thing here that is correctly still a number.