Token system

Tier 1/Tier 2 OKLCH tables for both themes and the drift-from-canonical reconciliation notes.

The Hypercolor docs theme runs Luminary β€” the same design system as the web UI, ported to Zola/SCSS. Tokens are the contract that makes a theme swap a single attribute flip and keeps both dark and light modes internally consistent.

The authoritative source of token values is the CSS, not this page. When this page and the CSS disagree, fix this page.

crates/hypercolor-ui/tokens/primitives.css   Tier 1: raw OKLCH primitives
crates/hypercolor-ui/tokens/semantic.css     Tier 2: intent-mapped, swapped per theme
docs/sass/_variables.scss                    Docs theme port of Tier 1 + Tier 2

πŸ”—Architecture

Luminary uses three tiers. Only Tier 2 changes per theme.

Tier 1  Primitive    Raw OKLCH values. Constant across themes.
                     Declared under @theme in primitives.css (app) or as SCSS
                     variables + CSS custom properties in _variables.scss (docs).
                     Components never reference these directly.

Tier 2  Semantic     Intent-mapped tokens. Swapped per theme via [data-theme].
                     Components reference these β€” var(--surface-base) not var(--color-void-1).

Tier 3  Utilities    Tailwind utility classes in the app (bg-surface-base, text-fg-primary).
                     In the docs theme, components reference Tier 2 CSS vars directly;
                     there is no Tailwind build step.

Theme switching is a single attribute on <html>: data-theme="dark" (default) or data-theme="light". The docs theme stores the preference in localStorage under the key theme. The app uses hc-theme β€” these are not shared.


πŸ”—πŸ’œ Tier 1: Primitives

πŸ”—Neutral scales

Two six-step ramps, both carrying a violet undertone (hue 280) so the chrome never reads as dead gray. void serves dark surfaces; cloud serves light.

Chroma falls as lightness rises β€” deep surfaces carry more violet, bright surfaces almost none. This is intentional: the dark UI reads rich, the light UI reads clean.

Step--color-void-N (dark)--color-cloud-N (light)
1oklch(0.110 0.020 280)oklch(0.985 0.005 280)
2oklch(0.130 0.018 280)oklch(0.960 0.008 280)
3oklch(0.155 0.016 280)oklch(0.930 0.010 280)
4oklch(0.185 0.014 280)oklch(0.890 0.012 280)
5oklch(0.220 0.012 280)oklch(0.840 0.014 280)
6oklch(0.280 0.010 280)oklch(0.780 0.016 280)

πŸ”—SilkCircuit accent palette

These are raw named primitives. In the UI they are consumed through semantic tokens (--accent, --status-*), never directly. The legacy hex aliases (--color-electric-purple etc.) remain in primitives.css for migration safety; do not use them in new code.

TokenOKLCHRole
--color-purpleoklch(0.65 0.30 320)Primary accent
--color-purple-hoveroklch(0.70 0.30 320)Accent hover
--color-purple-lightoklch(0.58 0.28 320)Light-mode accent base
--color-cyanoklch(0.88 0.18 175)Interactive focus (dark mode)
--color-coraloklch(0.72 0.22 350)Secondary semantic / display-face chrome
--color-yellowoklch(0.93 0.15 105)Warnings, attention
--color-greenoklch(0.85 0.22 155)Success
--color-redoklch(0.68 0.22 25)Error, danger
--color-blueoklch(0.72 0.12 260)Info

πŸ”—Motion

Easings and durations from primitives.css. The docs _variables.scss also defines --ease-smooth (cubic-bezier(0.25, 0.1, 0.25, 1)) and --duration-medium (300ms) as SCSS-only local additions; these have no counterpart in the canonical app tokens.

TokenValueSource
--ease-silkcubic-bezier(0.4, 0, 0.2, 1)canonical
--ease-springcubic-bezier(0.34, 1.56, 0.64, 1)canonical
--ease-outcubic-bezier(0, 0, 0.2, 1)canonical
--ease-smoothcubic-bezier(0.25, 0.1, 0.25, 1)docs only
--duration-fast120mscanonical
--duration-normal200mscanonical
--duration-medium300msdocs only
--duration-slow400mscanonical

πŸ”—Radii

Luminary is sharp. 8px is the largest corner in the product. Pills use 999px; circles use 50%. Nothing else.

TokenValue
--radius-sm2px
--radius-md4px
--radius-lg6px
--radius-xl8px
--radius-pill999px

πŸ”—Spacing

TokenValue
--spacing-xs4px
--spacing-sm8px
--spacing-md16px
--spacing-lg24px
--spacing-xl32px

πŸ”—Typography

Fonts load from Bunny Fonts (privacy-respecting, no Google tracking).

TokenStack
--font-sans'Satoshi', 'Inter', system-ui, -apple-system, sans-serif
--font-mono'JetBrains Mono', 'Fira Code', 'SF Mono', ui-monospace, monospace
--font-display'Satoshi', system-ui, sans-serif

Base font size is 112.5% (18px). Mono runs with ligatures off (font-feature-settings: 'liga' 0). Sans uses stylistic sets cv02 cv03 cv04 cv11 globally.


πŸ”—Tier 2: Semantic tokens

Tier 2 tokens are the ones components actually use. Each is defined in both theme blocks β€” that symmetry is what makes a theme swap a single attribute flip rather than a re-skin.

πŸ”—Surfaces (both themes)

Surfaces map the neutral ramps to elevation intent. In dark mode each step is brighter than the last; in light mode each step is darker.

TokenDarkLightUse
--surface-baseoklch(0.110 0.020 280)oklch(0.985 0.005 280)Page background
--surface-raisedoklch(0.130 0.018 280)oklch(0.960 0.008 280)Sidebar, header
--surface-overlayoklch(0.155 0.016 280)whiteCards, panels
--surface-sunkenoklch(0.185 0.014 280)oklch(0.930 0.010 280)Inputs, wells
--surface-hoveroklch(0.220 0.012 280)oklch(0.890 0.012 280)Hover states
--surface-activeoklch(0.280 0.010 280)oklch(0.840 0.014 280)Pressed, selected

πŸ”—Text (both themes)

Never pure white or pure black β€” always a violet-tinted near value.

TokenDarkLight
--text-primaryoklch(0.96 0.01 280)oklch(0.18 0.02 280)
--text-secondaryoklch(0.68 0.03 280)oklch(0.42 0.03 280)
--text-tertiaryoklch(0.52 0.04 280)oklch(0.58 0.02 280)
--text-inverseoklch(0.110 0.020 280)oklch(0.985 0.005 280)

πŸ”—Borders (both themes)

Dark borders are white at three opacity levels; light borders are black at three levels. Note the asymmetry at --border-strong: dark uses 22%, light uses 24%.

TokenDarkLight
--border-subtleoklch(1 0 0 / 0.10)oklch(0 0 0 / 0.10)
--border-defaultoklch(1 0 0 / 0.16)oklch(0 0 0 / 0.16)
--border-strongoklch(1 0 0 / 0.22)oklch(0 0 0 / 0.24)
--border-focusoklch(0.88 0.18 175 / 0.40) (cyan)oklch(0.65 0.30 320 / 0.50) (purple)

Focus color flips by theme: cyan in dark, purple in light. This is intentional and correct.

πŸ”—Accent (both themes)

Light mode desaturates the accent slightly so it does not vibrate against bright surfaces.

TokenDarkLight
--accentoklch(0.65 0.30 320)oklch(0.58 0.28 320)
--accent-hoveroklch(0.70 0.30 320)oklch(0.52 0.28 320)
--accent-mutedoklch(0.65 0.30 320 / 0.12)oklch(0.58 0.28 320 / 0.10)
--accent-subtleoklch(0.65 0.30 320 / 0.06)oklch(0.58 0.28 320 / 0.05)

πŸ”—Status (both themes)

Status tokens are darkened in light mode to maintain contrast on bright surfaces.

TokenDarkLight
--status-successoklch(0.85 0.22 155)oklch(0.55 0.18 155)
--status-erroroklch(0.68 0.22 25)oklch(0.55 0.22 25)
--status-warningoklch(0.93 0.15 105)oklch(0.58 0.14 85)
--status-infooklch(0.72 0.12 260)oklch(0.50 0.12 260)

πŸ”—Glass (Prism)

Glass tokens are for floating elements only: the search modal and the mobile sidebar drawer. Do not apply backdrop-filter to primary surfaces.

TokenDarkLight
--glass-bgoklch(0.13 0.02 280 / 0.70)oklch(0.98 0.005 280 / 0.70)
--glass-bg-denseoklch(0.13 0.02 280 / 0.85)oklch(0.98 0.005 280 / 0.85)
--glass-blur12px12px
--glass-saturate1.151.15
--glass-borderoklch(1 0 0 / 0.06)oklch(0 0 0 / 0.08)

πŸ”—Glow alpha ladder

The glow ladder provides alpha variants for accent glows without requiring inline rgba() values. Both themes define the full ladder; the base values differ to match the desaturated light accent.

TokenDarkLight
--glow-accentoklch(0.65 0.30 320)oklch(0.58 0.28 320)
--glow-accent-50oklch(0.65 0.30 320 / 0.5)oklch(0.58 0.28 320 / 0.5)
--glow-accent-70oklch(0.65 0.30 320 / 0.7)oklch(0.58 0.28 320 / 0.7)
--glow-accent-80oklch(0.65 0.30 320 / 0.8)oklch(0.58 0.28 320 / 0.8)
--glow-focusoklch(0.88 0.18 175 / 0.30)oklch(0.58 0.28 320 / 0.35)
--glow-focus-softoklch(0.88 0.18 175 / 0.15)oklch(0.58 0.28 320 / 0.18)
--selection-bgoklch(0.65 0.30 320 / 0.25)oklch(0.58 0.28 320 / 0.22)

πŸ”—Ambient reactivity

A single --ambient-hue property drives borders, scrollbars, and edge glows. In the UI, this value is updated at runtime from the active effect’s dominant hue. In the docs site it stays at the static fallback of 320 (purple), which keeps all dependent tokens on-discipline without any live source.

TokenDarkLight
--ambient-hue320 (static fallback)(inherits from dark)
--ambient-glowoklch(0.65 0.20 var(--ambient-hue) / 0.08)oklch(0.58 0.15 var(--ambient-hue) / 0.06)
--ambient-borderoklch(0.65 0.20 var(--ambient-hue) / 0.15)oklch(0.58 0.15 var(--ambient-hue) / 0.12)
--ambient-tintoklch(0.65 0.20 var(--ambient-hue) / 0.04)oklch(0.58 0.15 var(--ambient-hue) / 0.03)
--scrollbar-thumboklch(0.65 0.10 var(--ambient-hue) / 0.18)oklch(0 0 0 / 0.12)
--scrollbar-hoveroklch(0.65 0.12 var(--ambient-hue) / 0.32)oklch(0 0 0 / 0.24)

πŸ”—Noise

A near-invisible fractal SVG overlay on body::after adds tactile depth without images. Opacity drops in light mode and is removed entirely under prefers-reduced-motion.

TokenDarkLight
--noise-opacity0.0140.006

πŸ”—Brand stage

The canonical Hypercolor surface treatment for the hero. A deep magenta-velvet background that makes the brand mark read as β€œlit in a real room.”

TokenDarkLight
--brand-stage-inneroklch(0.250 0.130 295)oklch(0.92 0.06 300)
--brand-stage-outeroklch(0.100 0.020 285)oklch(0.985 0.005 280)

--gradient-brand-stage composes these as radial-gradient(ellipse at center top, var(--brand-stage-inner) 0%, var(--brand-stage-outer) 70%).


πŸ”—Reconciliation notes: docs vs canonical

The docs theme (docs/sass/_variables.scss) is a port of Luminary into Zola/SCSS. Most tokens are byte-correct against tokens/semantic.css. The drifts below were present at the time of the last reconciliation; treat any remaining mismatch as a bug in the docs theme, not the canonical tokens.

πŸ”—Already matching (confirmed in code)

Surfaces (both themes), text (both themes), accent (both themes), glass bg/border, noise opacity, and status (dark) are byte-correct against tokens/semantic.css. The SilkCircuit primitives match tokens/primitives.css; note the docs light block additionally darkens --color-cyan and --color-coral for contrast, where the app keeps Tier 1 invariant.

πŸ”—Known drift areas

Radii. The canonical values are 2/4/6/8px. An earlier docs theme used 6/10/14/20px. The _variables.scss now reflects the canonical values β€” if you see softer corners on any component, the component is applying its own radius rather than reading the token.

Border opacities. Dark mode: white at 10/16/22%. Light mode: black at 10/16/24%. Earlier versions had 6/10/16 (dark) and 6/10/18 (light) β€” both too faint. The canonical CSS is the source of truth.

Light-mode status tokens. The light block must define all four status tokens with darkened values for contrast on bright surfaces. If only the dark values are defined, light-mode callouts inherit values that fail contrast requirements.

Inline hardcoded oklch() in component SCSS. Any oklch() value in docs/sass/*.scss that is not behind a var() does not swap with the theme. These should be routed through the appropriate Tier 2 token. The most common leak is cyan oklch(0.88 0.18 175) appearing in component rules β€” see @/theme/accent-discipline.md for the full audit checklist.

Storage key separation. The app uses hc-theme in localStorage; the docs site uses theme. Do not claim they share state β€” they do not.


πŸ”—Using tokens in the docs theme

Reference Tier 2 tokens via var() in SCSS partials. Never inline a raw oklch() value in a component rule β€” it will not swap with the theme and will create a parity regression.

// Correct
.my-component {
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  color: var(--text-primary);
}

// Wrong β€” does not swap with theme, hardcodes dark values
.my-component {
  background: oklch(0.130 0.018 280);
  border: 1px solid oklch(1 0 0 / 0.10);
  color: oklch(0.96 0.01 280);
}

For glow effects, use the alpha ladder rather than constructing inline oklch() with alpha:

// Correct
.my-card:hover {
  box-shadow: 0 0 12px var(--glow-accent-50);
}

// Wrong
.my-card:hover {
  box-shadow: 0 0 12px oklch(0.65 0.30 320 / 0.5);
}

Every new component must be tested in both themes before merge. See the contributing guide at @/theme/contributing.md for the verification checklist.


  • @/theme/accent-discipline.md β€” the purple-only chrome rule, de-cyan audit, and semantic vs chrome decision table
  • @/theme/contributing.md β€” how to add a styled component without drifting from canonical
  • @/theme/_index.md β€” overview of the docs Luminary port
  • Canonical source: docs/DESIGN-SYSTEM.md in the repository root (the design intent guide; when it conflicts with the CSS, fix the guide)