# 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).
