Theme engine
One four-field seed — a background, an accent, a contrast scalar, and an optional mode — becomes the entire token set: every surface, text tier, border, control, and status colour, derived in OKLCH, solved for legibility on APCA, floored on WCAG, and gamut-mapped into sRGB. Components touch only the roles, so the whole interface re-themes from that one seed with no rebuild. Every graph below is the engine explaining itself — live output recomputed from the active theme, so reach for the System Controls and watch them move.
Why OKLCH
Colour arithmetic in hsl() lies about the two things a theme depends on most. Its hue axis drifts — push a deep blue toward white and it slides purple — and "50% lightness" is nowhere near half as bright for a saturated colour as for a gray. Build a contrast ramp on those coordinates and the steps look uneven and the hues wander.
The engine works entirely in OKLCH — Björn Ottosson's Oklab in cylindrical form — where a step in lightness is a step in perceived lightness and a hue holds steady as chroma and lightness change. That perceptual grounding is the whole foundation: it is what lets a single contrast number walk every tier off the seed by a predictable perceptual distance.
Oklab is built scale-independent for HDR, so for a screen with a defined reference white it takes one correction — a lightness toe that lifts the mid-tones back to where the eye puts them:
Perceptual uniformity has a price, though: the sRGB gamut is a wildly irregular shape in OKLCH. Maximum chroma is not a constant — it depends on both hue and lightness, so the most saturated blue a screen can show sits far higher than its most saturated yellow.
The sRGB gamut at lightness 0.7, every hue filled to its cusp — the most saturated colour a display can reach there. Blues and magentas tower; yellows and greens top out early. "Maximum chroma" is meaningless without a hue, which is exactly why the engine never assumes a fixed one. The dashed line marks the active accent's hue.
The seed
A theme is four fields. The background sets the canvas and the entire neutral foundation; the accent is the one reserved chromatic signal; contrast is clamped to [0,100] and calibrated at 30 — the reference where one surface step is exactly one magnitude, not a ceiling; and mode optionally forces light or dark instead of letting the canvas decide. The contrast number becomes a signed, magnitude-bearing gain that every tier reads:
Surfaces use the unsigned magnitude (they always rise toward white); borders, text, and accents keep the signed gain (they walk down in light, up in dark).
Gamut mapping
Because the gamut boundary is so irregular, every colour the engine emits is mapped into sRGB at the moment it serializes — chroma is reduced toward the cusp while lightness and hue are held, so a vivid accent lands as the most saturated in-gamut colour of its own hue, never a clipped flat block. Ottosson gives the ceiling in closed form from the cusp:
where and encode the cusp for hue . Reducing chroma to that ceiling preserves the identity of the colour; clipping RGB channels would shift its hue.
The surface ramp
Surfaces climb toward white in both modes — they do not darken in light mode, they reserve a sliver of white headroom and rise into it. A dark canvas lifts by fixed OKLCH-lightness offsets scaled by the magnitude; a light canvas has only that narrow headroom to work in, so it distributes fractions of the headroom through a saturating response:
The factor is the trick. As contrast climbs, every rung pushes further toward its share of the headroom but never reaches it — so the light ramp widens with contrast instead of clamping its top rungs together into one flat white.
The six light surface rungs as the contrast knob sweeps 0 → 100, live off the active seed. Each rung fans out yet asymptotes below the dashed white ceiling — turn the contrast all the way up and the ramp spreads apart, it does not collapse into white.
Contrast-solved text, floored on WCAG
Text tiers are not picked by hand; they are solved. The primary tier is pure auto-contrast — white or black, whichever wins, carrying half the canvas hue at chroma ≤ 0.02 so it reads as a faint tint rather than flat gray. The deeper tiers and the link are binary-searched in OKLCH to descending APCA Lc targets — mode-aware, because the same target means different things in each polarity (dark {78, 58, 38}, light {90, 81, 68}; the link 60 dark, 64 light).
That mode-awareness is forced by APCA itself, which weights the two polarities with different exponents:
So the same Lc target buys far less raw separation on a light page than a dark one. The engine answers that by flooring every solved tier on the plain WCAG ratio as well — APCA sets the hierarchy, WCAG guarantees a usable minimum on any canvas:
text-foreground
text-secondary
text-muted
text-subtle
text-link
APCA Lc plotted against WCAG ratio as a neutral text lightness sweeps a light canvas vs a dark one. Read across at any height: for the same Lc the light-canvas curve sits at a lower WCAG. That gap — and the AA floor it crosses earlier — is why the engine solves on APCA but guarantees on WCAG.
Controls are their own gentler gain
Interactive controls are a separate, modeless tier with real rest, hover, and selected steps. They lift more gently than surfaces — a secondary button references 70 against the ramp's 30, so it climbs roughly half as fast and never overpowers the panel it sits on — but their magnitude is symmetric across modes, so a light control separates exactly as far as its dark twin. The ghost (tertiary) tier rests as a near-invisible wash yet its hover and selected states keep full strength, so an icon-only ghost trigger still reads on a light page. Every state is a token shift, never an opacity hack.
Elevation is re-generation
A raised or recessed surface is not a tint — it is a full re-generation of the theme at a shifted base. The re-run injects the scope's shifted canvas as a verbatim bg-1 override and forces the mode, so it skips the white-headroom reserve and re-derives every role off that base, which is how the menu scope reaches near-white in light mode. The same roles (bg-surface, bg-control, border-border) resolve to that layer's own contrast-correct colours with zero per-component work.
The one thing the engine does not emit is the drop-shadow ladder: it emits only --noctis-engine-shadow-color, and the shadow geometry and per-layer alphas are static parts in @stridge/noctis-design-tokens, tinted via oklch(from var(--noctis-engine-shadow-color) l c h / <alpha>) — so shadow tone re-derives under every scope without the engine touching the shadows.