Every token is declared in app/globals.css and mapped to a utility class in its @theme inline block. Both halves are required — a variable with no mapping generates no class, silently.

The values below are the current ones. app/globals.css is the source of truth, and its comments record the measured contrast ratios and gamut ceilings behind each number.

Colour

Authored parameters

Four values. Everything else derives from them.

TokenValuePurpose
--brand-h350The only hue in the product (deep raspberry). Rotate to rebrand.
--brand-c0.2Primary chroma.
--brand-c-subtle0.06Tints, badges, hover states.
--neutral-c0.006Neutral tint toward the brand — cohesion, not visible colour.

Surfaces

TokenLightDarkUtility
--background0.940.115bg-background
--card1.00.205bg-card
--muted0.9580.265bg-muted
--sidebarvar(--background)samebg-sidebar
--border0.918border-border

Card↔canvas measures 1.195 (light) and 1.136 (dark). That gap is what lets the design drop borders — see the overview.

Brand

TokenLightDark
--primaryoklch(0.54 var(--brand-c) var(--brand-h))oklch(0.72 0.19 var(--brand-h))
--primary-foregroundnear-whitenear-black
--accent0.96 at subtle chroma
--accent-foreground0.50 at full chroma

Primary sits at L0.54 for a reason worth knowing: a pink only reads as pink in the upper half of the lightness range — below about L0.50 it turns to wine. 0.54 is the brightest stop that still carries a near-white label at AA (5.46:1) and works as a link on --background (5.55:1).

Dark mode is desaturated and lifted, never inverted. Inverting produces vibrating colour on dark backgrounds.

Semantic states — fixed hues

TokenHueLight LDark L
--destructive270.550.70
--success1550.550.72
--warning750.550.80
--info2400.550.70

These never reference --brand-h. A rebrand must not turn "success" pink. Note that --destructive sits 37° from the brand hue so a primary button and a delete button never read as the same colour — worth re-checking if you rotate the brand toward red.

The uniform 0.55 in light mode is deliberate: it is the value that satisfies text contrast, which is strictly harder than fill contrast. In dark mode the values are deliberately not uniform, because against a dark background lifting a token increases contrast — so there the risk runs the other way. The rule is not "all four must match": it is "no token drifts toward its own background."

Chart ramp

One analogous ramp anchored on the brand, hue-stable across themes:

css
--chart-1: oklch(0.55 0.2  var(--brand-h));
--chart-2: oklch(0.62 0.18 calc(var(--brand-h) - 45));
--chart-3: oklch(0.68 0.16 calc(var(--brand-h) - 90));
--chart-4: oklch(0.6  0.19 calc(var(--brand-h) + 45));
--chart-5: oklch(0.72 0.14 calc(var(--brand-h) + 90));
The chart ramp is authored for fills

Chart stops are tuned for 3:1 against an adjacent surface, which is the bar for non-text. Small text needs 4.5:1, and the lighter stops do not reach it. Measure before using text-chart-4 or similar for a label.

Typography

--font-sansDM Sans, --font-monoJetBrains Mono, both loaded via next/font/google in app/layout.tsx. Mono is preload: false because nothing above the fold uses it.

Banned across the project: Inter, Roboto, Arial, Open Sans, Syne.

The scale is fluid — clamp() rather than breakpoint-specific classes, so text-4xl adapts on its own:

TokenValue
--text-2xs0.6875rem (11px, micro-labels)
--text-xlclamp(1.25rem, 1.15rem + 0.5vw, 1.5rem)
--text-2xlclamp(1.5rem, 1.3rem + 1vw, 2rem)
--text-3xlclamp(1.875rem, 1.5rem + 1.9vw, 2.75rem)
--text-4xlclamp(2.25rem, 1.6rem + 3.2vw, 4rem)
--text-5xlclamp(2.75rem, 1.7rem + 5.2vw, 5.5rem) (hero only)

Every numeral in a table, credit balance, price, token count or timer uses tabular-nums. Proportional digits make a column jitter as values update.

Containers

TokenValueUse
--container-app80remDashboards — max-w-app
--container-marketing90remHomepage — max-w-marketing
--container-prose45remLegal, blog, docs — max-w-prose

Before these existed, dashboards sprawled unbounded above 1280px.

Elevation

TokenUse
--shadow-raiseA nav item or small tile lifting off the canvas
--shadow-cardThe resting state of a card or content panel
--shadow-floatHover / raised state
--shadow-modalDialogs, drawers

They are brand-tinted, not neutral blackoklch(0.16 0.02 var(--brand-h) / …). A pure-black shadow on a hue-tinted surface reads as dirt.

In dark mode, express elevation through surface lightness rather than shadow: a shadow on a dark background is nearly invisible, which is why --card lifts from 0.115 to 0.205 there.

No ad-hoc box-shadow. Use shadow-card / hover:shadow-float.

Motion

TokenValue
--ease-outcubic-bezier(0.23, 1, 0.32, 1)
--ease-ui-in-outcubic-bezier(0.65, 0, 0.35, 1)
--dur-instant100ms
--dur-fast150ms
--dur-base200ms
--dur-slow280ms
--dur-reveal500ms

--ease-out deliberately overrides Tailwind's built-in curve project-wide, so every existing ease-out utility picks it up. One easing across the product.

House rules: no ease-in for UI, no bounce or elastic easing, no transition: all, nothing over 300ms for UI feedback, and no animation on keyboard-initiated actions. Exit animations run at roughly 65% of enter — the user has already decided, so getting out of the way should feel faster than arriving.

Z-index

--z-base 0 · --z-raised 10 · --z-sticky 20 · --z-drawer 30 · --z-overlay 40 · --z-modal 50 · --z-popover 60 · --z-toast 70 · --z-max 100

Used through the arbitrary-property syntax: z-(--z-modal). Never a bare z-50.

Scrollbars

Styled once, globally, and inherited by every overflow-* container. A new scroll container needs no scrollbar class at all — adding one is the bug.

TokenValue
--scrollbar-size10px
--scrollbar-inset2px
--scrollbar-thumb--foreground at 0.22 alpha
--scrollbar-thumb-hoverat 0.38 alpha

The track is transparent — the panel's own surface is the track — and carries a 6px pill. The inset is a transparent border plus background-clip: content-box, not padding: background-clip cannot exclude padding, and this keeps the hit target at the full track width while the thumb reads as a floating capsule.

Two utilities exist for deliberate exceptions:

UtilityUse for
scrollbar-noneHorizontal rails only — a snap carousel, a chip row — where the peeking next item is the affordance. Never on a vertical panel: hiding the only signal that a region scrolls is a real discoverability loss.
scrollbar-stablePanels whose content length changes between routes, so they do not jog sideways at the scroll threshold.
A utility that matches nothing fails silently

Two hand-rolled scrollbar implementations preceded the global one, and both failed without a trace. One inlined [scrollbar-width:none]; the other reached for scrollbar-none — a class that did not exist at the time — so the rail it was meant to clean up shipped with a raw bar. A Tailwind class that matches nothing produces no error, no warning and no visual clue in review. If you write one, confirm it compiles.