Files
brain-of-reese/frontend/assets/themes/README.md
T
ducoterra c738105932 feat(web): customizable placeholder, footer text, and color theme via BOR_* env vars
BOR_INPUT_PLACEHOLDER / BOR_FOOTER_TEXT / BOR_THEME (+ the indigo.css example theme); authoring guide: frontend/assets/themes/README.md, docs: README 'Customizing the look'.
2026-09-01 12:04:06 -04:00

78 lines
4.1 KiB
Markdown

# 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=<file>` (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
`<link rel="stylesheet" href="/assets/themes/<file>">` 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/…`.