1.1 Colour
Eighteen named colours, four font families, one hero accent. The default surface is warm cream — never default to dark. Use semantic tokens (--fg, --bg, --accent) in component CSS; reserve the raw palette names for the token file.
Ink & paper
Warm neutrals (default surface family)
Block quad — pink is hero
Semantic — resolved tokens
Token reference
CSS custom propertiesEvery component CSS selector in this library reads from tokens below. Flipping [data-theme="dark"] on <body> remaps the semantic layer in place.
/* Always reference tokens, never literal hex values,
so theme-switching flips automatically. */
color: var(--fg); /* body text */
background: var(--bg); /* page surface */
background: var(--bg-paper); /* cards / panels */
background: var(--bg-sunk); /* code blocks, chips */
border-color: var(--hair); /* 12% ink — default hairline */
border-color: var(--hair-soft); /* 6% ink — subliminal */
color: var(--fg-soft); /* secondary copy */
color: var(--fg-dim); /* 55% ink — metadata */
color: var(--fg-faint); /* 35% ink — whispers */
background: var(--accent-soft); /* 12% pink — bg for pill/alert */1.2 Spacing scale
A 4-step base multiplied up to 128px. Use tokens, not magic numbers. Most components live between --s-3 and --s-7; heroes and chapter heads reach for --s-10 upward.
The ruler
--s-1 → --s-13Lowest values for compact inline gaps; highest values for page rhythm. Skipping a step is usually a sign you need a different token, not a fractional one.
/* Use spacing tokens everywhere — never hard-coded px. */
.card { padding: var(--s-7); gap: var(--s-5); }
.section { padding: var(--s-11) 0; }
.button { padding: var(--s-3) var(--s-5); }
.stack > * + * { margin-top: var(--s-4); }1.3 Radii
Seven steps from hairline chip to full pill. Friendly but restrained — the whole system leans warm, so radii should too. Avoid mixing non-token radii on the same surface.
Radius tokens
--r-xs → --r-pill.chip { border-radius: var(--r-xs); }
.input { border-radius: var(--r-sm); }
.button { border-radius: var(--r-md); }
.card { border-radius: var(--r-lg); }
.hero { border-radius: var(--r-2xl); }
.avatar, .pill { border-radius: var(--r-pill); }1.4 Shadow & elevation
Four elevation steps, one pink emphasis, one focus ring. Shadows bias warm — they're all based on ink at low opacity, never pure black. Don't stack shadows; pick one token.
Elevation tokens
--sh-0 → --sh-pink.card { box-shadow: var(--sh-2); }
.card:hover { box-shadow: var(--sh-3); }
.modal { box-shadow: var(--sh-4); }
.button.cta { box-shadow: var(--sh-pink); }
:focus-visible { outline: 0; box-shadow: var(--sh-focus); }1.5 Motion
Four durations. One ease. Motion should be confident, not chatty — it confirms what happened, not what's about to happen. Respect prefers-reduced-motion; the tokens below do.
Durations + easing
--dur-1 → --dur-4 · --ease--ease is a spring-like out-curve (0.2, 0.8, 0.2, 1) that feels alive without being bouncy. Use it for nearly every transition.
Hover the squares → each transitions at its own duration.
:root {
--dur-1: 100ms;
--dur-2: 160ms;
--dur-3: 240ms;
--dur-4: 400ms;
--ease: cubic-bezier(0.2, 0.8, 0.2, 1);
}
.button { transition: background var(--dur-2) var(--ease); }
.card { transition: box-shadow var(--dur-3) var(--ease),
transform var(--dur-3) var(--ease); }
.modal { animation: pop var(--dur-4) var(--ease); }
@media (prefers-reduced-motion: reduce) {
* { transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important; }
}1.6 Borders
Borders are almost always 1px, almost always var(--hair). A 2px border reads as a shout — reserve it for the one element that needs a shout.
Border styles
hair / hair-soft / accent.card { border: 1px solid var(--hair); }
.nested { border: 1px solid var(--hair-soft); }
.dropzone { border: 1px dashed var(--hair); }
.selected { border: 1px solid var(--accent);
background: var(--accent-soft); }
/* Inset border — no layout shift on hover */
.chip { box-shadow: inset 0 0 0 1px var(--hair); }1.7 Opacity & emphasis
Text hierarchy is built with named foreground tokens, not raw opacity. The only time opacity-alone is appropriate is for disabled affordances.
Emphasis levels
fg / fg-soft / fg-dim / fg-faintEach step drops the reader's attention by a clear notch. Don't use more than three of these in the same block of text.
/* Text emphasis by token, not by raw opacity. */
.title { color: var(--fg); } /* primary */
.body { color: var(--fg-soft); } /* secondary */
.meta { color: var(--fg-dim); } /* metadata */
.caption { color: var(--fg-faint); } /* hints */
/* Disabled uses real opacity. */
.button[aria-disabled="true"] { opacity: .4; pointer-events: none; }1.8 Z-index
Five named layers, big gaps between them. Never write a naked z-index — if something needs to sit between two layers, the layer tokens are wrong, not the component.
Stacking tokens
--z-base → --z-toast:root {
--z-base: 1;
--z-sticky: 10;
--z-overlay: 100;
--z-modal: 200;
--z-toast: 300;
}
.topnav { position: sticky; z-index: var(--z-sticky); }
.tooltip { position: absolute; z-index: var(--z-overlay); }
.modal { position: fixed; z-index: var(--z-modal); }
.toast { position: fixed; z-index: var(--z-toast); }1.9 How a token flows into a component
Every component in this library is assembled from the same short list of tokens. Here's the canonical example — the hero CTA button, broken out into the four aspects (colour, radius, spacing, motion) that every component in the library uses.