theme · git:20260809.db5dbbd · 2026-08-09 · sha256 80adea9951af149f

theme git:20260809.db5dbbdA

Immutable. This exact content is served forever at /api/v1/blob/80adea9951af149f.

---
name: theme
description: "Preview, validate, and apply color palettes. Use when changing colors, testing dark/light mode, or evaluating a new palette. Trigger: theme, colors, palette, dark mode, contrast, color scheme."
metadata:
  type: process
  invocation: both
  practice: null
---

# Theme

Preview, validate, and apply color palettes to the lesson UI.

## Workflow

### 1. Preview a palette

```bash
python tools/theme-preview.py --palette palettes/purple-night.json
```

Generates `.scratch/theme-preview.html` showing all UI elements with the palette applied, plus a contrast validation table. Open it in a browser or screenshot for analysis.

### 2. Validate contrast

The tool prints a contrast report to stdout:
```
✓ text on bg: 14.2:1 (AAA) — need 7.0:1
✓ text-muted on elevated: 5.1:1 (AA) — need 4.5:1
✗ text-faint on surface: 2.8:1 (FAIL) — need 4.5:1
```

Fix any failures before applying. Common fixes:
- Lighten the text token
- Darken the background token
- Use a less-saturated accent

### 3. Compare palettes

Generate previews for multiple palettes and compare:
```bash
python tools/theme-preview.py --palette palettes/purple-night.json --output .scratch/preview-purple.html
python tools/theme-preview.py --palette palettes/zinc-dark.json --output .scratch/preview-zinc.html
```

### 4. Get CSS snippet

```bash
python tools/theme-preview.py --palette palettes/purple-night.json --css
```

Prints the `:root { --bg: ...; }` block ready to paste into `style.css`.

### 5. Apply

Copy the CSS variables into `assets/style.css` `:root` block. Then verify:
```bash
mise run visual-qa
```

## Palette Format

Palettes live in `palettes/*.json`:

```json
{
  "name": "Purple Night",
  "mode": "dark",
  "tokens": {
    "bg": "#13111C",
    "bg-elevated": "#1C1A2E",
    "bg-surface": "#252340",
    "text": "#CDD6F4",
    "text-muted": "#9399B2",
    "text-faint": "#6C7086",
    "accent": "#CBA6F7",
    "link": "#89B4FA",
    "border": "#313244",
    "code-bg": "#1E1E2E",
    "success": "#A6E3A1",
    "warning": "#F9E2AF",
    "error": "#F38BA8",
    "callout-bg": "#1E293B",
    "key-concept-bg": "#2D1F0E"
  }
}
```

## Contrast Rules

| Pair | Minimum | Why |
|------|---------|-----|
| text on bg | 7:1 (AAA) | Extended reading — body text must be crisp |
| text on elevated | 7:1 | Tray panel body text |
| text-muted on bg | 4.5:1 (AA) | Secondary text still readable |
| text-muted on elevated | 4.5:1 | Common failure — always test this |
| text-muted on surface | 4.5:1 | Tooltips, popovers — the #1 failure point |
| link on bg | 4.5:1 | Interactive elements distinguishable |
| accent on bg | 4.5:1 | Headings readable |

## Design Principles (dark mode)

- Backgrounds: single hue family (purple ~250°), differentiated by lightness only (7%→20%)
- Text: off-white with slight blue/lavender tint — never pure #fff (causes halation)
- Accents: desaturated pastels (200-400 weight tones from light mode)
- Semantic colors: success/warning/error at reduced saturation
- Elevation = lighter (not shadowed) — dark mode inverts the depth model
- Body font weight: 500 (medium) in dark mode to counter irradiation thinning