Tokens come in two layers. Primitives are raw values with no opinion about where they are used — --l is 16px, --font-m is 18px, and neither knows what it is for. Semantic tokens name an intent and resolve through a primitive, so --text-primary is the colour body text takes, whatever that colour turns out to be. Page and component code uses the semantic layer; this page documents the primitive layer beneath it.
Why the split exists
Reaching past a semantic token to a primitive is the mistake this architecture is built to prevent. --neutral-800 is a grey; --text-secondary is the colour supporting text takes, which today happens to be that grey. Write the primitive and the element stops following the theme — it will not invert in dark mode, and a palette change leaves it behind.
- Use a semantic token in page and component code. If no semantic token fits, the semantic layer is missing one, and the fix is to add it rather than to reach past it.
- Use a primitive only when defining a semantic token. That is the one place the raw value belongs.
The unit scale
Every spacing and sizing token resolves through this scale, so the whole system moves together if it is re-stepped. Values are rem, which means they track the reader's browser zoom and root font-size preference rather than pinning to device pixels.
| Token | Value | Token | Value |
|---|---|---|---|
| 0 | 3.5rem · 56px | ||
| 0.125rem · 2px | 4rem · 64px | ||
| 0.25rem · 4px | 4.5rem · 72px | ||
| 0.5rem · 8px | 5rem · 80px | ||
| 0.75rem · 12px | 5.5rem · 88px | ||
| 1rem · 16px | 6rem · 96px | ||
| 1.5rem · 24px | 6.5rem · 104px | ||
| 2rem · 32px | 7rem · 112px | ||
| 2.5rem · 40px | 7.5rem · 120px | ||
| 3rem · 48px | 10rem · 160px |
The --space-* tokens are aliases onto this scale — --space-l is — and they are what layout code actually uses. The Spacing page covers them and the utilities that apply them.
Type scale
Font sizes are primitives. Nothing here says which element takes which step; that mapping is the web hierarchy on the Typography page.
| Token | Value | Token | Value |
|---|---|---|---|
| 0.625rem · 10px | 1.75rem · 28px | ||
| 0.75rem · 12px | 2rem · 32px | ||
| 0.875rem · 14px | 2.25rem · 36px | ||
| 1rem · 16px | 2.5rem · 40px | ||
| 1.125rem · 18px | 3rem · 48px | ||
| 1.25rem · 20px | 3.5rem · 56px | ||
| 1.375rem · 22px | 4rem · 64px | ||
| 1.5rem · 24px | 4.5rem · 72px |
--font-10xl · 72px
Where Culture Meets Care
--font-9xl · 64px
Where Culture Meets Care
--font-8xl · 56px
Where Culture Meets Care
--font-7xl · 48px
Where Culture Meets Care
--font-6xl · 40px
Where Culture Meets Care
--font-5xl · 36px
Where Culture Meets Care
--font-4xl · 32px
Where Culture Meets Care
--font-3xl · 28px
Where Culture Meets Care
--font-2xl · 24px
Where Culture Meets Care
--font-xl · 22px
Where Culture Meets Care
--font-l · 20px
Where Culture Meets Care
--font-m · 18px
Where Culture Meets Care
--font-s · 16px
Where Culture Meets Care
--font-xs · 14px
Where Culture Meets Care
--font-2xs · 12px
Where Culture Meets Care
--font-3xs · 10px
Where Culture Meets Care
Line height
| Token | Value | Use |
|---|---|---|
| 0.7 | Tight display settings where the leading would otherwise open the line up | |
| 1 | Single-line controls — buttons, badges, anything that never wraps | |
| 1.2 | Headings, where the type is large enough that generous leading reads as a gap | |
| 1.4 | Sub-headings and short passages | |
| 1.6 | Body copy, which is the one place leading does the most work for readability | |
| 1.8 | A long measure that needs more air than body copy |
Weight
The tokens span 300 to 900, but the loaded faces do not. Erode is a variable font covering 300–700; Inter is loaded at 400–700. So 400 to 700 is the range both faces render natively, 300 is available in Erode only, and nothing on the site provides 800 or 900 — a browser asked for those will clamp to 700 or synthesise a heavier face.
| Token | Value | Token | Value |
|---|---|---|---|
| 300 | 600 | ||
| 400 | 700 | ||
| 450 | 800 | ||
| 500 | 900 |
- Stay between 400 and 700. Those are the weights both faces actually ship; 800 and 900 are declared tokens with no loaded face behind them.
Letter spacing
| Token | Value | Use |
|---|---|---|
| -0.03em | Large headings, where default tracking looks loose | |
| 0.03em | A slight opening, for small text set in caps | |
| 0.06em | Labels | |
| 0.12em | Eyebrows and badges — the widest setting in regular use | |
| 0.24em | Reserved for display settings at large sizes |
Alpha
The alpha tokens are color-mix operands, not colours. --alpha-10 expands to transparent 90%, so color-mix(in srgb, var(--text-primary), var(--alpha-10)) is the text colour at 10 % strength. They step in fives from --alpha-5 to --alpha-95.
background: color-mix(in srgb, var(--text-primary), var(--alpha-10));
This is how the faded surfaces and hairlines are built, and it is why they follow the theme: mixing against a semantic token inverts when that token does.
Where the rest is documented
No family is listed twice, so each one has a single page that owns its values.
| Family | Documented on |
|---|---|
| Colour — semantic tokens, light and dark | Colour |
| Colour — the neutral and gold primitive ramps | Brand Book → Colour |
| Font slots, and which element takes which size | Typography |
--space-* and the spacing utilities |
Spacing |
| , the 44px floor for anything pressable | Spacing |
The four --safe-area-* insets |
Spacing |
| Border widths and radius | Border |
| Containers and max-widths | Layout |
| Easing and duration | Motion |
| Component tokens | Each component's own page, as those pages land |
Declared but unread
Some role tokens ship in the engine — four of them re-stepped at the breakpoints — but no rule reads them: --headline-size, --headline-weight, --headline-leading, --headline-tracking, --headline-width, --subline-size, --subline-weight, --title-size, --label-weight, --label-leading and --label-tracking.
- Do not build against them. Setting one changes nothing today, and a display class has to set
font-familyitself because--title-familyis the only role family token that exists.
This is recorded as an engine gap in docs/figma-mapping.md. The fix belongs upstream — either ship the classes that read them, or drop the tokens.