The design system, showing itself

Design tokens

Every colour, distance, corner, shadow and curve in mySpoils is a named token, shown here in both themes at once. The page is generated from the stylesheet at build time, so it cannot fall behind what ships. Contrast is measured in the browser rather than claimed.

tokens in light
119
re-picked for dark
115
same in both
4
families
16

Surfaces

8 tokens

Light is the pre-theming palette, value for value: neutral-50 page, white cards, amber-50/40-over-white inset, neutral-100 subtle.

Ink

8 tokens

The original neutral ramp, with one deliberate exception: --ink-meta and --ink-faint (dates, counts, hints, placeholders) were neutral-400 at ~2.5:1, which is a legibility problem rather than a conformance technicality. …

Lines

5 tokens

Four hairlines and a track, from the faintest divider to the edge of a control. The step between them is lightness alone in light; dark re-picks them, because a border that only separates and a border that bounds a control diverge once the page behind them is dark.

Brand

9 tokens

#6366f1 is a fixed brand value and does not move. --brand-ink (links and inline brand text) is the same colour in light, as it shipped; only dark lifts it, where there is no prior version to preserve.

Inverse

4 tokens

For the handful of elements that deliberately oppose the page: selected filter chips, tooltips, the toast. These have to actually invert rather than darken, or a "selected" chip stops reading as selected the moment the page around it is already dark.

Status

17 tokens

Each has a tint (the panel) and an ink (the copy on it), measured as a pair so a soft notice reads the same way in both themes.

Favourite

3 tokens

A rose, not the destructive red: marking a favourite is not an error state. On the card it paints inside the marker chip, whose surface flips with the theme, so this flips with it - deep enough to hold on a near-white chip here, lifted in dark.

Accent well

3 tokens

Empty-state icon wells. Violet, deliberately a step off the indigo brand, which is how it shipped.

Extended ramps

14 tokens

Further steps in ramps the families above already own, for the places that need one between the named roles. Indigo at five levels, two cool greys, a warm caution tint and a deeper critical red. …

Brand mark

3 tokens

The wordmark's two colours: a near-black "my" and the brand indigo on "Spoils". The neutral inverts in dark; the indigo follows --brand into whatever --brand is there. --logo-brand is --brand, exactly, and has no value of its own. …

Avatar fallback

3 tokens

The warm gradient behind a user's initials. Kept as its own token rather than reusing --surface-inset: it needs enough chroma to read as a portrait slot, not as a well.

Trophy

2 tokens

The one card in the product that holds what the *reader* put there: "My experience" on the spoil sheet. Nothing else takes it - a panel that is merely celebratory is not this, and the moment a second kind of panel wants it, one of the two has the wrong token. …

Elevation

10 tokens

In light the shadow carries the lift and the ring is a faint warm edge. In dark that inverts: the ring carries it, the shadow only deepens.

Overlays

9 tokens

Scrims and the two surfaces that do not flip. Dialogs dim the page and no more; a trailer removes it. --surface-cinema and --surface-artwork are the only two values in the product that are the same in both themes, and each has a paragraph in the stylesheet saying why.

Category derivation

5 tokens

Components set --cat-accent (and --cat-surface) inline from the one category catalogue. Everything else is derived here, so a category's colour is authored exactly once. The lightness clamp is what makes every badge AA by construction: …

shadcn compatibility layer

16 tokens

Not a palette. This is the surface shadcn/ui's primitives read from, restored to its pre-theming values verbatim and deliberately held still: aliasing these onto the semantic tokens above shifted defaults everywhere - #0a0a0a body ink became #111, neutral greys moved 2-11 units - so light keeps the originals and only .dark does the aliasing. Nothing new should be built against these names; they exist so that what was built against them does not move.

Radius

One authored value, --radius at 0.625rem, and six multipliers of it. Nothing here is themed - a corner is a shape rather than a colour - so these are shown once.

Elevation, as a pair

A shadow and a ring per step, and the two themes divide the work differently: light lifts with the shadow and edges with the ring, dark reverses it, because a drop shadow stops reading once the page behind it is dark. Each row is the same two tokens painted twice.

Motion

Nine curves, plotted from their own control points. The two figures are computed here rather than quoted: how much of the duration has passed when 90% of the travel is done, which is what a motion reads as, and the pass beyond the target, which is what it reads as having weight.

What each one is for is a different question and is settled elsewhere - MOTION.md for the rules and the list of what never moves, and /preview/motion for the tiers, which writes its values out as literals on purpose so it can disagree with the stylesheet.

Spacing

Six steps, named for the job rather than the size, and direction-agnostic because they are gap-*. The brand stripe in each row is the gap itself, cut out of a solid strip by three blocks that are the same size in every row, so the only thing that changes down the column is the distance being named. Each one is laid out with its own class, so these are distances rather than pictures of distances. pagePaddingX is deliberately off this scale, since it is measured against the edge of the screen rather than the content rhythm.

Type

Eight named roles, each a bundle of face, size, weight, tracking and ink, named for where it goes rather than how big it is. The three faces are below them.

Not yet a scale. There is no named size step between these bundles, so a line that is not one of these roles still reaches for a number of its own. That is the one family on this page with a gap in it.

The three faces

Focus, and the states

The focus ring is one class - focusRing, which is --brand-ring at 2px and on focus-visible only. Tab through the controls below, in either pane.

These are the product's own Button and IconButton, not look-alikes. The skeleton is the state worth seeing twice: it is translucent ink rather than a colour, so it is the same step from whichever surface is behind it.

light

neutral, disabled, critical

--surface-card

--surface-inset

doneworkingwrong

dark

neutral, disabled, critical

--surface-card

--surface-inset

doneworkingwrong