Skip to content

Colour Palette

"One accent colour, used with purpose. Everything else is neutral."

Mode Architecture

The site defaults to light mode and offers a dark mode toggle. Colours are defined as CSS custom properties on :root (light) and [data-theme="dark"] (dark).

:root { /* Light mode — default */ }
[data-theme="dark"] { /* Dark mode — toggled */ }

Theme is determined by: 1. localStorage.getItem('theme') (user's explicit choice) 2. prefers-color-scheme media query (OS preference) 3. Fallback: light


Neutral Scale

The backbone of the system. Every background, text colour, and border comes from this scale.

Light Mode (Default)

Token Value Swatch Usage
--bg-page #FAFAFA #FAFAFA Page background
--bg-surface #FFFFFF #FFFFFF Cards, panels, modals
--bg-elevated #F5F5F5 #F5F5F5 Sidebar, secondary surfaces
--text-primary #1A1A1A #1A1A1A Headings, important text
--text-secondary #404040 #404040 Body text
--text-tertiary #737373 #737373 Descriptions, meta text
--text-muted #A3A3A3 #A3A3A3 Timestamps, placeholders
--border #E5E5E5 #E5E5E5 Default borders
--border-emphasis #D4D4D4 #D4D4D4 Hover/active borders

Dark Mode

Token Value Swatch Usage
--bg-page #0A0A0A #0A0A0A Page background
--bg-surface #141414 #141414 Cards, panels
--bg-elevated #1A1A1A #1A1A1A Secondary surfaces
--text-primary #F5F5F5 #F5F5F5 Headings
--text-secondary #D4D4D4 #D4D4D4 Body text
--text-tertiary #A3A3A3 #A3A3A3 Descriptions
--text-muted #737373 #737373 Timestamps
--border #262626 #262626 Default borders
--border-emphasis #404040 #404040 Hover/active borders

Accent Colours

Primary — Indigo

The one accent colour for the entire site. Used for CTAs, links, active states, focus rings.

Token Light Dark Usage
--accent #6366F1 #818CF8 Links, CTAs, active tabs
--accent-hover #4F46E5 #6366F1 Hover states
--accent-subtle #EEF2FF rgba(99,102,241,0.1) Accent backgrounds (selected row, active sidebar item)

Why indigo? Linear uses #5e6ad2, Stripe uses #635bff. Indigo #6366F1 sits between both — professional, calm, works beautifully in both light and dark modes. Not warm, not cold. Not attention-seeking, not invisible.

Semantic Colours

Used only for their specific meaning. Never as decoration.

Token Light Dark Meaning
--success #22C55E #4ADE80 Pass, correct, saved, healthy
--warning #EAB308 #FACC15 Attention needed, expiring, new
--error #EF4444 #F87171 Failed, wrong, error, critical
--info #60A5FA #93C5FD References, docs, informational
--teal #14B8A6 #5EEAD4 Scenarios, instructions, processes

Badge Text Colours (Added Z4d)

Text colours for use ON tinted semantic backgrounds. Dark in light mode (contrast on light tints), light in dark mode (contrast on dark tints). Defined in body.zen-migrated.

Token Light Dark Usage
--badge-red #B91C1C #FCA5A5 Text on red-tinted badges
--badge-orange #C2410C #FDBA74 Text on orange-tinted badges
--badge-amber #92400E #FDE68A Text on yellow/amber-tinted badges
--badge-green #166534 #86EFAC Text on green-tinted badges
--badge-blue #1D4ED8 #93C5FD Text on blue-tinted badges
--badge-purple #6D28D9 #C4B5FD Text on purple-tinted badges

What's NOT in the palette

These colours no longer exist anywhere in the system:

  • #66ffff (neon cyan)
  • #ff66ff (neon magenta)
  • #39ff14 (neon green)
  • #ffe600 (neon yellow)
  • ❌ Any of the 53 per-tool accent colours from tool_colours.toml
  • ❌ Any gradient fills or glow colours
  • ❌ Any color-mix() derived colours
  • ❌ Any rgba(255, 102, 255, ...) magenta variants

WCAG Contrast Compliance

All colour combinations must pass WCAG AA (4.5:1 for normal text, 3:1 for large text).

Combination Ratio Status
--text-primary on --bg-page (light) 15.3:1 ✅ AAA
--text-secondary on --bg-page (light) 8.6:1 ✅ AAA
--text-tertiary on --bg-page (light) 4.8:1 ✅ AA
--accent on --bg-page (light) 4.6:1 ✅ AA
--text-primary on --bg-page (dark) 17.4:1 ✅ AAA
--text-secondary on --bg-page (dark) 12.2:1 ✅ AAA
--accent (dark) on --bg-page (dark) 6.8:1 ✅ AA

CSS Implementation

:root {
  /* Backgrounds */
  --bg-page: #FAFAFA;
  --bg-surface: #FFFFFF;
  --bg-elevated: #F5F5F5;

  /* Text */
  --text-primary: #1A1A1A;
  --text-secondary: #404040;
  --text-tertiary: #737373;
  --text-muted: #A3A3A3;

  /* Borders */
  --border: #E5E5E5;
  --border-emphasis: #D4D4D4;

  /* Accent */
  --accent: #6366F1;
  --accent-hover: #4F46E5;
  --accent-subtle: #EEF2FF;

  /* Semantic */
  --success: #22C55E;
  --warning: #EAB308;
  --error: #EF4444;
}

[data-theme="dark"] {
  --bg-page: #0A0A0A;
  --bg-surface: #141414;
  --bg-elevated: #1A1A1A;
  --text-primary: #F5F5F5;
  --text-secondary: #D4D4D4;
  --text-tertiary: #A3A3A3;
  --text-muted: #737373;
  --border: #262626;
  --border-emphasis: #404040;
  --accent: #818CF8;
  --accent-hover: #6366F1;
  --accent-subtle: rgba(99, 102, 241, 0.1);
  --success: #4ADE80;
  --warning: #FACC15;
  --error: #F87171;
}