Library · Foundations
Tokens, scale & motion
The measurable half. Every number the system uses, in one place, with the reasoning attached — because a scale you can't justify is a scale you'll abandon. Drop the block at the bottom into a new repo and the primitives land correctly.
Color
Four tokens
Ground, ink, rule, accent. Everything else is one of those four at reduced opacity — which is why the system reads as one surface instead of a palette. OKLCH throughout, so lightness is perceptual and a dark-mode flip is a lightness change, not a re-pick.
| token | value | |
|---|---|---|
| --background | oklch(0.145 0.004 106) | |
| --foreground | oklch(0.93 0.003 106) | |
| --border | oklch(0.27 0.004 106) | |
| --primary | oklch(0.71 0.11 152) | |
| --card | foreground @ 4% | |
| --muted | foreground @ 8% |
The opacity ladder. Text steps down foreground → /85 → muted-foreground → /70 → /50 → /40. Six steps is the whole hierarchy; if something needs a seventh, the layout is wrong, not the palette.
Space
The rhythm
A 4px base, but only six steps are ever used. Sections breathe at 64, cards at 20, and related things sit 8–12 apart. Constraint is the point: fewer choices, faster decisions, consistent result.
| token | value | |
|---|---|---|
| gap-2 | Icon to label, chip to chip | 8px |
| gap-3 | Buttons in a row, list items | 12px |
| mt-4 | Inside a card, between blocks | 16px |
| p-5 | Card padding — the default | 20px |
| mt-10 | Header to first section | 40px |
| mt-16 | Between sections | 64px |
Shape
Radius & rule
Radius scales with the surface: the bigger the box, the softer the corner. One border weight everywhere — 1px at token color. No shadows anywhere in the system; depth comes from surface lightness, not from a glow.
Type
The ramp
One family, four weights, and a ramp that leans light. Display sizes get extralight with negative tracking; small text gets medium with positive tracking. The inversion is deliberate — it's what makes small type read as a label rather than shrunken body copy.
Page title
Section title
The default paragraph, set light for long-form comfort.
Captions, secondary detail.
SECTION LABEL
const token = value
Motion
Timings & the rule
One rule governs everything, including the brand engines: energy moves space, never brightness — scale, position, displacement. No strobe, no flash, no opacity pulsing. It keeps long loops watchable and the whole system safe for photosensitive viewers.
| token | value | |
|---|---|---|
| 150ms | Hover, focus — color only | transition-colors |
| 200ms | Tab content swap | animate-in fade-in duration-200 |
| 300ms | Image hover scale | transition-transform duration-300 |
| 450ms | Scroll reveal | opacity + translateY(16px) |
| 700ms | Hero entrance | motion-safe:animate-in fade-in slide-in-from-bottom-2 |
| 120ms | Stagger step | animationDelay: step * 120ms |
Always gated. Entrances use motion-safe: and canvas loops check prefers-reduced-motion before starting — reduced motion renders one representative frame, never a frozen blank.
Layout
Widths & breakpoints
| token | value | |
|---|---|---|
| max-w-2xl | Write-ups — one column of prose | 672px |
| max-w-3xl | Reference pages, galleries | 768px |
| max-w-site | Landing, project index | 1200px |
| site-inset | The universal gutter | px-6 |
| sm: | One column → two | 640px |
| md: | Stacked hero → side by side | 768px |
| lg: | Two columns → three | 1024px |
No device sniffing. Layout responds to width; touch-specific behavior responds to pointer: coarse and hover. There is no isMobile anywhere in any repo running this system.
Transfer
The whole system, in one block
Paste into globals.css and every primitive on this site renders correctly in the new repo. Tailwind v4 reads the variables directly — no config file needed.
@import "tailwindcss";
:root {
--background: oklch(0.985 0.002 106);
--foreground: oklch(0.185 0.004 106);
--border: oklch(0.90 0.003 106);
--primary: oklch(0.42 0.08 152); /* the one accent */
--card: oklch(0.185 0.004 106 / 0.03);
--muted: oklch(0.185 0.004 106 / 0.06);
--radius: 0.75rem;
}
.dark {
--background: oklch(0.145 0.004 106);
--foreground: oklch(0.93 0.003 106);
--border: oklch(0.27 0.004 106);
--primary: oklch(0.71 0.11 152);
--card: oklch(0.93 0.003 106 / 0.04);
--muted: oklch(0.93 0.003 106 / 0.08);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-border: var(--border);
--color-primary: var(--primary);
--color-card: var(--card);
--color-muted: var(--muted);
--color-muted-foreground: color-mix(in oklch, var(--foreground) 62%, transparent);
}
/* the two layout helpers everything uses */
.max-w-site { max-width: 1200px; }
.site-inset { padding-inline: 1.5rem; }With the block above plus primitives and the brand engine, a new repo starts with the same design language on day one — which is the entire point of writing this down.