01 · Foundations

Colour, type, and the numbers underneath

Everything here is a CSS custom property on :root. The light theme redefines the same names under html[data-theme="light"] and changes nothing else — flip the toggle in the nav and watch the whole page follow.

01

Palette

Cool slate in the dark theme, warm stone in the light one. That is deliberate: inverting slate goes muddy, and stone gives the light theme its own character rather than making it read as the dark theme with the lights on. Click any swatch to copy its token.

Surfaces
Lines
Ink
Accent and status
One accent, honestly. Amber, red and green are status, not palette. If you find yourself picking between them for emphasis rather than for meaning, the answer is cyan or nothing.
02

Token reference

The complete set. Anything not on this list is a value someone hardcoded.

TokenDarkLightUse
--bg#020617#f5f5f4Page background
--panel#0b1120#fafaf9Raised surface
--panel-2#0f172a#e7e5e4Track and ground fills
--hairline#1e293b#bfbbb7Every border
--text#f8fafc#0c0a09Headings
--body#cbd5e1#292524Interface copy
--ink / --ink-2#dde5ef / #9fb0c4#1c1917 / #44403cProse, secondary prose
--muted / --dim#7c8ba1 / #64748b#57534e / #78716cChrome only
--cyan#67c7d4#0e7490The accent
--font-headingAlliance No 1Headings, nav, UI labels
--font-sansAlliance No 2Body, small uppercase chrome
--font-monoSpace MonoCode, tokens, indices, figures
--maxw1320pxRail separation
--gutterclamp(1.25rem, 4vw, 2.8rem)Padding inside the rails
--band64pxNav height, footer height
--chrome130px64 + 1 + 1 + 64 — both hairlines
--measure44remReading column
--radius-notch3pxThe only radius in the system
--t-fast / --t / --t-slow0.2s / 0.25s / 0.42sTransitions
--easecubic-bezier(.22,.6,.36,1)The house curve
03

Contrast

On a near-black background these are easy to get wrong by eye — a grey that looks fine at 48px is unreadable at 14px. Compute the ratio. The two chrome greys are listed here precisely so nobody reaches for them by accident.

TokenOn --bgRatingMay carry
--text19.4:1AAAAnything
--body11.9:1AAAAnything
--ink13.6:1AAALong-form prose
--ink-27.4:1AAASecondary prose
--muted5.8:1AALabels, chrome — not paragraphs
--dim4.2:1Fails AADecorative chrome only
--cyan9.7:1AAALinks, active state
--dim is below the 4.5:1 floor. It exists for things a reader never has to read: a decorative index number, an axis tick that repeats a labelled value, the resting state of an icon that also has a text label. Anything else needs --muted or brighter.
04

Typography

Two faces and a mono. Alliance No 1 carries headings, nav and UI labels; Alliance No 2 carries body copy, and ships a single weight — emphasis switches face to No 1 rather than asking No 2 for a bold it does not have; Space Mono carries data only: code, token names, part numbers, indices and figures, never chrome labels. Both Alliance cuts are light-weight grotesques and read small for their nominal size — size up rather than down.

.displayAlliance No 1 · 300
clamp(2.39 – 3.95rem)
-0.02em · 1.08
Inventory that earns its keep
.titleAlliance No 1 · 300
clamp(2.1 – 3.27rem)
-0.02em · 1.05
Work order intelligence
.headingAlliance No 1 · 300
clamp(1.63 – 2.2rem)
-0.015em · 1.15
Where the stock actually goes
.subheadingAlliance No 1 · 400
1.5rem · +0.01em
Method and assumptions
.ledeAlliance No 1 · 300
clamp(1.2 – 1.37rem)
1.65 · 38ch
Data science for supply chain. AI used as a tool, not as a headline.
bodyAlliance No 2 · 300
1rem · 1.6
Default body copy. Sentence case, generous leading, and a measure that stops around 44rem. Long-form reading uses .prose, which steps the colour up to --ink and the leading to 1.8.
.monoSpace Mono · 400
0.96rem · 0.22em
uppercase · data
Figure 04 — Consumption vs. reorder point
.eyebrowAlliance No 1 · 300
0.95rem · 0.12em
accent
Section label
.pullAlliance No 1 · 300
1.54rem · 2px left rule
The forecast was never the problem. The reorder point was.
Character set — Alliance No 1
ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 &@#%$€£ ()[]{}/\|—–-·•→↑↓↔ .,:;!?'"
Figures

Every number a reader compares down a column gets .num, which turns on tabular and lining figures (tnum, lnum, ss01). Tables do it automatically.

Proportional vs. tabular
Default
.num
1,184,203
911,470
1,000,111
1,184,203
911,470
1,000,111
05

Space & geometry

There is no 4px/8px spacing scale to memorise. Layout space comes from three places: the --gutter inside the rails, the 96px grid module the background draws, and clamp() on anything that must survive from 320px to 2560px.

Frame
1320px, centred. Side margins are whatever is left over — (100vw − 1320px) / 2 — and collapse to zero below 1320px. There is no fixed sidebar.
Gutter
clamp(1.25rem, 4vw, 2.8rem) — 20px on a phone, 44.8px on a desktop. Used for horizontal padding inside the rails and nothing else.
Module
96px. The grid background draws it; the layout implies it. Card gaps are clamp(1rem, 2vw, 1.8rem), which sits inside it comfortably at every width.
Bands
64px nav, 64px footer, 1px hairline on each. A viewport-locked page takes calc(100vh − 130px). Count both hairlines — 129 overflows by exactly 1px and puts a scrollbar on a page that is supposed to be fixed.
Measure
44rem for reading. Centred inside the frame, not pinned to its left edge — a 44rem column against a 1320px rail reads as a layout that failed to finish.
Vertical rhythm
Section padding clamp(1.4rem, 3.5vh, 2.6rem); heading margins from the type's own leading. Nothing is set in fixed rem above 3rem.
06

Motion

Short, eased, and always in service of a change of state. The house curve is cubic-bezier(0.22, 0.6, 0.36, 1) — quick to start, long to settle. Three durations cover everything.

TokenValueFor
--t-fast0.2sColour and border changes on hover
--t0.25sPopovers, tooltips, notch states
--t-slow0.42sPanel slides, drawer track, bar growth
Entrances
.anim-fade-up
.anim-pop-in
Running
Reduced motion is honoured globally. prefers-reduced-motion: reduce collapses every animation and transition to 0.01ms and kills the headline sweep outright. You do not need to handle it per component.