Chapter 01 / 12 · Tokens & primitives

Foundations. The grammar.

Every component in this library is built from the tokens on this page. Learn these first — the rest is just combinations.

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

Ink
--ink
#191E32
Primary foreground; deep slate blue — never pure black.
Paper
--paper
#FFFFFF
Pure white. Reserve for paper-like surfaces.
Slate
--slate
#466099
Soft foreground for secondary copy.
Lavender
--lavender
#6E90CC
Muted accent; hints at sky without stealing attention.

Warm neutrals (default surface family)

Warm 3
--warm-3
#F4ECE4
Default page background — cream, warm, human.
Warm 5
--warm-5
#E8DBCB
Sunken surface (cards-in-bg, code tabs).
Warm 7
--warm-7
#D2BFAA
Deep warm (borders on warm, dividers, tertiary tone).

Block quad — pink is hero

Pink 300
--pink-300
#FE84A9
Soft tint — highlights, borders-on-fill.
Pink 500
--pink-500
#FF5B8D
Secondary hue.
Pink 700
--pink-700
#FF3F7A
Hero accent — the one colour that does the heavy lifting.
Yellow 300
--yellow-300
#FFD878
Yellow 500
--yellow-500
#F9C33E
Yellow 700
--yellow-700
#F9AD03
Warning / highlight surface.
Green 300
--green-300
#7DF4D0
Green 500
--green-500
#47DDB2
Green 700
--green-700
#37BC9B
Success / confirmed.
Blue 300
--blue-300
#5BD9FC
Blue 500
--blue-500
#30C4F2
Blue 700
--blue-700
#1FAAE8
Info / system.

Semantic — resolved tokens

Accent
--accent
#FF3F7A
Resolves to --pink-700 in light mode.
Success
--success
#37BC9B
Warning
--warning
#F9AD03
Info
--info
#1FAAE8
Error
--error
#D64545
Only this red appears anywhere in the system.

Token reference

CSS custom properties

Every 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-13

Lowest 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.

--s-1
4px
Inline gap between icon + text; tight chips
--s-2
8px
Compact pad; icon buttons; tag gaps
--s-3
12px
Form field internal gap; inline paddings
--s-4
16px
Default gap; button padding vertical
--s-5
20px
Card internal gap; button padding horizontal
--s-6
24px
Section padding; form field margin
--s-7
32px
Card padding; page gutter
--s-8
40px
Hero padding; section head margin
--s-9
48px
Section rhythm; chapter head bottom
--s-10
64px
Hero rhythm; big section breaks
--s-11
80px
Top of chapter; generous breathing room
--s-12
96px
Chapter-level vertical rhythm
--s-13
128px
Page-level bottom; landing hero
/* 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

--r-xs
4px
Hairline chips, inline badges
--r-sm
6px
Inputs; tight buttons
--r-md
10px
Default button; menu items
--r-lg
14px
Cards; demo panels; tooltips
--r-xl
20px
Feature cards; marketing hero
--r-2xl
28px
Hero shelves; large panels
--r-pill
999px
Pills, avatars, segmented toggles
.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

--sh-0
Flush — no elevation
--sh-1
Hairline lift — buttons, toggle pills
--sh-2
Cards at rest
--sh-3
Hover / raised / menus
--sh-4
Modals, dialogs
--sh-pink
Hero CTA — use once per screen
--sh-focus
Keyboard focus ring (not elevation)
.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.

--dur-1 · 100ms
Keyboard feedback, checkbox toggles
--dur-2 · 160ms
Hover, focus, colour shifts
--dur-3 · 240ms
Card hover, modal enter
--dur-4 · 400ms
Layout shifts, hero reveals

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

1px solid var(--hair)
Hairline — dividers, cards
1px solid var(--hair-soft)
Whisper — chips, nested surfaces
2px solid var(--fg)
Loud — sparingly, never default
1px dashed var(--hair)
Empty states, drop zones
1px solid var(--accent)
Selected state — with bg accent-soft
inset box-shadow 1px var(--hair)
Border that doesn't affect layout
.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-faint

Each step drops the reader's attention by a clear notch. Don't use more than three of these in the same block of text.

Aa
--fg
100% · primary
Aa
--fg-soft
slate · secondary
Aa
--fg-dim
55% · metadata
Aa
--fg-faint
35% · whispers
Aa
opacity: .4
disabled states
/* 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

--z-base · 1
Default flow. Everything starts here.
--z-sticky · 10
Top nav, side TOC, sticky shelf.
--z-overlay · 100
Dropdowns, popovers, tooltips.
--z-modal · 200
Modal dialog + its backdrop.
--z-toast · 300
Toasts + transient system messages.
: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.

1
Colourbackground: var(--accent); color: var(--paper);
2
Radiusborder-radius: var(--r-md);
3
Spacingpadding: var(--s-3) var(--s-5); gap: var(--s-2);
4
Motiontransition: transform var(--dur-2) var(--ease);