Theme engine
The theme engine is the function that turns a four-field seed into the entire token set — every surface, text tier, border, control, and status colour derived in OKLCH and contrast-solved. Components touch only the roles, never the raw colours, so the whole UI re-themes from one seed with no rebuild.
The seed
A theme is four fields: a background, an accent, a contrast number, and an optional forced light/dark mode. The background sets the canvas and the whole neutral foundation; the accent is the reserved chromatic signal; contrast is clamped to [0,100] and calibrated at 30 — the reference where a surface step is exactly one magnitude, not a ceiling — so dialing it up or down scales how far every tier walks off the seed.
How a colour is born
There are three strata, not a numbered hierarchy. The engine emits private primitives (--noctis-engine-*) — not a graph tier, just the raw OKLCH output; the token graph maps those to intent-named semantic roles (--noctis-color-{role}); Tailwind bridges each role into the utility a component writes. Components only ever touch the roles, so swapping the seed reskins everything.
Every emitted colour is gamut-mapped into sRGB at emit time — chroma is reduced until the colour fits, while lightness and hue are preserved — so a vivid accent never clips to a flat block.
The surface ramp
Surfaces climb toward white in both modes — they do not darken in light mode; they reserve white headroom and rise into it. Lightness walks contrast-scaled steps off the seed to build the neutral ramp — the canvas, its hover, panels, and raised surfaces — lifting toward white with a faint canvas-hue chroma as it climbs; only selected and focused surfaces lean toward the accent.
Contrast-solved text
Text tiers are not picked by hand. The top 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, not flat gray; the deeper tiers and the link are APCA-solved by binary search to descending Lc targets ({78, 58, 38}, and the link to 60), so legibility is guaranteed on every seed. The link keeps its accent hue while it solves.
text-foreground
text-secondary
text-muted
text-subtle
text-link
Controls are their own gentler gain
Interactive controls are a separate, modeless tier with real rest, hover, and selected steps. A secondary button lifts roughly half as fast as a card — controls reference 70 against the surface ramp's 30 — and the ghost/tertiary tier is softened further in bright mode, so a control never overpowers the panel it sits on. Hover is a token shift, never an opacity hack.
Elevation scopes
A raised or recessed surface is a full re-generation of the theme at a shifted base, not a tint. 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 why the menu scope can reach near-white in light mode. There are two paths to the same scopes: the static build-time CSS in @stridge/noctis-design-tokens keys off a data-elevation attribute, while the runtime engine writes a generated data-noctis-theme-scope attribute and targets that selector — so a control inside a dialog, menu, or sidebar re-derives to its layer with zero per-component work.
The drop-shadow ladder is the one thing the engine does not emit: it emits only --noctis-engine-shadow-color, while 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.