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

6.6 KiB
Raw Blame History

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:

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:

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