# Color tokens Color is defined in two steps: raw OKLCH ramps, then semantic tokens that point at them. App CSS reads the semantic tokens. Everything here lives in `themes/default.css` inside `@layer tokens`. ## Why OKLCH The ramps are authored in OKLCH so that the same numbered step reads at the same perceived weight across every ramp. `--danger-500` and `--success-500` sit at the same lightness, so a red badge and a green badge feel like the same object in different colors. A couple of brand-exact anchors are pinned as hex (`--neutral-900` is the Elements navy `#102C42` exactly). ## The neutral ramp A single cool, navy-tinted gray ramp (hue 240, low chroma), from `--neutral-50` (near-white) to `--neutral-950` (near-black), plus `--white` and `--black`. `--neutral-900` is pinned to `#102C42`. ```text --white --black --neutral-50 --neutral-100 --neutral-200 --neutral-300 --neutral-400 --neutral-500 --neutral-600 --neutral-700 --neutral-800 --neutral-900 --neutral-950 ``` ## The status ramps Four ramps, each a full 50–950 scale on the same lightness curve as the neutral ramp: - `--success-*`: green, hue 150 - `--warning-*`: amber, hue 75 - `--danger-*`: red, hue 25 - `--info-*`: cyan, hue 200 (deliberately distinct from a typical blue accent) ## Semantic tokens These are what you consume. Each flips automatically in dark mode. **Surfaces** (`--bg*`), page and panel backgrounds: | Token | Role | |---|---| | `--bg` | Page background (white in light) | | `--bg-soft` | Slightly raised surface | | `--bg-muted` | Muted fill (code, chips) | | `--bg-panel` | Panel background | | `--bg-elevated` | Elevated surface (cards, popovers) | | `--bg-canvas` | Recessed canvas behind panels | | `--bg-inverse` | Inverted surface (`
` blocks) |
**Ink** (`--ink*`), text colors, high to low emphasis:
| Token | Role |
|---|---|
| `--ink` | Primary text |
| `--ink-soft` | Secondary text |
| `--ink-dim` | Tertiary / captions |
| `--ink-faint` | Placeholder-level |
| `--ink-inverse` | Text on an inverse surface |
**Rules** (`--rule*`), border colors:
| Token | Role |
|---|---|
| `--rule` | Default border |
| `--rule-soft` | Faint divider |
| `--rule-strong` | Emphasized border |
**Accent** (`--accent*`), the brand axis, monochrome by default:
| Token | Role |
|---|---|
| `--accent` | The brand color (defaults to `--neutral-900`) |
| `--accent-deep` | Hover / pressed / gradient end |
| `--accent-soft` | Tinted accent backgrounds |
| `--accent-ink` | Text/icons on an accent fill |
| `--accent-edge` | Accent border on tinted surfaces |
`--accent` points at `--neutral-900` by default, so an unbranded app is
black-and-white. Repointing this block is the entire branding API. See
[branding](../theming/branding).
## Status triples
Every status ramp collapses to a four-token set built for alerts, badges, and
toasts (a soft background, an ink color, a solid color, and an edge):
```css
.alert {
background: var(--success-soft);
color: var(--success-ink);
border: 1px solid var(--success);
}
```
The set exists for each status:
| Solid | Soft bg | Ink | Edge |
|---|---|---|---|
| `--success` | `--success-soft` | `--success-ink` | `--success-edge` |
| `--warning` | `--warning-soft` | `--warning-ink` | `--warning-edge` |
| `--danger` | `--danger-soft` | `--danger-ink` | `--danger-edge` |
| `--info` | `--info-soft` | `--info-ink` | `--info-edge` |
The accent axis follows the same shape (`--accent` / `--accent-soft` /
`--accent-ink` / `--accent-edge`), which is why callouts and pills have an
`is-accent` variant alongside the status ones.
Note: the solid status colors are tuned for text-on-tint and borders, where the
ink color is the readable foreground. A **filled** danger button is a separate
case. It sets `color: var(--white)` on a `--danger` fill deliberately, rather
than using `--danger-ink` (which in dark mode is a light text tint, not a fill
foreground). Don't assume `--{status}-ink` is always a valid text color on a
solid `--{status}` fill.
## Focus
Focus is one ring spec, consumed by every focusable component:
| Token | Role |
|---|---|
| `--focus` | The ring color (defaults to `--accent`) |
| `--focus-ring` | The full `box-shadow` ring |
| `--focus-ring-danger` | The ring for destructive controls |
```css
:focus-visible { box-shadow: var(--focus-ring); }
```
## Dark mode
`themes/default.css` remaps every semantic token above under `@media
(prefers-color-scheme: dark)` and again under `:root[data-theme="dark"]`. The
ramps don't move; the semantic pointers do (`--bg` → `--neutral-950`, `--ink`
→ `--neutral-50`, and so on). Anything built from semantic tokens is
dark-ready for free. See [dark-mode](../theming/dark-mode).