# Thermograph — design system The project-specific visual spec. For general aesthetic guidance the global `frontend-design` skill still applies; **this file is the source of truth for Thermograph's own tokens and conventions.** When they disagree, this file wins. The authoritative values live in **`static/style.css`** (a single hand-written stylesheet — no framework, no build step). This doc describes *how* to use them; it deliberately avoids copying hex values that could drift. When in doubt, read the `:root` block at the top of `style.css`. ## Tokens (never hardcode) Every color is a CSS custom property in `static/style.css` `:root` (top of the file), with a `@media (prefers-color-scheme: light)` override right below it. **Always use `var(--token)`; never paste a raw hex into a rule or an inline style.** New surfaces get their color from the existing tokens so light mode and future retints keep working for free. Structural tokens: `--bg`, `--surface`, `--surface-2`, `--border`, `--text`, `--muted`, and the warm orange brand `--accent` (`#f0803c`). The light-mode block remaps the first six; the grade scales below are shared across both schemes. ### Grade palettes These encode meaning, not decoration — keep their order and midpoints intact: - **Temperature** — a 9-step diverging scale, cold → green → hot, chosen to be colorblind-safe: `--rec-cold` `--very-cold` `--cold` `--cool` **`--normal` (green midpoint)** `--warm` `--hot` `--very-hot` `--rec-hot`. The two `--rec-*` ends are the "Near Record" danger tiers — deliberately dark and saturated. - **Precipitation** — `--dry` plus `--wet-1`…`--wet-9` (light green → teal → deep navy), with `--wet-5` the scale midpoint. - **Seasons** — `--season-winter/-spring/-summer/-fall` for month/season chrome. The inline-SVG charts (`static/chart.js`, and the SVG strings built in `app.js`) pull from these same tokens, so a chart and its legend never diverge. ## Typography `font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif`. **Inter is not self-hosted or imported** — it renders only where the OS has it, otherwise the stack falls back to `system-ui`. Do not add a webfont link without a deliberate decision (it's a network dependency on every page). - Headings are tight: `h1` is 22px with `letter-spacing: -0.02em`. - Section/eyebrow labels are small, uppercase, muted, and letter-spaced (~12px, `letter-spacing: .04–.05em`, `color: var(--muted)`). - Body `line-height: 1.45`. ## Components Match the existing recipes rather than inventing new ones: - **Panels** — `.panel`: `--surface` background, `1px solid var(--border)`, `border-radius: 14px`, generous padding. The primary content container. - **Grade cards** — `.normal-card`: `--surface-2`, ~11px radius, the big value tinted by its grade color via `color-mix(in oklab, …)`; hover lifts the border to `--accent`. - **Buttons** — accent background, dark text, `border-radius: 10px`, `font-weight: 600`. Toggle chips (e.g. `.today-chip`) go outlined → filled when active. - **Segmented toggles** — unit (°C/°F) and metric switches; the active segment is tinted by that metric's grade color (tmax → warm/hot reds, tmin → cold blues, precip → wet blues). - **Inputs** — `--surface` background, `1px solid var(--border)`, and **`font-size: 16px` minimum** — smaller text makes iOS zoom on focus. This is a hard rule, not a preference. - **Charts** — bespoke inline SVG (percentile fan + median + value trace + pointer-driven `.chart-tip`). No canvas, no charting library. - **Map** — Leaflet in the shared modal picker (`mappicker.js`). ## Layout & responsive Mobile-first: the base stylesheet is the phone layout; wider screens layer on via `min-width` queries. Design and test in this order. - **Phone** — single column. Primary phone breakpoint is **640/641px**; a few tweaks at 560px. Keep ~44px touch targets and use pointer (not mouse-only) events. The mobile header folds into a hamburger menu. - **Large monitors** — the content column widens in real steps, it does **not** stay a centered 1200px strip: `main` grows to 1440px at **1680px**, 1640px at **2400px**, 1880px at **3400px** (`style.css` ~89–99). Per-page grids (normals, calendar months, day cards) flow into the extra room; charts scale with it. - **Both color schemes** — dark is the default; light comes from `prefers-color-scheme`. Every change must look right in both. - Honor `prefers-reduced-motion: reduce` — gate non-essential animation behind it. **Validate every visual change at 390 / 800 / 1920 / 2560 / 3840px in both light and dark.** These viewport widths straddle the breakpoints above (phone, tablet, 1440p, 2K, 4K). Use `make shots` (below) to capture the full matrix. ## Conventions - **Metric order is always `Precip · High · Low`** — chart legend, normal cards, day rows, exported tables, everywhere. - **Grades are relative, never absolute.** Use the percentile tier names ("Above Normal", "High", "Near Record"), never absolute-temperature words ("hot", "warm", "cold") — the same reading is Above Normal in a cool climate and Below Normal in a hot one. This is a percentile against each place's own climate history, not a thermometer reading (see `thermograph-backend`'s `data/grading.py`'s `pct_ordinal()`, which `static/shared.js`'s `pctOrd()` mirrors — see `CLAUDE.md`). - The two chart temperature lines are labeled **"Daily high / Daily low"** so they aren't confused with the **"High / Low"** percentile tiers. ## Viewing & iterating — `make shots` There is no design without seeing it rendered. To view the running app across the full breakpoint matrix, serve this repo's app (see `README.md`/`CLAUDE.md` for the `uvicorn` invocation) and run the screenshot sweep: ```sh python3 -m venv .venv && .venv/bin/pip install -r requirements.txt # if not already set up .venv/bin/pip install -q -r tools/requirements.txt .venv/bin/python -m playwright install chromium .venv/bin/python tools/shoot.py ``` `tools/shoot.py` drives headless Chromium over the served app and writes PNGs to `.screenshots/` (gitignored), named `{page}@{width}-{scheme}.png` (e.g. `index@390-dark.png`, `city@3840-light.png`). Read those PNGs back to see the result, adjust `static/style.css`, and re-shoot. Narrow the matrix while iterating on one thing: ```sh .venv/bin/python tools/shoot.py index --width 390 --scheme dark ``` Point it at a different server with `SHOTS_BASE` (default `http://127.0.0.1:8137/thermograph` — override to match wherever this repo's `app.py` is actually serving, e.g. `http://127.0.0.1:8080` for a local run without `THERMOGRAPH_BASE` set).