# Themes — authoring guide (phase 62) A theme is a small CSS file that overrides the `:root` palette variables. That is the entire mechanism — no component CSS is theme-aware, every color in the app reads a `--*` variable, so a later stylesheet wins by cascade order. ## How a theme loads 1. Set `BOR_THEME=` (a bare FILENAME, e.g. `BOR_THEME=indigo.css`). `app/config.py` validates it at startup — anything not matching `^[a-z0-9_-]+\.css$` (a path, `..`, uppercase, a missing extension) refuses to boot, naming the value (the phase-56 fail-loud house style). 2. The value rides the existing boot fetch: `GET /api/config` → `frontend/assets/brand.js` inserts `` IMMEDIATELY AFTER the `styles.css` link — later wins the cascade. 3. A theme file MISSING at runtime (typo past the validator, or the file deleted after the image was built) degrades to the built-in theme — `brand.js` warns in the console, the page never breaks (the loadHealth house style, A5). 4. UNSET (`BOR_THEME` empty) ⇒ no link is inserted at all — the deployment renders byte-identical to the built-in dark-tech palette. Loading is opt-in via the env var, never by directory scanning. ## The variables A theme overrides the **8 identity variables** in a single `:root` block. Built-in values (from `frontend/assets/styles.css`) for reference: | Variable | Built-in | Role | | -------------- | ---------- | ----------------------------------------------------------- | | `--bg` | `#0f0a0a` | page background (text on it: `--ink`) | | `--surface` | `#1a0f0f` | cards, panels, code blocks (text on it: `--ink`) | | `--ink` | `#f0e6e6` | primary text | | `--ink-soft` | `#b8a8a8` | secondary text (5.1:1 on `--surface`) | | `--line` | `#2d1a1a` | decorative 1px borders (no contrast obligation) | | `--brand` | `#f43f5e` | brand accent — buttons, links (text ON it is `--bg`) | | `--brand-soft` | `#2d0a0a` | brand-tinted surface (chips, hover washes) | | `--brand-ink` | `#fca5a5` | brand-tinted text (9.0:1 on `--surface`) | The **semantic families are deliberately NOT identity** — do not override them: `--accent-*` (deflection amber), `--ok-*` (success green), `--err-*` (error red) encode *states*, and they are already AA in the built-in theme. A theme that keeps them stays honest: your indigo app still tells success from error. ## Rules - **Filename:** `^[a-z0-9_-]+\.css$` — lowercase, bare filename, in this directory. The server validator rejects anything else at startup (naming the value), so keep the env var and the filename in lockstep. - **One `:root` block.** No selectors, no `@media`, no other declarations — the file overrides variables and nothing else (the cascade does the rest). `indigo.css` is the reference shape. - **Every text/background pair ≥ 4.5:1** (AGENTS.md rule 5, WCAG 2.1 AA). The pairs that matter: `--ink` on `--bg` and on `--surface`, `--ink-soft` on `--surface`, `--bg` on `--brand` (the text on brand buttons is the DARK background ink — that is the pattern), and `--brand-ink` on `--surface`. - **Never white-on-brand.** The built-in documents the trap: white on `#f43f5e` is 3.7:1 — it fails. Pick a `--brand` whose luminance carries the dark `--bg` ink at ≥ 4.5:1 (indigo.css: 6.5:1). - Keep `--line` close to `--surface` (a 1px step, not a wall) — the layout reads by surfaces, not borders. ## Deployment - **Dev:** works immediately — the file is served from the static dir (`frontend/`, `BOR_STATIC_DIR`), so drop the file in, set `BOR_THEME`, restart uvicorn. - **Container:** rebuild the image. Stage 1 ships the WHOLE directory (`cp -r ./assets/themes /out/assets/themes` — no per-file esbuild), so a new or edited theme file needs **no Containerfile change** (A7): whatever is in `frontend/assets/themes/` at build time is what the image serves at `/assets/themes/…`.