Listening for events…

TerraPulse — Palette & Style Audit

Generated audit of the current user-facing site (web/, Astro 6 + Tailwind 4). Source of truth for design tokens is web/src/styles/global.css (@theme block). This document also flags every place the site bypasses the token system (inline style=, hardcoded hex, Tailwind arbitrary [#...] values) so we can drive the codebase toward "free of inline stuff."


1. How styling is wired

Concern Mechanism
Framework Astro 6 (output: 'server', Node standalone adapter)
CSS engine Tailwind 4 via @tailwindcss/vite
Token definition @theme { --color-*, --font-* } in web/src/styles/global.css
Token consumption Tailwind utilities auto-generated from theme vars (e.g. text-tp-muted, bg-forest)
Markdown rendering .prose rules hand-written in global.css

In Tailwind 4 every --color-foo in @theme becomes utilities text-foo, bg-foo, border-foo, fill-foo, etc. So the palette below is the utility vocabulary.


2. Color tokens

2a. Legacy tp-* aliases — the workhorse set

These are the most-used tokens across the site. They were remapped onto the "Verdant" palette. (Usage = count of utility occurrences in .astro/.mdx.)

Token Hex Role Usage
tp-green-dark #091E1A Deep Forest — nav, footer, headings, primary dark 71
tp-green #1AAB7A Viridian — primary accent, CTAs 38
tp-green-light #148A62 Evergreen — hover/pressed depth 3
tp-teal #22C3D4 Cyan Flare — secondary accent 37
tp-blue #3B8FD9 Arctic Blue — links, info 4
tp-bg #EDF5F2 Seafoam Mist — light page surface 22
tp-card #F6FAF8 Frost — elevated card surface 38
tp-text #091E1A Deep Forest body text 4
tp-muted #3D5C53 Verdigris — secondary text 104
tp-border #DDE9E4 Glacier — light borders 103

Pruned (2026-05-29): tp-danger, tp-success, tp-warning had zero references (danger is covered by coral).

2b. Core identity

Token Hex Name
forest #091E1A Deep Forest — used: text-forest, bg-forest
seafoam-mist #EDF5F2 Seafoam Mist — used: text-seafoam-mist

Pruned: viridian (= tp-green) and evergreen (= tp-green-light) — unused duplicates of the legacy aliases.

2c. Neutral scale (cool-green undertone)

Light surfaces / muted text. Pruned the unused dark end (abyss, kelp, lichen); the survivors below are the steps actually referenced.

Token Hex
verdigris #3D5C53
pewter #7A9B90
celadon #B8D4CA
glacier #DDE9E4
frost #F6FAF8

2d. Semantic / status

Token Hex Meaning
coral #D4453A danger / error — used: bg-coral, text-coral
status-live #22c55e live-feed dot: connected (≈ green-500)
status-down #ef4444 live-feed dot: disconnected (≈ red-500)

The ticker's connection dot (#pulse-dot) toggles between bg-status-live and bg-status-down (in PulseTicker.astro) — no inline hex.

Pruned: signal (= tp-success), marigold (= tp-warning), arctic (= tp-blue) — unused.

2e. Data-visualization ramp

Categorical ramp kept as infrastructure for future charts. Currently none are consumed (the article heroes that briefly used bg-viz-2 now derive their color from features.ts instead — see the §5 per-article note). All six remain available as bg-viz-N / text-viz-N utilities when charts need them, and tree-shake out of the compiled CSS until referenced, so they cost nothing.

Token Hex Name
viz-1 #1AAB7A Viridian
viz-2 #1B6B9A Ocean — article hero bands
viz-3 #22C3D4 Cyan Flare
viz-4 #5E8C4A Moss
viz-5 #7A7EB5 Lavender Steel
viz-6 #8CC4B4 Seafoam

2f. Event / hazard kind colors

One token per live-ticker event kind — single source of truth for PulseTicker.astro's KIND_STYLES. Change a value here and the ticker follows. Most mirror a Tailwind palette shade (exact oklch, so no drift); volcano / lightning / alert are intentional custom hues. In the component: chip bg uses /40/60, ring uses /70/90; the light text tint is a per-kind readability foreground (a Tailwind light shade), not a brand color.

Token Value Mirrors Family
event-quake oklch(70.5% 0.213 47.604) orange-500 Subterranean
event-volcano #FF4F00 — (custom: NASA "International Orange") Subterranean
event-drought oklch(55.5% 0.163 48.998) amber-700 Subterranean
event-storm oklch(62.3% 0.214 259.815) blue-500 Atmospheric
event-tornado oklch(48.8% 0.243 264.376) blue-700 Atmospheric
event-hail oklch(70.7% 0.165 254.624) blue-400 Atmospheric
event-wind oklch(62.3% 0.214 259.815) blue-500 Atmospheric
event-cyclone oklch(60.9% 0.126 221.723) cyan-600 Atmospheric
event-flood oklch(58.8% 0.158 241.966) sky-600 Atmospheric
event-lightning #FFFF00 — (custom: pure primary yellow) Atmospheric
event-flare oklch(76.9% 0.188 70.08) amber-500 Solar
event-cme oklch(79.5% 0.184 86.047) yellow-500 Solar
event-solar-wind oklch(83.7% 0.128 66.29) orange-300 Solar
event-space-weather oklch(55.5% 0.163 48.998) amber-700 Solar
event-meteor oklch(66.7% 0.295 322.15) fuchsia-500 Meteor/impact
event-fireball oklch(74% 0.238 322.16) fuchsia-400 Meteor/impact
event-forbush oklch(51.1% 0.262 276.966) indigo-600 Cosmic
event-wildfire oklch(72.3% 0.219 149.579) green-500 Biosphere
event-air-quality oklch(79.2% 0.209 151.711) green-400 Biosphere
event-system oklch(55.4% 0.046 257.417) slate-500 Fallback
event-alert #DC143C — (custom: crimson) Semantic exception

Usage example (in KIND_STYLES): bg-event-quake/40 ring-event-quake/70.

2g. Knowledge-graph palette (Data Garden)

Colors for the D3 force graphs on /garden and the lab paper navigator (/lab/[slug]/navigator) — both consume this one palette. Defined as plain :root custom properties in global.css (not @theme) — they're read by D3 via getComputedStyle(), and Tailwind tree-shakes any @theme var no utility references, which would leave the read empty. Edit a value here and a reload recolors both graphs. Verified color-identical to the prior hardcoded values.

Token Hex Mirrors Role
graph-datasource #2563eb blue-600 data-source nodes
graph-metric #d97706 amber-600 metric nodes
graph-workspace #7c3aed violet-600 paper/workspace nodes + cites edges
graph-finding #059669 emerald-600 finding nodes
verdict-positive #059669 emerald-600 positive correlation (= finding)
verdict-negative #dc2626 red-600 anti-correlation
verdict-null #9ca3af gray-400 null result
graph-accent #1B5E4B — (brand forest) buttons, search focus, hover stroke

Consumed via var(--…) in inline styles / CSS, and via a _tok() reader in the D3 script. Incidental chrome left as literals (out of palette scope): page bg #f0f4f3, panel heading #1a1a1a, hover stroke #fff, lab link #4ade80, PDF link #a855f7.


3. Typography

Stack Var Value
Sans (body/UI) --font-sans 'Montserrat', 'Inter', system-ui, sans-serif
Serif (headlines/brand) --font-serif 'Libre Baskerville', Georgia, serif
Mono (code/data) --font-mono 'JetBrains Mono', 'Fira Code', monospace

Helper classes: .brand-serif / .font-serif apply the serif stack with letter-spacing: -0.02em for display headings.

Fonts inventory

Family Source Loaded where Notes
Montserrat (400–800) Google Fonts <link> in Base.astro:95 Primary UI font
JetBrains Mono (400) Google Fonts <link> in Base.astro:95 Mono/data
Libre Baskerville (400/700) Google Fonts <link> in Base.astro Unified with the other two (was a CSS @import)
DSEG7-Classic Local woff2 @font-face in PulseTicker.astro:438 (global) Seven-segment ticker timestamps (.pulse-time); also the /dev/ticker-7seg-preview A/B variants

4. Other global style rules (global.css)

  • Focus ring:focus-visible → 2px Viridian outline + 4px rgba(26,171,122,.2) glow.
  • Overflow guardhtml, body { overflow-x: hidden; max-width: 100vw }.
  • .prose — full Markdown typography set (h1–h3 serif, links in tp-blue, blockquote border tp-green, code on tp-bg, styled tables, mobile size-downs).
  • .dropdown-menu.force-show — click-toggle override for data dropdowns.

5. Audit — where we bypass the token system

The goal is "free of inline stuff." Current state (after the ticker pass):

Bypass type Occurrences Files vs. baseline
Inline style= attributes 78 ~14 (dynamic — see note)
Hardcoded hex (#rrggbb) 72 ~18 ↓ from 146
Tailwind arbitrary [#...] 10 a few ↓ from 35

Inline style= rose by design (65 → 78): the 13 article heroes now derive their background from features.ts via featureBg(slug) (§2e note) — a dynamic, single-source style, not a hardcoded literal. The remaining inline styles are likewise dynamic: the homepage masthead colors (from features.ts) and the ticker/incident glyph transform:scale() (computed per row). None are literals to hunt down. Hardcoded hex / arbitrary [#…] are the real targets, and both are down sharply.

Done — ticker (§2f): the 3 custom ticker hues (#FF4F00, #FFFF00, #DC143C) and the 15 Tailwind-palette event shades are now --color-event-* tokens. PulseTicker.astro KIND_STYLES carries no bg-[#…]/ring-[#…] arbitrary values. Verified color-identical in the compiled CSS.

Done — garden (§2g): node-type + verdict + accent colors are now --color-graph-* / --color-verdict-* :root tokens, consumed by the D3 script and inline styles. garden.astro dropped from 34 hardcoded hex to 6 (incidental chrome). Verified in headless Chromium: 812 nodes render with the exact prior colors, zero empty fills.

Worst offenders (hardcoded hex, by count)

File Hex count Also has inline style=?
data/features.ts 26 — (intentional per-article theming, see note)
pages/data/earthquakes.mdx 10 yes
pages/dev/ticker-palette.astro 7 (dev)
pages/dev/ticker-7seg-preview.astro 7 (dev)
pages/garden.astro 6 yes (incidental chrome only — palette done, §2g)
pages/chips.astro 6 yes
pages/lab/[slug]/navigator.astro 4 yes (incidental chrome only — palette done, §2g)
pages/incidents.astro 0 colors on brand tokens; only dynamic glyph-scale inline left
pages/chips.astro 0 KIND_STYLES now uses bg-event-* tokens (mirrors ticker); dynamic glyph-scale inline left
components/PulseTicker.astro 0 real fully tokenized (event kinds §2f + status dots)
article pages (wspr-*, sanriku, fireball, …) ~30 combined a few

features.ts accentColor/bgColor are per-article hero colors — content data, deliberately bespoke per feature, not UI chrome. Leave as data.

Files with inline style=

FeatureEyebrow.astro, HomepageMasthead.astro, PulseTicker.astro, articles/cascadia-swarm-apr2026.astro, articles/index.astro, articles/reno-earthquake-swarm.astro, chips.astro, data/earthquakes.mdx, garden.astro, incidents.astro (dynamic glyph-scale only), lab.astro, lab/[slug]/navigator.astro.

Hardcoded colors that already have a token (easy swaps)

Hardcoded Token Status
#22C3D4 (masthead "Pulse") tp-teal ✅ swapped → text-tp-teal
bg-[#1B6B9A] (3 article heroes) viz-2 (Ocean) ✅ swapped → bg-viz-2
#22C3D4, #1B6B9A, #8CC4B4 in features.ts viz/tp tokens ⏭ left — per-article content theming
#9ca3af in navigator.astro verdict-null ⏭ left — it's the verdict-null semantic, not a neutral gray; belongs with a navigator-graph-palette pass (§6.1)
#FFFFFF in earthquakes.mdx frost? ⏭ left — Plotly plot_bgcolor is intentionally pure white; frost (#F6FAF8) ≠ white

Note: the generic "grays → pewter/verdigris" and "white → frost" rows from the first audit didn't survive context review — the only real occurrences are semantic (verdict-null) or genuinely white (chart bg), so swapping would change meaning or be a regression.

Per-article hero colors — ✅ settled (derive from features.ts)

The big category/event clusters are tokenized (ticker §2f, graphs §2g, chips, status dots). Per-article hero colors are now single-sourced: each article's hero <section> derives its background from its features.ts entry via featureBg('<slug>') (helper in data/features.ts), so the hero always matches the homepage feature card you clicked. This removed 13 hero bg-[#…]/bg-viz literals and fixed 3 drifts (cascadia#0E4C6B, wspr-aircraft-detection#148A62, wspr-tornado-v2-historical#2A1428). The accentColor/bgColor values in features.ts remain deliberate per-article content theming — that's the one source, by design.


6. Cleanup backlog (suggested order)

  1. Promote event/category colors to tokensDone — ticker event kinds (§2f), garden knowledge-graph palette (§2g), and ticker status dots (§2d).
  2. Swap the easy duplicatesDone — masthead teal → tp-teal, 3 article ocean heroes → bg-viz-2 (§5). Remaining hex are content/semantic/white (left deliberately). navigator.astro's graph palette is now tokenized too (§2g).
  3. De-inlineDoneincidents.astro (colors → brand tokens, display toggles → hidden class) and chips.astro (KIND_STYLESbg-event-* tokens, now a true mirror of the live ticker). Only dynamic glyph-scale inline left in each.
  4. Unify font loadingDone — Libre Baskerville moved from CSS @import to the Base.astro <link> block; the Vite @import warning is gone.
  5. Remove or relocate DSEG14Done — deleted the orphan woff2, its @font-face + .font-14seg class, and the dev-preview "Variant C" that was its only user.
  6. Prune unused tokensDone (2026-05-29) — removed 11 zero-reference tokens: tp-danger/success/warning, viridian, evergreen, abyss, kelp, lichen, signal, marigold, arctic. Verified each had no utility or var() usage before removal; pages render unchanged. The viz-* categorical ramp was kept intentionally as future-chart infrastructure (§2e).
Live Feed