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

4.1 KiB

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