thermograph/frontend/DESIGN.md
Emi Griffith d6df04eab2 Subtree-merge thermograph-frontend (origin/main) into frontend/
git-subtree-dir: frontend
git-subtree-mainline: a4be7066e5
git-subtree-split: 3a98146da4
2026-07-22 22:01:11 -07:00

134 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` ~8999). 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).