Skip to content

Color

In review

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.

One brand anchor, expressed as a small ladder of tints and shades so fills, hovers, and borders all derive from the same hue.

cyan-300
#5ED4E8 · light
cyan-400
#22D3EE
cyan-500 · brand
#0CC0DF · accent.primary
cyan-600
#0A9BB5
cyan-700
#0891B2 · light accent

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.

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.

orange-400 · brand
#FB923C · accent.warm (dark)
orange-600
#EA580C · accent.warm (light)
warm-soft
16% · outline / notice fill

--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.

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.

canvas
#0A0B0D · ground
chrome-surface
#15171A · nav / sidebar
chat-bg / field
#181B20 · recessed
card-surface1
#2C313A · lifted
card-surface2
#333942
card-surface3
#3A414A

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 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.

success
#4CAF50
warning
#FFC107
error
#EF5350
info
#0CC0DF · cyan

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.

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.