Color
Nockerl color is monochrome surfaces plus one cyan anchor. The brand is carried by a
single hue (cyan #0CC0DF) over a dark-first neutral ramp. Warm tones are reserved for
status. Every color is a token; nothing downstream is hardcoded.
The cyan system
Section titled “The cyan system”One brand anchor, expressed as a small ladder of tints and shades so fills, hovers, and borders all derive from the same hue.
cyan-500 (#0CC0DF) is the brand accent, --color-accent-primary in dark theme.
The light theme steps the accent down to cyan-700 (#0891B2) so it still clears AA on a
light surface.
The secondary accent: warm, and rare
Section titled “The secondary accent: warm, and rare”A second brand hue: a warm orange that pairs with the cool cyan. Used rarely (even more sparingly than cyan) for outlines, special notices, and the occasional second action. Never a decorative wash, never on every screen. Sourced from the Android app’s agent orange.
--color-accent-warm is orange-400 (#FB923C) on dark, stepping to orange-600
(#EA580C) on light to hold AA on white, exactly as cyan steps #0CC0DF → #0891B2. The
per-theme value lives in one token (accentWarm in color.{dark,light}.json); making
the two themes identical later is a one-line change. (Named accent.warm, not
accent.secondary, because that token name is already taken by a cool categorical accent.)
Restraint is the whole point. Cyan stays forward. The orange is a seasoning, not an ingredient: aim for at most one orange element in view, and reach for it as a line or border before anything else. If a screen reads as “an orange UI,” it is wrong.
- Do: a hairline border / outline (a special-notice frame, a featured row’s edge), a single small status-disc on a special notice, the occasional outlined second action.
- Don’t: filled orange buttons, orange icons or text as general accents, orange chips, or orange anywhere it competes with cyan for attention.
Semantic surface roles (dark theme)
Section titled “Semantic surface roles (dark theme)”Surfaces follow one law: ground is the darkest layer; cards lift off it. The canvas is darkest, cards are a clear step above (lighter + shadow + catch-light), and chrome (nav / sidebar) sits between.
Text-on-surface roles
Section titled “Text-on-surface roles”Text binds to an on-* role rather than a raw grey, so contrast is guaranteed against its
intended surface.
| Token | Value (dark) | On | Contrast |
|---|---|---|---|
--color-on-canvas |
#E8E8E8 |
canvas #0A0B0D |
~14.8:1, AAA |
--color-on-canvas-muted |
#A0A0A0 |
canvas #0A0B0D |
~6.9:1, AA |
--color-on-card |
#E8E8E8 |
card #2C313A |
~9.4:1, AAA |
--color-on-card-muted |
#A0A0A0 |
card #2C313A |
~4.4:1, AA (large / non-body) |
Status hues as functional signals
Section titled “Status hues as functional signals”Status colors are functional (success, warning, error, info), paired with an icon and text (never color alone) so they stay legible to color-blind users. The warning amber here is a status signal, deliberately distinct from the warm brand accent (orange) introduced above for emphasis.
The AA contrast rule
Section titled “The AA contrast rule”Contrast is checked on every state (including disabled and loading) in both dark and light themes:
- Text ≥ 4.5:1 (≥ 3:1 for large text).
- Non-text (borders, icons, focus indicators) ≥ 3:1.
- Disabled is still visible. Fading a control below 3:1 until it vanishes is a defect: disabled must read as clearly inert, not invisible.
- Never color alone. Errors are color + icon + text; status dots pair with a label.
Tokens
Section titled “Tokens”The full color token set is published in @dizyx/nockerl-tokens as CSS custom properties
(--color-*) under :root (light) and .dark (dark). Core primitives (the cyan ramp,
neutral ramp, status hues) live in the core layer; the surface, text, accent, and status
roles live in the semantic layer and reference the primitives. See the
Token hierarchy for how the layers compose.