Typography
One family, eight steps, and no half values. The eight are primitives in design/system/tokens.css, adopted at the founder's review of the foundations on 2026-08-11. The product they correct shipped 21 sizes and no variable behind any of them, five of those sizes half steps that existed for no stated reason. This page shows the eight at size in real product copy, and then where every one of the 21 lands.
The family
One declaration sets it and nothing overrides it: design/system/base.css line 34, on body. There is no second family anywhere in the product. DESIGN.md names the two that will not exist, a display face and a mono face for numbers, and the reason is in its Character line: the personality is in the size jump, not in the letterforms. Figures still line up in a column, because base.css turns tabular numerals on for table, .amt and .total, which is everywhere figures stack.
The monospace you can see on this page belongs to the stand, from design/kit/_page.css. It marks a measurement. The product never ships it.
'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif
Three weights, and this pass did not touch them. 400, 600 and 700, exactly as the product already carries them. The scale below is a scale of sizes: a step says how big, and the place says how heavy. What that leaves open is written at the foot of this page, because it is worth leaving open on purpose rather than by omission.
The eight steps
Eight steps and no half values. Each one is a job a reader can name, with a perceptible gap between neighbours, and each is a primitive in tokens.css that a component reads through var(). Every line below is real product copy from voice/docs/microcopy.md, set at that step's size.
Size is the whole of what a step declares. The weight, line height and tracking on each specimen are what the product already gives that place, and they are not part of this pass; the section at the foot says why they were left alone. Colour is left out on purpose, and the reason matters: colour is where the product's real second tier lives, not size.
The scale has gained two steps since this review, and one clamp, and this page keeps the record of the eight rather than quietly becoming ten. --type-step (40px, the ordinal of a step in a sequence) arrived on 2026-08-15 and --type-section (32px, the subject of a section on the public page, past the tablet point) on 2026-08-16, both for the landing; --type-hero is a clamp() rather than a step and is not counted in either number. --type-section carries the same value as --type-figure and is a token of its own on purpose: two roles means two tokens even at one value today, and one of the two is a number about money on an app screen while the other is a heading on a marketing page. The eight below are what the founder's review of the foundations adopted, and this section is the record of that review.
Each specimen below reads its own token, through var(--type-display) and its seven neighbours: change a value in tokens.css and this ladder moves with it. The ladder further down does not, and must not. That one is a record of the sizes the product used to carry, so it is drawn from literals and stays where it was even if every token in the file changes. A page that documents a scale without reading it can quietly disagree with the scale.
$192.90
$11.99
Add a subscription
Spotify Premium
Connect your bank
See your subscriptions
Active
The steps are rem and not px, since 2026-08-13, and the px in brackets is what each one renders at the browser default. The reason is not a convention, it is a contradiction this system was already carrying: stage 10 declared both width points in rem and gave as its reason that a point should ask how wide the window is relative to the text the person set. A px scale does not move with that setting at all, so the promise was only half kept. Measured on design/home.html at a 24px root: the text stayed 16 and 12px while the 900px window fell below the tablet point, so the rail went away and the page measure came off. The person who asked for larger text paid the whole layout change and got no larger text. Page zoom is a different mechanism and scales px and rem alike, which is why this stayed invisible for two stages.
Nothing moved for anybody who has not changed that setting, and that is measured rather than asserted. 8 596 element records (box, font size, line height, weight) across all 32 coloured screens of stage 10 at 390, 760, 1280 and 1600, the px scale against the rem one: identical, not one record different. Recounted on 2026-08-14: the first pass read 7 660 off a page list of 28 that predated the four Alerts screens and named a design/index.html that does not exist. The finding held, the corpus had grown. The root font size is set nowhere in this system, so at the default every step is the number it always was, and every conversion above is exact with no rounding. The geometry stayed px on purpose - spacing, radii, the 220px rail, the 300px column floor - because that is geometry and not text, the same distinction that denies the width block a dark pair. The reading measures were already in ch and every line height in the system is already unitless, so both scale by themselves. One thing broke and it is named: the plan card's button label stopped fitting a 390px phone at a 24px root, which is button.html, the one exception to the no-wrap rule.
The namespace is --type- and not --text-, and that is not a preference. --text- already belongs to the ink roles, and the first draft of this block called the body step --text-body, which is the name of a colour three hundred lines further down the same file. The later declaration wins, so every component asking for the body ink would have been handed 14px. It was caught by reading the file back before anything was wired to it. Two things called the same name are one thing.
What folded into what
The rule, and it was applied mechanically: every old size goes to the nearest step, and a tie goes to the larger, which is the direction a product for anxious readers should round in. Nobody decided value by value. One exception, and it is named: 40px is nearer 46 than 32, but 46 is the Display step and a screen gets one object at it under the One Number Rule (the monthly total in the app, the plan price on the landing since 2026-08-15), so the share card's total goes down to 32 rather than up.
| Was | Becomes | Move | Token |
|---|---|---|---|
| 46px | 46px | none | --type-display |
| 40px | 32px | -8px | --type-figure, the named exception |
| 32px | 32px | none | --type-figure |
| 30px | 32px | +2px | --type-figure |
| 24px | 24px | none | --type-head |
| 21px | 20px | -1px | --type-title |
| 20px | 20px | none | --type-title |
| 17px | 16px | -1px | --type-sub |
| 16px | 16px | none | --type-sub |
| 15px | 16px | +1px | --type-sub, a tie taken upward |
| 14.5px | 14px | -0.5px | --type-body |
| 14px | 14px | none | --type-body |
| 13.5px | 14px | +0.5px | --type-body |
| 13px | 14px | +1px | --type-body, a tie taken upward |
| 12.5px | 12px | -0.5px | --type-meta |
| 12px | 12px | none | --type-meta |
| 11.5px | 12px | +0.5px | --type-meta |
| 11px | 12px | +1px | --type-meta, a tie taken upward |
| 10.5px | 10px | -0.5px | --type-label |
| 8px | 10px | +2px | --type-label, below the smallest step |
One size moves by more than two pixels, and it is the one with a name on it. Every other line in the table moves by two pixels or less, and eight of the twenty do not move at all. That is what a mechanical rule buys: the argument happens once, over the rule, instead of twenty times over twenty values.
Three values are not in the table, and each is out for its own reason. 22px on .plan-opt .amt is dead: the identical selector at kit.css line 680 set 30px at the same specificity and won, so the 22px rule at line 307 has never been seen by anybody. It is deleted rather than folded. 18px and 10px, on .sidebar-close and .scr-label, were never the product: they are the reviewer's chrome, and they leave with it to _page.css. The scale's own 10px arrives from the badge, not from them.
The ladder this replaces
This section is the evidence, and it is short on purpose. Twenty-three sizes in kit.css, 111 declarations, and no variable behind any of them. Two of the twenty-three are the reviewer's chrome, so the product shipped twenty-one, and one of those twenty-one rendered nowhere. A size was written where it was used, so changing one changed exactly one place and nothing else.
Five of them were half steps: 14.5, 13.5, 12.5, 11.5 and 10.5. No rule in the file said why any of them was half a pixel from its neighbour, because there was no rule. They are the clearest thing the eight steps remove, and describing them proves nothing, so each one is below beside the step it folded into.
Read each pair left to right, in one string of copy so the words cannot take the blame. The left half is the half step and it is frozen on a literal size, because it is a record of what the product used to carry and must not move when a token does. The right half is the step it became and it reads tokens.css. If you cannot tell them apart, that is the argument: the counts are the number of places in kit.css that stood on the half step, counted in the file rather than taken from the audit.
Data and privacy Data and privacy
That pair is the one place a weight gets in the way, and it is worth saying rather than hiding: the 14.5px rule on .alert .what is 600 and the body step is 400, so the left half is heavier as well as half a pixel larger. Weights are not part of this pass. The other four pairs match in weight and differ in size alone.
See what they are See what they are
in 10 days · Aug 11
Typically $11.99 a month
Active Active
One of the five counts needed its rule stated. 13.5px was written fifteen times in kit.css and only fourteen of them were the product: the fifteenth is .nav-brand at line 106, the reviewer's chrome, which _page.css now owns and which travels to no build. The other four counts hold exactly as measured: 3, 9, 1 and 1.
The sharpest evidence was inside a single row. .row .name was 15px and .row .amt was 14px: the two halves of the same line, one pixel apart, for no reason anybody wrote down. Under the scale the name is 16px and the amount is 14px, which is a difference a reader can name, or both sit on one step if the row wants them level. Either is a decision. One pixel was not.
And the largest group was one pixel from two others. 13px carried 29 declarations, with 13.5px on 15 above it and 12.5px on 9 below: fifty-three places on three sizes covering a range of a single pixel. All of 13.5, 13 and 14 now sit on --type-body, and 12.5 sits on --type-meta beside 12.
What did not change, and why
Line heights, letter spacing and weights are out of scope for this pass. Eleven line heights, nine tracking values and three weights are still exactly where the product left them. They were never the complaint, and folding them at the same time would hide what the size change did: if a line moves and its size, its weight and its leading all changed at once, nothing about the move can be attributed to anything. One axis at a time is how a change stays readable afterwards.
Colour did not move either. Twenty-two primitives and twenty-nine roles, all paired across both themes, untouched by this review. Nothing on this page changes what any line is coloured, and the second tier of information is still carried by --text-muted rather than by a size.
The open question this leaves, stated so it is not discovered later. Nine heading rules in kit.css set a size and no weight, so they rendered at the browser's bold rather than at a chosen one, and body declares no weight either. The scale does not settle that, because the scale is sizes. It is the next thing to decide about type, and it belongs to whoever writes the heading component rather than to this page.
One thing the scale does not fix. All eight steps are in px, so none of them answers a browser text-size setting. Changing that is a rem question and it belongs to Responsive, stage 10. It is written here so it is not discovered there.