U of I × MindRouter App Gallery

Idaho Seismic Atlas

Scientific visualization Built with Goose v1.50.0 Critic status: shipped

Idaho Seismic Atlas

Earthquakes in and around Idaho

Replay decades of earthquakes across Idaho — including the 2020 Stanley M6.5 sequence — on an animated map with magnitude, depth and aftershock statistics.

Idaho Seismic Atlas running on a desktop screen, earthquakes in and around idaho Desktop · click to enlarge
Idaho Seismic Atlas running on a phone, earthquakes in and around idaho
Phone · click to enlarge

What it is ✎ blurb written by the AI critic

An animated map of every M2+ earthquake recorded in and around Idaho since 1980, drawn from the USGS catalog. Press play to watch four decades of shaking unfold, drag a box on the map to compare a region's magnitude statistics, or open the 2020 Stanley M6.5 spotlight to replay its first 90 days of aftershocks and see how their rate decays.

Try this

  1. Press Play and watch the 1983 Borah Peak M6.9 flash in central Idaho, then the 2017 Soda Springs swarm light up the southeast corner.
  2. Drag a box around the Stanley–Challis area and compare its b-value with the whole catalog's (0.82 vs 0.79).
  3. Open the Stanley spotlight, press 'Replay first 90 days', then check how closely the red dots follow the fitted Omori curve.

Highlights ✎ written by the AI critic

  • 11,779 real USGS ComCat events (1980 to Sept 2026) on a hand-drawn Idaho county map, sized by magnitude and coloured by depth
  • Timeline playback at 1 to 8 years per second, with flash rings for new quakes and a cumulative-count chart that follows along
  • Drag a box on the map to filter it: every count, the largest-quakes list and the Gutenberg–Richter b-value update for that region
  • 2020 Stanley M6.5 spotlight: a 90-day aftershock replay coloured by days since the mainshock, plus an Omori decay chart with its fitted curve
  • Hover or tap any quake for time, magnitude, depth, coordinates and a link to its USGS event page

Who it's for

Geology courses, emergency management, curious Idahoans

Features

  • Map of Idaho drawn in SVG/canvas (simple equirectangular or Albers projection coded by hand) with county lines, quakes as circles sized by magnitude and colored by depth
  • Timeline slider + play button that animates the catalog through time, with a cumulative-count chart that follows
  • Brush a region on the map to filter; details on hover/click (time, magnitude, depth, place)
  • Gutenberg–Richter magnitude-frequency plot with a fitted b-value for the current selection
  • Spotlight button for the 2020 Stanley sequence (Omori aftershock decay chart)
  • Source/citation panel

Data & sources

  • USGS ANSS ComCat FDSN event service (earthquake.usgs.gov/fdsnws/event/1/query), GeoJSON, bbox lat 41.9-49.1 lon -117.3 to -110.9, M>=2.0, 1980-01-05 to 2026-09-12, 11,779 events in 4 time-window files (reused from the earlier attempt after validation; public domain)
  • U.S. Census Bureau TIGERweb Generalized_ACS2023 State_County layer 11 (Counties 500K): 44 Idaho counties as GeoJSON, downloaded 2026-09-13 (public domain)
  • U.S. Census Bureau TIGERweb Generalized_ACS2023 State_County layer 7 (States 500K): outlines of Idaho plus WA/OR/MT/WY/UT/NV as GeoJSON, downloaded 2026-09-13 (public domain)
Full data provenance ✎ written by the AI reviewer

Raw data sources — Idaho Seismic Atlas

All files in this folder were downloaded unmodified from official public web services. Retrieval date for every file: 2026-09-13. Total size ≈ 12 MB. Nothing here is synthetic. Turn these into compact JSON in data/embed/ with a script in scripts/ (Python 3 standard library only) — do not embed these raw files as-is.


1. USGS ANSS Comprehensive Earthquake Catalog (ComCat) — earthquakes around Idaho, M ≥ 2.0, 1980 → 2026-09-12

Publisher: U.S. Geological Survey, Earthquake Hazards Program (ANSS ComCat), via the FDSN Event Web Service. Service documentation: https://earthquake.usgs.gov/fdsnws/event/1/ Catalog home page (for the app's citation link): https://earthquake.usgs.gov/earthquakes/search/

The catalog was split into four time windows (the API limits one response to 20,000 events and smaller files are easier to handle). Same query everywhere: bounding box lat 41.9–49.1, lon −117.3 to −110.9, minmagnitude=2.0, orderby=time-asc, format=geojson. The windows do not overlap (endtime is exclusive) and contain no duplicate event ids.

FileExact URLEventsFirst event (UTC)Last event (UTC)Max M
usgs_comcat_idaho_m2_1980-1999.geojsonhttps://earthquake.usgs.gov/fdsnws/event/1/query?starttime=1980-01-01&endtime=2000-01-01&minlatitude=41.9&maxlatitude=49.1&minlongitude=-117.3&maxlongitude=-110.9&minmagnitude=2.0&orderby=time-asc&format=geojson2,0251980-01-051999-12-256.9 (1983-10-28 Borah Peak)
usgs_comcat_idaho_m2_2000-2019.geojsonhttps://earthquake.usgs.gov/fdsnws/event/1/query?starttime=2000-01-01&endtime=2020-01-01&minlatitude=41.9&maxlatitude=49.1&minlongitude=-117.3&maxlongitude=-110.9&minmagnitude=2.0&orderby=time-asc&format=geojson4,3952000-01-182019-12-305.8
usgs_comcat_idaho_m2_2020.geojsonhttps://earthquake.usgs.gov/fdsnws/event/1/query?starttime=2020-01-01&endtime=2021-01-01&minlatitude=41.9&maxlatitude=49.1&minlongitude=-117.3&maxlongitude=-110.9&minmagnitude=2.0&orderby=time-asc&format=geojson2,2602020-01-012020-12-316.5 (Stanley)
usgs_comcat_idaho_m2_2021-2026.geojsonhttps://earthquake.usgs.gov/fdsnws/event/1/query?starttime=2021-01-01&endtime=2026-09-14&minlatitude=41.9&maxlatitude=49.1&minlongitude=-117.3&maxlongitude=-110.9&minmagnitude=2.0&orderby=time-asc&format=geojson3,0992021-01-012026-09-124.9

Total: 11,779 events (unique ids).

Format notes (GeoJSON FeatureCollection)
  • Top level: {"type":"FeatureCollection","metadata":{...,"count":N,"url":...},"features":[...],"bbox":[...]}.

  • Each feature: id (e.g. "us70008jr5"), geometry.coordinates = [longitude, latitude, depth_km] (depth is in kilometres, positive down; range −3.5 to 42.9 km; a few shallow events have small negative depths).

  • Useful properties:

    • mag (number, 2.0–6.9), magType (mostly ml; also md, mc, mwr, mww, mb, …)

    • time — epoch milliseconds, UTC (e.g. 1585698750781 = 2020-03-31 23:52:30 UTC)

    • place — human-readable text such as "21 km WNW of Stanley, Idaho" (never empty)

    • type — earthquake (11,615), plus non-tectonic events: explosion (79), quarry blast (67), rock burst (16), mine collapse (1), mining explosion (1). Filter to type == "earthquake" for seismic statistics (or show the others distinctly).

    • status — reviewed/automatic/manual (mixed case in older records, e.g. REVIEWED).

    • url — the USGS event page for the quake; felt, cdi, mmi, sig, title are also present (many nulls).

  • Magnitude distribution: M2–2.9: 9,218 · M3–3.9: 2,315 · M4–4.9: 230 · M5–5.9: 14 · M6+: 2.

Notable sequences in this box (useful for annotations — verify against the data)
  • 1983-10-28 Borah Peak M6.9 (Custer County, Lost River Range) — largest event in the catalog window.

  • 2017 Soda Springs swarm (Caribou County, SE Idaho) — 1,437 events in 2017.

  • 2020-03-31 23:52:30 UTC Stanley M6.5 (id us70008jr5, lon −115.1175, lat 44.4646, depth 12.1 km) — largest Idaho earthquake since Borah Peak. ≈3,970 later events lie within 60 km of the epicentre (≈2,300 in the first 365 days), which is enough for an Omori (aftershock-rate decay) plot. Largest aftershock M4.8 on 2020-04-01.

License / terms

USGS-authored data are U.S. Government works in the public domain (no copyright). USGS asks for attribution, e.g. "U.S. Geological Survey, Earthquake Hazards Program, 2026, ANSS Comprehensive Earthquake Catalog (ComCat), accessed 2026-09-13". Policy: https://www.usgs.gov/information-policies-and-instructions/copyrights-and-credits


2. U.S. Census Bureau TIGERweb — Idaho county boundaries (generalized 1:500,000, January 1 2023 vintage)

File: census_tigerweb_idaho_counties_500k_acs2023.geojson (0.9 MB) Publisher: U.S. Census Bureau, Geography Division — TIGERweb REST service "Generalized_ACS2023 / State_County", layer 11 "Counties 500K" (these are the same generalized cartographic-boundary geometries as the Census "cb_2023_us_county_500k" files). Service page: https://tigerweb.geo.census.gov/arcgis/rest/services/Generalized_ACS2023/State_County/MapServer Exact URL: https://tigerweb.geo.census.gov/arcgis/rest/services/Generalized_ACS2023/State_County/MapServer/11/query?where=STATE%3D%2716%27&outFields=GEOID,STATE,COUNTY,BASENAME,NAME,AREALAND,AREAWATER,CENTLAT,CENTLON,INTPTLAT,INTPTLON&outSR=4326&f=geojson

Format notes
  • GeoJSON FeatureCollection with 44 features (all 44 Idaho counties), every geometry is a Polygon (coordinates = [ring, ...], ring = [[lon, lat], ...], WGS84 degrees). ≈21,400 vertices in total — simplify/round (e.g. 3 decimal places ≈ 100 m, drop near-duplicate points) before embedding.

  • Coordinate extent: lon −117.243 to −111.044, lat 41.988 to 49.001.

  • Properties: GEOID ("16051"), STATE ("16"), COUNTY ("051"), BASENAME ("Jefferson"), NAME ("Jefferson County"), AREALAND/AREAWATER (square metres), CENTLAT/CENTLON and INTPTLAT/INTPTLON (strings like "+43.8201483" — internal point, good for labels).

License / terms

Census Bureau TIGER/Line and cartographic boundary data are U.S. Government works in the public domain. Suggested credit: "U.S. Census Bureau, TIGERweb (Generalized ACS 2023 State_County, 1:500k)". Terms: https://www.census.gov/data/developers/about/terms-of-service.html


3. U.S. Census Bureau TIGERweb — Idaho state outline plus neighbouring states (generalized 1:500,000, 2023 vintage)

File: census_tigerweb_idaho_and_neighbor_states_500k_acs2023.geojson (0.7 MB) Publisher: U.S. Census Bureau — TIGERweb REST service "Generalized_ACS2023 / State_County", layer 7 "States 500K". Exact URL: https://tigerweb.geo.census.gov/arcgis/rest/services/Generalized_ACS2023/State_County/MapServer/7/query?where=STUSAB%20IN%20(%27ID%27,%27WA%27,%27OR%27,%27MT%27,%27WY%27,%27UT%27,%27NV%27)&outFields=GEOID,STATE,STUSAB,NAME,CENTLAT,CENTLON,INTPTLAT,INTPTLON&outSR=4326&f=geojson

Format notes
  • GeoJSON FeatureCollection with 7 features: Idaho (STUSAB "ID", STATE "16") and its neighbours Washington, Oregon, Montana, Wyoming, Utah, Nevada. The quake bounding box (lon −117.3…−110.9, lat 41.9…49.1) extends a little into every neighbour, so the neighbours give context for quakes just outside Idaho.

  • Idaho, MT, WY, UT, NV are Polygon; WA (60 rings incl. islands) and OR (6 rings) are MultiPolygon (coordinates = [polygon, ...], polygon = [ring, ...]). Neighbour polygons cover whole states — clip or crop them to the map's bounding box (or just let the SVG viewport clip them) and simplify before embedding. Idaho's outline ≈3,300 vertices.

  • Properties: GEOID, STATE (FIPS), STUSAB (postal code), NAME, centroid / internal-point strings as above.

License / terms

Public domain (U.S. Government work), same as source 2.

How it was built

Written by Goose 1.50.0 (github.com) on qwen/qwen3.8-27b, served by MindRouter, the University of Idaho's on-premises AI platform.

16harness rounds
148.2minutes of harness work (2.5 h)
2,125lines of code
1,995KB, single-file app

Rounds by kind

build1
feedback8
continue5
critic2

Lines of code per file

body html164
app css254
app js1,549
scripts158

Critic scores ✎ scored by the independent AI critic, 1–10

visual 8 out of 10
usefulness 8 out of 10
correctness 8 out of 10
interactivity 8 out of 10

Independent audits ✎ by the AI critic

Data honesty: checked against raw data Authorship audit: every app-code commit is a harness round

Automated checks

Console no console errors No network 0 external requests Phone layout 0 px overflow Canvases 1 empty canvases Brand header header present No key leak no key in source

Checked by critic-verify.

Data spot checks ✎ written by the AI critic

  • Borah Peak hover: app shows M6.9, Oct 28 1983 02:06 PM UTC, 10.0 km, 44.06°N 113.86°W. Raw usp0001zbv: M6.9, 1983-10-28 14:06:06 UTC, depth 10, lat 44.058, lon -113.857. Match.
  • Auburn 'Largest quakes' button: app shows M5.8, Feb 3 1994 09:05 UTC, 7.9 km, 42.76°N 110.98°W. Raw usp00067xb: M5.8, 1994-02-03 09:05:04 UTC, depth 7.9, 42.762 / -110.976. Match.
  • Stanley mainshock tap (phone): app shows M6.5, Mar 31 2020 11:52 PM UTC, 12.1 km. Raw us70008jr5: 2020-03-31 23:52:30 UTC, depth 12.06. Match.
  • Stanley stats: app shows 3,972 aftershocks within 60 km, 2,302 in the first 365 days, largest M4.8. My own haversine calculation from raw gives 3,972 / 2,302 / M4.8 (2020-04-01 00:27 UTC). Match.
  • Whole-catalog b-value: app shows b = 0.79 ± 0.01, Mc 2.3, N 8,181. My own Aki–Utsu calculation (0.1 bins rounded half-up, Mc = max-count bin + 0.2) gives b 0.7887, error 0.0087, Mc 2.3, N 8,181. Match. Rounding half-to-even instead gives b 0.806 and Mc 2.4, so the method choice matters.
Show all 9 spot checks
  • Brushed central-Idaho box (43.6–45.2°N, 113.5–116.0°W): app shows 5,367 earthquakes, b = 0.82 ± 0.02, Mc 2.6, N 2,029. Raw gives 5,367, b 0.8197, error 0.0182, Mc 2.6, N 2,029. Match.
  • Omori fit: app shows p = 0.61, c = 0.20 d, K = 79. Re-running the same log-binned least-squares fit on raw data in Python gives p 0.61, c 0.20, K 79. A maximum-likelihood fit on the first 365 days gives p 0.60.
  • Timeline at 1983-09-30 17:31 UTC: app shows 94 quakes, largest M4.6. Raw has 94 earthquakes up to that time, max M4.6. Match.
  • Counts at every tested state match my calculation from raw data after each fix: full catalog 11,615, earthquakes-only unticked 11,779, zoomed 6,272, last-12-months at 2015-05 333, Stanley 90-day replay 1,193. Intro 11,779 events / 11,615 earthquakes also matches raw.
Build notes ✎ written by the AI reviewer

The app is entirely harness-built (Goose 1.50.0 on MindRouter default-agent / qwen3.8-27b). I wrote only data/raw (two Census boundary downloads plus SOURCES.md; the quake files were reused after validation) and FEEDBACK.md, and removed FEEDBACK.md at the end (commit 98f4822). The tamper guard never fired.

  • Rounds: 13 in total (1 build, 8 feedback, 4 continue), 128 minutes of harness time. Round 1 built a good-looking base in 6.5 minutes, with serious bugs: Play jumped to the end instantly, the Stanley radius was 60 degrees instead of 60 km, the b-value was negative, and there were page errors.

  • Round 2 failed on a MindRouter network error; round 3 re-ran it successfully.

  • The main harness weakness is repetition loops. Rounds 4, 5 and 7 re-ran the same failing python3 -c data query 55 to 188 times with no file changes, and I killed them under the stuck-round rule. Round 8 dumped the full catalog repeatedly until the session ended with no edits.

  • What helped: FEEDBACK.md written as short, code-level instructions with an explicit ban on exploratory data scripts, and no data values the model felt it had to verify. From then on, rounds 9–13 each took 1–5 minutes.

  • Goose fixed every item it was given, but three fix rounds introduced a new regression each:

    • a ReferenceError that broke the spotlight

    • the quake layer misaligned with the map on first load

    • the map resizing on hover, which pinned the wrong quake

  • I caught all three with my own Playwright checks, and the next round fixed each one.

  • The final build passes all my checks with zero errors. Brushing (mouse and touch), zoom, playback at about 1 year per second, the Stanley replay and exit, and hover/pin (correct quake, no layout shift) all work.

  • The b-value of 0.79 matches my own calculation of 0.78.

  • The Stanley counts (3,972 / 2,302 / M4.8) match my haversine calculation.

verify's "empty_canvases: 1" is a false positive: it samples only the transparent top-left corner of the quake overlay.

Round-by-round timeline

Every harness round, in order. The prompts and FEEDBACK.md were written by the AI reviewer; the app code was written by the harness. Expand a round to see the exact prompt and the FEEDBACK.md the harness received.

    1 build 6.6 min · 9 files changed, 2630 insertions(+), 3 deletions(-) Show prompt

    Prompt given to the harness:

    Read AGENTS.md, SPEC.md and BRAND.md in this directory. Then build the complete app they describe, step by step: first write scripts in scripts/ that turn the raw data in data/raw/ (see data/raw/SOURCES.md — USGS ComCat earthquake GeoJSON plus Census county and state boundary GeoJSON) into compact JSON in data/embed/ and run them; then write src/body.html, src/app.css and src/app.js; run python3 build.py --check and fix any error it reports. Implement every required feature in SPEC.md with a polished, modern, responsive University of Idaho design. Do not stop until python3 build.py --check succeeds and every feature is implemented.
    

    No feedback file was sent in this round.

    3 continue 4.7 min · 4 files changed, 1559 insertions(+), 763 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — round 1 (tested index.html in Chromium at 1440×900 and 390×844)

    The base looks promising (clean cards, good brand use, real data, map with counties). But several core features are broken or scientifically wrong. Most important first. Keep everything that already works.

    1. Play does not animate — it jumps straight to the end. Click "▶ Play" (timeline at the end, or after dragging the slider to 1994): within one frame the date reads "Sep 11, 2026" again and the button returns to "▶ Play". The time advance per frame is ~1000× too large (the frame delta is in milliseconds but is multiplied as if it were seconds). Expected: at 1× a full 1980→2026 replay takes about 45 s (≈1 year per second; 2×/4×/8× faster), the date readout ticks visibly, the map fills in progressively, the cumulative chart grows, and Pause works mid-replay. To make the replay feel alive: newly appearing quakes should briefly flash (e.g. an expanding gold/red ring fading over ~1 s) and large ones (M ≥ 4.5) should get a short on-map label ("M6.9 Borah Peak 1983"). Add a "Show: all events up to date / only the last 12 months" option so replays show sequences appearing and fading. Playback must stay smooth (≥ 30 fps) — right now every frame rebuilds ~11,600 SVG circles.

    2. Uncaught page errors (must be zero):

      • Cannot read properties of undefined (reading '4') — happens when the mouse moves over an empty part of the map (the "no quake under cursor" case is returned as -1 but treated as a valid index).

      • svgPoint is not defined — happens on every mouse-wheel scroll over the map, so scroll-zoom never works.

    3. "Brush a region to filter" is not implemented as a filter — dragging only zooms the map. Expected (SPEC): dragging on the map draws a selection rectangle that stays visible (gold outline, light fill) while quakes outside it are dimmed; the "shown" count, the cumulative chart, the Gutenberg–Richter plot and b-value all update to the selected region; a visible chip such as "Selection: 1,284 earthquakes · 44.1–45.0°N, 115.6–114.4°W ✕ Clear" appears, and Esc / ✕ clears it. Keep zoom as a separate control (+ / − / reset buttons and working wheel zoom). Brushing and tapping must also work with touch on phones (use pointer events; at present only mouse events are handled, so nothing can be brushed on a phone).

    4. Cumulative chart does not follow the selection or the filters. After brushing central Idaho, toggling "Earthquakes only", or opening the Stanley spotlight, the curve is unchanged (always the whole catalog, y-max 11,779 which even includes blasts). Expected: the curve is computed for the current selection/filters, is drawn only up to the playhead (future part faded or hidden) so it grows during playback, shows the current count next to the playhead dot, has a y-axis title, marks big events (M ≥ 5.5, e.g. 1983 Borah Peak, 2020 Stanley) with small labelled ticks, and shows a hover tooltip (date + count).

    5. Gutenberg–Richter b-value is wrong. It shows "b = -0.74" for the full catalog and "b = -0.19" when the timeline is at 1994; a b-value is a positive number (typically 0.8–1.2). The fit is a least-squares line through incremental 0.1-M bins including sparse bins, and bars start at log10 N = 0 so single-event bins vanish. Expected: plot cumulative N(≥M) as dots on a logarithmic y-axis with labelled decades (1, 10, 100, 1,000, 10,000) and the incremental counts as light bars behind; estimate the magnitude of completeness Mc (maximum-curvature method: the magnitude bin with the most events, plus 0.2); compute b with the Aki–Utsu maximum-likelihood estimator b = log10(e) / (mean(M for M ≥ Mc) − (Mc − 0.05)) with its uncertainty b/√N; draw the fitted line log10 N = a − b·M from Mc upward; report "b = 0.98 ± 0.02 · Mc = 2.4 · N = 5,432"; mark Mc with a vertical dashed line; x-axis should end at the largest magnitude present (not an empty 6–7 range). When fewer than ~50 events are above Mc, show a friendly empty state ("Select a larger region or time span to estimate a b-value").

    6. Stanley aftershock numbers are wrong. The card says "5,230 aftershocks (60 km)" and "Largest aftershock M 4.9", but the distance test compares squared degrees with 60² (i.e. a 60-degree radius), so it counts quakes all over the region. With a real 60 km great-circle (haversine) distance from the mainshock (lon −115.1175, lat 44.4646) the data give ≈3,970 events after the mainshock, ≈2,300 in the first 365 days, and the largest aftershock is M4.8 (2020-04-01 00:27 UTC).

    7. The Omori chart is not an Omori analysis. It is a linear, 7-day-smoothed daily count with the formula only in the caption. Expected: a log–log plot of aftershock rate (events/day) vs days since mainshock, using logarithmically growing time bins (e.g. from 0.01 day to ~2,000 days), shown as dots, with the fitted modified Omori law n(t) = K / (t + c)^p drawn as a line and the fitted values shown ("p = 1.05 · c = 0.08 d · K = …"), plus a one-sentence plain-language explanation. Also: pressing "Show sequence" currently leaves the timeline at Sep 2026, so the zoomed map shows 46 years of quakes, not the sequence. Expected: the spotlight shows only events from the mainshock onward (aftershocks coloured by days since mainshock, mainshock star + label), offers a "▶ Replay first 90 days" button that animates the sequence on the map, and a clear "Exit spotlight" that restores the previous view.

    8. Times labelled "UTC" are really the browser's local time. Hover details say "May 19, 2026, 06:36 AM UTC" but the value is formatted in the local time zone (same for the timeline readout "Sep 11, 2026, 11:44 PM"). Format all times in UTC (and label them UTC) or explicitly in Mountain Time.

    9. Phone (390 px) map is hard to use. Idaho fills only about a third of the 328 px-wide map (lots of empty neighbour-state area left and right), the legend box covers the whole southern third of Idaho, and the dark hint "Drag to zoom a region · scroll to zoom · double-click to reset" covers the panhandle. Expected: on narrow screens the map frames Idaho tightly (taller map, roughly 1.25× as tall as wide), the legend sits below the map (not over it) and the hint is short ("Drag to select a region · tap a quake") and not on top of data. On desktop the map also wastes the grey space left and right of Idaho — frame it to Idaho plus a small margin.

    10. Timeline year labels collide on the phone ("19801985199019952000200520102015…" as one run of digits) and on desktop they are spaced evenly with flexbox rather than at their true time positions, so they don't line up with the histogram bars or the slider thumb. Expected: tick labels positioned by time, every 10 years on narrow screens. Also mark the big events on the histogram (small labelled dots for M ≥ 5.5).

    11. Unbalanced layout with large empty areas (desktop 1440 px). The Timeline card has ~130 px of blank space under the year axis; the cumulative chart card has ~120 px blank at the bottom; the Gutenberg–Richter card is taller than its content; under the map there is an empty dashed box (the detail area) when nothing is hovered. Expected: a balanced, dashboard-like layout with no dead space — e.g. put the timeline/player as a full-width bar directly under the map section, give the detail area a helpful empty state ("Hover or tap a quake to see its time, magnitude, depth and place"), and size chart cards to their content. The intro stat tiles have uneven heights because "1980–2026" wraps onto two lines.

    12. Chart text is tiny and not in the brand font. Canvas axis labels are 10 px and set with "10px var(--font)" — CSS variables do not work in canvas font strings, so the browser falls back to its default font. Expected: 11–12 px labels in Public Sans (use the real family name), axis titles on every chart, crisp on high-DPI screens.

    13. Pinned highlight drawn in the wrong place. Click a quake to pin it, then drag-zoom elsewhere: a gold ring stays stuck in the top-left corner of the map even though that quake is outside the view. Only draw hover/pin rings for quakes inside the current view, and clear the pin when the selection changes.

    14. Emoji used as icons (🗺️ ⏱️ 📈 💥 in the intro chips, 📅 ⬇ 📍 in the details) render differently on every OS and look less polished than the rest of the design. Replace them with small inline SVG icons or plain text labels.

    15. Wording accuracy: the intro says "11,779 earthquakes", but 11,779 is all events; 11,615 are earthquakes and the rest are blasts / rock bursts. Say "11,779 seismic events (11,615 earthquakes)".

    5 continue 28.4 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    6 continue 8.6 min · 5 files changed, 556 insertions(+), 195 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — round 2 (tested index.html in Chromium at 1440×900 and 390×844)

    How to work on this: edit src/app.js, src/app.css and src/body.html directly, one item at a time, and run python3 build.py --check after each few items. The formulas below are already correct — there is no need to write or run Python scripts to re-derive the statistics (earlier attempts got stuck repeating the same failing script). If a shell command fails twice, stop repeating it and continue with the code change.

    Good progress so far: brushing filters with a clear chip (also by touch), UTC times, correct Stanley counts (3,972 / 2,302 / M4.8), zoom buttons, SVG icons. Keep all of that. Still broken, most important first:

    1. Play still jumps to the end in one frame. In playStep, the time advance multiplies the frame delta (in milliseconds) by the length of a year in milliseconds, i.e. one year per millisecond. Divide the frame delta by 1000 so 1× = one simulated year per real second (full replay ≈ 46 s; 2×/4×/8× faster). Test: slider at Jan 1994, press Play, after 1 s the readout should say about Jan 1995. The Stanley "Replay first 90 days" has the same bug and ends instantly; it should run the 90 days over ~15 s (about 6 simulated days per real second), stopping at day 90.

    2. Frozen gold rings after Play. Because playback stops on its first frame, every quake keeps a "new quake" flash ring forever and the map turns into a mass of gold/red circles. Flash rings must fade out within ~1 s and be removed (redraw once more after they expire, and clear them when playback stops or pauses).

    3. Page error Cannot read properties of undefined (reading '4') whenever the pointer moves over an empty part of the map: quakeAt returns -1 for "nothing here" and that -1 is stored in state.hover / state.pinned and later used as an index. Treat -1 as null everywhere (hover, pin, cursor, detail panel).

    4. Gutenberg–Richter chart is empty and b is ~5× too big ("b = 3.70 ± 0.05"). In renderGR: (a) the cumulative array is filled with cum[b] = cum[b + 1] + bins[b] starting at the last bin, which reads past the end of the array (undefined → NaN), so every cumulative value is NaN and nothing is drawn — allocate one extra zero element or start from bins[last]; (b) the Aki–Utsu estimator is b = log10(e) / (mean(M ≥ Mc) − (Mc − 0.05)) where log10(e) = 0.4343 (Math.LOG10E); the code uses Math.log(10) (= 2.303) instead. Also round each magnitude to 0.1 before binning. After the fix the whole catalog gives a b-value a bit below 1 and the plot shows dots, bars and the red fit line.

    5. Omori chart. (a) The log time bins only go from 0.01 to 10 days (25 edges × 3/24 decades); extend them to ~2,000 days (about 0.01 → 2,000 d, ~8 bins per decade). (b) The fit is done with natural logs (Math.log) but K is computed as Math.pow(10, logK) — use Math.exp(logK) so the line passes through the dots. (c) Clip the fit line to the plot area (it currently runs off into the top-right corner). (d) Fit only bins from ~0.1 day on and say in the caption that the first hours are under-recorded.

    6. Stanley spotlight shows 40 years of unrelated quakes. After "Show sequence" or "Replay first 90 days" the map still draws 1980–2019 events around Challis/Clayton ("M6.9 The 1983 Borah Peak Earthquake 1983"). While the spotlight is open, draw only events from the mainshock time onward, coloured by days since the mainshock with a small legend, plus the mainshock star. "Exit spotlight" restores the normal view.

    7. Map labels pile up. Every M ≥ 4.5 quake (~40) has a permanent label; around Challis/Stanley and Soda Springs they overlap into unreadable stacks, and on the phone they run off the right edge ("M5.8 Lincoln 20"). Permanently label only M ≥ 5.5, skip any label whose box would overlap an already-drawn label or leave the map, and use short text like "M6.9 Borah Peak · 1983". Smaller M ≥ 4.5 events may get a label only briefly (~3 s) as they appear during playback.

    8. Desktop map is taller than the screen. At 1440×900 the map is 1230×1046 px, so the Play button and the date readout are off-screen whenever the map is visible, and Idaho sits between wide empty grey bands. Make the map about 640–700 px tall with a side column beside it (legend, quake details, selection chip, live stats: shown count, largest magnitude, b-value) and keep the timeline player directly underneath, so map + Play + date fit in one screen. On phones keep the current stacked layout.

    9. "Last 12 months" does not update the count. Ticking it drops the map to ~400 recent events, but the readout still says "11,615 shown". Call the same update used by the slider when the checkbox changes.

    10. Cumulative chart text collisions. The rotated "Cumulative count" title overlaps the y-tick numbers; "Year" overlaps the "2005" tick; the count label next to the playhead dot is cut off at the right edge (a stray "1" shows). Give the axis titles their own margin, keep the count label inside the chart (flip it to the left of the dot near the right edge), and make sure the hover tooltip (date + count) is visible and disappears on mouse leave.

    11. Phone (390 px). The dark "Drag to select a region · tap a quake" hint still covers quakes in the panhandle — put it below the map or fade it out after 3 s. The timeline's "1980" tick is clipped at the card's left edge (also sticks out on desktop). The cumulative chart's year labels touch each other — use 10-year ticks on narrow screens.

    12. Selection chip count differs from the readout ("5,659 earthquakes" vs "5,657 shown"): the chip counts all event types and ignores the date. Use the same filters as the rest of the page.

    8 continue 11.9 min · 1 file changed, 48 insertions(+), 51 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — round 3 (tested index.html in Chromium at 1440×900 and 390×844)

    How to work on this (important): fix these by editing src/app.js, src/app.css and src/body.html directly, one item at a time, and run python3 build.py --check after every few items. Do not investigate the data with python3 -c one-liners — everything needed is described below. Never run the same command twice in a row; if a command does not give you what you need, make the code change instead and move on to the next item.

    Big improvement: playback runs at ~1 year per second with fading flash rings, the Stanley replay animates day by day, the Gutenberg–Richter plot and b-value work, brushing and the side "Live stats" update, and there are no page errors. Keep all of that. Remaining problems, most important first:

    1. Hover / tap shows the wrong quake. Pointing exactly at the centre of the big M6.9 Borah Peak circle (1983) shows "M 2.1 · 23 km WNW of Mackay, Idaho · Nov 26, 2020" in the detail panel; tapping it on the phone shows an M2.2 from 2021. quakeAt picks the nearest centre, so tiny quakes next to a big one steal the hover. Expected: among quakes whose drawn circle contains the pointer (radius + 3 px), choose the one with the largest magnitude (it is drawn on top); only when no circle contains the pointer fall back to the nearest centre within 12 px.

    2. Click-to-pin does not stay pinned. Click a quake, then move the mouse elsewhere over the map: the detail panel returns to "Hover or tap a quake to see its time, magnitude, depth and place." Expected: after a click/tap the pinned quake's details stay (with a small "✕ Unpin" button) and its gold ring stays; hovering other quakes may preview them temporarily, but leaving them shows the pinned quake again. Clicking empty map, "✕ Unpin" or Esc unpins.

    3. Omori fit line is far below the data. The chart reads "p = 0.57 · c = 0.01 d · K = 3" and the orange line runs from ~30 events/day at 0.01 day to below 1/day at ~50 days, while the red dots are ~100–200/day at 0.1 day, ~60/day at 1 day and ~1/day at 1,000 days. Fit in exactly the quantities that are plotted (x = bin-centre days, y = events/day): minimise the squared error of ln(rate) − [ln K − p·ln(t + c)] over the plotted dots with t ≥ 0.1 day, and draw K / (t + c)^p with that same K. Sanity check in code: the curve at t = 1 day should be within a factor of ~2 of the dot nearest 1 day. Expected p roughly 0.6–1.2 and K in the tens to hundreds.

    4. b-value drops the Mc bin (floating point) and the two b-values disagree. In renderGR, Mc = mMin + mcBin * 0.1 + 0.2 evaluates to 2.3000000000000003, so m >= Mc excludes every M2.3 event (header shows "N = 6,951" although about 8,090 earthquakes have M ≥ 2.3) and b comes out too low (0.68). Round Mc to one decimal (Math.round(x * 10) / 10) before comparing. The side "Live stats" card shows b = 0.72 while the chart header shows b = 0.68 for the same selection (Live stats uses unrounded magnitudes) — compute the b-value in one shared function and show that same number in both places. The red fit line should start at the cumulative dot at Mc.

    5. Stanley spotlight: live stats and marker text don't match the map. While the spotlight shows only events since the mainshock, "Live stats" still says "5,804 shown · M6.9 largest" (largest should be M6.5). Stats must describe what is drawn. A second pink "Stanley M6.5" text is drawn behind the "M6.5 Stanley · 2020" label above the star, producing garbled overlapping text — draw only one label there.

    6. Desktop side column wastes space; legend appears twice. At 1440 px the side column (Legend + Live stats) ends ~330 px above the bottom of the map card, while the depth/magnitude legend is repeated under the map, followed by the selection chip and the detail panel. Move the quake detail panel and the selection chip into the side column below Live stats and remove the legend under the map, so the map card is shorter and the timeline player sits right under the map. On phones show the legend only once.

    7. Awkward label text. The largest event is labelled "M6.9 The 1983 Borah Peak Earthquake · 1983". When building a label, strip a leading "The ", a year and the word "Earthquake", any "NN km DIR of " prefix and a trailing ", Idaho"/", Montana"/etc. → "M6.9 Borah Peak · 1983".

    8. Clipped chart text. In the cumulative chart the count label above the playhead dot ("11,615", or "5,993" after brushing) is cut off by the top edge when the playhead is at the end — place it below/left of the dot when it would leave the plot. In the Gutenberg–Richter chart the rotated y-axis title "log₁₀ N(≥M)" overlaps the "1,000" tick label — add left padding.

    9. Timeline axis "1980" label starts left of the card's content edge (x ≈ 86 px while the slider starts at x ≈ 101 px on desktop; at x = 0 on the phone). Keep the first and last tick labels inside the card (left-align the first, right-align the last).

    10. Empty Spotlight card before it is opened. The Spotlight card next to the cumulative chart is mostly blank (one sentence and a button). Show a small static teaser: "Mar 31, 2020 23:52 UTC · M6.5 · depth 12 km · 3,972 aftershocks within 60 km", so the card is informative before the button is pressed.

    11. Keyboard access to the map. Tab goes from the page header straight to the timeline; the map cannot be explored without a mouse. Add a short "Largest quakes in view" list (top 8 by magnitude for the current filters) in the side column — each row a button that pins that quake on the map and shows its details.

    14 critic 9.9 min · 1 file changed, 78 insertions(+) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check. Do not run exploratory data queries; the data is already verified.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    FEEDBACK — reviewer findings from browser testing (fix every item, then rebuild)

    • Rules for this round: Edit only src/app.js, src/app.css and (if needed) src/body.html. Do not touch data/ or scripts/.

    • Do NOT write or run exploratory python/node/python3 -c data queries. Every number the app shows was already verified against the raw USGS data and is correct. Nothing here needs data checking.

    • Run python3 build.py --check once at the end.

    1. Omori fit line is drawn wrong (function drawOmori)

    The fit line is ONE straight segment from t = 0.01 to t = 2000 days (moveTo at tMin, lineTo at tMax). But n(t) = K / (t + c)^p with c = 0.20 is curved on log-log axes, so the straight chord sits below almost every red dot and misrepresents the fit. Replace the moveTo/lineTo pair with a sampled polyline:

    for (let i = 0; i <= 120; i++) {
      const t = tMin * Math.pow(tMax / tMin, i / 120);
      const x = xOf(t), y = yOf(K / Math.pow(t + c, p));
      i ? ctx.lineTo(x, y) : ctx.moveTo(x, y);
    }

    Keep the existing clip rectangle.

    2. Cumulative chart hover tooltip never appears (cumCanvas "mousemove" handler)

    The handler uses pad = { l: 52, r: 12 } — no t/b, so dy is NaN and nothing is drawn; it also only accumulates counts up to ci, so maxC is wrong. Fix: use exactly the same pad as renderCum (l: 62, r: 16, t: 12, b: w < 500 ? 40 : 34), accumulate the whole array (for i < counts.length), then draw the dark tooltip box (date · count) above the point, clamped inside the canvas horizontally. Easiest: refactor renderCum(hoverX) to take an optional hover x and draw the tooltip at the end, and have mousemove call renderCum(x) and mouseleave call renderCum().

    3. Counters go stale after "Earthquakes only" and zoom
    • Untick "Earthquakes only": the map draws 11,779 events but the Timeline still says "11,615 shown" and Live stats "Shown" still says 11,615.

    • Zoom in twice: the map draws 6,272 events but both still say 11,615.

    Fix: call updateTimelineUI() (it refreshes #time-count and Live stats) at the end of the #eq-only change handler, zoomAt(), the map dblclick handler, and both branches of the stanleyBtn click handler.

    4. During the Stanley spotlight the two counters disagree

    With the spotlight on, Timeline says "5,804 shown" but Live stats says "4,363 shown" (the map only draws events from the mainshock onward). Add one helper and use it everywhere a quake is counted, drawn or hit-tested:

    function drawn(i) {
      return visible(i) && !(state.stanley && DATA.meta.stanley && Q[i][4] < DATA.meta.stanley.t);
    }

    Use drawn(i) in renderQuakes, renderLiveStats, renderTopQuakes, updateTimelineUI (#time-count), updateSelChip and quakeAt, and delete the duplicated stanleyActive … continue checks there.

    5. An interrupted Stanley replay leaves replay mode stuck

    Steps: Show sequence → "Replay first 90 days" → press Pause → drag the timeline to 2010 → press Play. Time now advances at 6 days per second instead of 1 year per second and stops in June 2020, because state.stanleyReplay stays true and stanleyStopAt stays set. Fix: in the slider "input" handler and in the #play click handler (when it starts playback), set state.stanleyReplay = false; stanleyStopAt = null;. Only stanleyReplay() may set them.

    6. "Hide sequence" must fully leave the spotlight

    The gold "Hide sequence" button in the spotlight card header leaves the map zoomed on Stanley and the counters stale. When it is pressed while the spotlight is on, call exitStanley() (same as "Exit spotlight").

    7. Side Legend should match the map during the spotlight

    While the spotlight is on, the map colours dots by days since the mainshock, but the side Legend still shows the depth scale. Make renderLegend() check state.stanley: when on, show a "Days since mainshock" bar (CSS linear-gradient using stanleyColor(0), stanleyColor(10), stanleyColor(1000), labels "0 · 10 · 1000 d") instead of the depth bar; otherwise the depth bar. Call renderLegend() when the spotlight opens and in exitStanley(). Then remove the small white "Days since mainshock" legend box drawn on the map canvas.

    8. Desktop side column is clipped and scrolls internally

    .map-side { max-height: 780px; overflow-y: auto; } cuts off the bottom of "Largest quakes in view", and after brushing a region the "Selected quake" card scrolls out of sight inside the column. Remove max-height and overflow-y from .map-side so the column takes its natural height, and list the 5 (not 6) largest quakes.

    9. Cumulative chart y-axis uses odd tick values (2,904 / 5,808 / 8,711)

    Use round tick values: step = the smallest of {1, 2, 2.5, 5} × 10^k that is ≥ maxC / 4; ticks at 0, step, 2·step, … and set the top of the y scale to the first tick ≥ maxC (use that top in yOf).

    10. Phone: the map hint covers the quakes

    On screens ≤ 560 px wide the dark "Drag to select a region · tap a quake" pill sits on top of the dots. In the @media (max-width: 560px) block make .map-hint position: static, transparent background, muted text colour, no animation, shown as a small line under the map (it must not cover the map).

    15 continue 9.3 min · 4 files changed, 174 insertions(+), 168 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check. Do not run exploratory data queries; the data is already verified.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    FEEDBACK — reviewer findings from browser testing (fix every item, then rebuild)

    • Rules for this round: Edit only src/app.js, src/app.css and (if needed) src/body.html. Do not touch data/ or scripts/.

    • Do NOT write or run exploratory python/node/python3 -c data queries. Every number the app shows was already verified against the raw USGS data and is correct. Nothing here needs data checking.

    • Run python3 build.py --check once at the end.

    • Every function, id and class named below is in src/app.js, src/app.css or src/body.html. template.html and index.html contain none of them — do not grep or read those two files. Never run the same command twice in a row; after reading a section, edit it.

    • Work through the items in order and edit the file after each item.

    1. Omori fit line is drawn wrong (function drawOmori)

    The fit line is ONE straight segment from t = 0.01 to t = 2000 days (moveTo at tMin, lineTo at tMax). But n(t) = K / (t + c)^p with c = 0.20 is curved on log-log axes, so the straight chord sits below almost every red dot and misrepresents the fit. Replace the moveTo/lineTo pair with a sampled polyline:

    for (let i = 0; i <= 120; i++) {
      const t = tMin * Math.pow(tMax / tMin, i / 120);
      const x = xOf(t), y = yOf(K / Math.pow(t + c, p));
      i ? ctx.lineTo(x, y) : ctx.moveTo(x, y);
    }

    Keep the existing clip rectangle.

    2. Cumulative chart hover tooltip never appears (cumCanvas "mousemove" handler)

    The handler uses pad = { l: 52, r: 12 } — no t/b, so dy is NaN and nothing is drawn; it also only accumulates counts up to ci, so maxC is wrong. Fix: use exactly the same pad as renderCum (l: 62, r: 16, t: 12, b: w < 500 ? 40 : 34), accumulate the whole array (for i < counts.length), then draw the dark tooltip box (date · count) above the point, clamped inside the canvas horizontally. Easiest: refactor renderCum(hoverX) to take an optional hover x and draw the tooltip at the end, and have mousemove call renderCum(x) and mouseleave call renderCum().

    3. Counters go stale after "Earthquakes only" and zoom
    • Untick "Earthquakes only": the map draws 11,779 events but the Timeline still says "11,615 shown" and Live stats "Shown" still says 11,615.

    • Zoom in twice: the map draws 6,272 events but both still say 11,615.

    Fix: call updateTimelineUI() (it refreshes #time-count and Live stats) at the end of the #eq-only change handler, zoomAt(), the map dblclick handler, and both branches of the stanleyBtn click handler.

    4. During the Stanley spotlight the two counters disagree

    With the spotlight on, Timeline says "5,804 shown" but Live stats says "4,363 shown" (the map only draws events from the mainshock onward). Add one helper and use it everywhere a quake is counted, drawn or hit-tested:

    function drawn(i) {
      return visible(i) && !(state.stanley && DATA.meta.stanley && Q[i][4] < DATA.meta.stanley.t);
    }

    Use drawn(i) in renderQuakes, renderLiveStats, renderTopQuakes, updateTimelineUI (#time-count), updateSelChip and quakeAt, and delete the duplicated stanleyActive … continue checks there.

    5. An interrupted Stanley replay leaves replay mode stuck

    Steps: Show sequence → "Replay first 90 days" → press Pause → drag the timeline to 2010 → press Play. Time now advances at 6 days per second instead of 1 year per second and stops in June 2020, because state.stanleyReplay stays true and stanleyStopAt stays set. Fix: in the slider "input" handler and in the #play click handler (when it starts playback), set state.stanleyReplay = false; stanleyStopAt = null;. Only stanleyReplay() may set them.

    6. "Hide sequence" must fully leave the spotlight

    The gold "Hide sequence" button in the spotlight card header leaves the map zoomed on Stanley and the counters stale. When it is pressed while the spotlight is on, call exitStanley() (same as "Exit spotlight").

    7. Side Legend should match the map during the spotlight

    While the spotlight is on, the map colours dots by days since the mainshock, but the side Legend still shows the depth scale. Make renderLegend() check state.stanley: when on, show a "Days since mainshock" bar (CSS linear-gradient using stanleyColor(0), stanleyColor(10), stanleyColor(1000), labels "0 · 10 · 1000 d") instead of the depth bar; otherwise the depth bar. Call renderLegend() when the spotlight opens and in exitStanley(). Then remove the small white "Days since mainshock" legend box drawn on the map canvas.

    8. Desktop side column is clipped and scrolls internally

    .map-side { max-height: 780px; overflow-y: auto; } cuts off the bottom of "Largest quakes in view", and after brushing a region the "Selected quake" card scrolls out of sight inside the column. Remove max-height and overflow-y from .map-side so the column takes its natural height, and list the 5 (not 6) largest quakes.

    9. Cumulative chart y-axis uses odd tick values (2,904 / 5,808 / 8,711)

    Use round tick values: step = the smallest of {1, 2, 2.5, 5} × 10^k that is ≥ maxC / 4; ticks at 0, step, 2·step, … and set the top of the y scale to the first tick ≥ maxC (use that top in yOf).

    10. Phone: the map hint covers the quakes

    On screens ≤ 560 px wide the dark "Drag to select a region · tap a quake" pill sits on top of the dots. In the @media (max-width: 560px) block make .map-hint position: static, transparent background, muted text colour, no animation, shown as a small line under the map (it must not cover the map).

    16 critic 0.6 min · 4 files changed, 66 insertions(+), 99 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check. Do not run exploratory data queries; the data is already verified.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    FEEDBACK — reviewer findings from browser testing (fix every item, then rebuild)

    The previous fixes all work. Only these 3 small items remain.

    • Rules for this round: Edit only src/app.js and src/app.css. Everything named below is in those two files. Do not grep or read template.html, index.html or src/body.html.

    • Do NOT write or run exploratory python/node data queries. The data is already verified.

    • Never run the same command twice in a row. Make the 3 edits, then run python3 build.py --check once and stop.

    1. REGRESSION: "Replay first 90 days" no longer stops at 90 days
    • stanleyReplay() sets state.stanleyReplay = true and stanleyStopAt, then calls startPlay() — but startPlay() now clears both (lines state.stanleyReplay = false; and stanleyStopAt = null;), so the replay plays at 1 year per second and runs to 2026. Fix: Delete those two lines from startPlay().

    • Replace the play-button handler $("play").addEventListener("click", () => state.playing ? stopPlay() : startPlay()); with

      $("play").addEventListener("click", () => {
        if (state.playing) { stopPlay(); return; }
        state.stanleyReplay = false;
        stanleyStopAt = null;
        startPlay();
      });

    (Keep the two reset lines in the slider "input" handler.)

    2. Cumulative chart: the rotated "Cumulative count" title overlaps the "10,000" / "15,000" tick labels

    In renderCum, change pad to { l: 76, r: 16, t: 12, b: narrow ? 40 : 34 } and translate the rotated title to x = 10 instead of 14 (cumCtx.translate(10, pad.t + ph / 2)).

    3. Legend polish
    • Spotlight legend: "Days since mainshock" and "0 · 10 · 1000 d" each wrap onto two lines. In renderLegend(), for the spotlight branch give the row an extra class legend-days and use the labels 0 d and 1000 d on either side of the bar: <span>Days since mainshock</span><span>0 d</span><div class="legend-bar" …></div><span>1000 d</span>. In src/app.css add: .legend-days { flex-wrap: wrap; row-gap: 4px; } .legend-days > span:first-child { flex-basis: 100%; }

    • Magnitude legend: the biggest circle covers its "6" label. In the magnitude <svg> change height="26" to height="36", viewBox="0 0 90 26" to viewBox="0 0 90 36", and all three <text … y="25" to y="34".

What the agent was asked

The full specification the harness received, rendered from the original Markdown.

Read the specification

Idaho Seismic Atlas — Earthquakes in and around Idaho

Category: Scientific visualization Who it is for: Geology courses, emergency management, curious Idahoans

What it should do

Replay decades of earthquakes across Idaho — including the 2020 Stanley M6.5 sequence — on an animated map with magnitude, depth and aftershock statistics.

Data

Real USGS ComCat earthquake catalog (earthquake.usgs.gov/fdsnws/event/1/query, GeoJSON) for a bounding box around Idaho (lat 41.9–49.1, lon −117.3 to −110.9), M≥2.0, 1980–present, plus an Idaho state outline and county boundaries (e.g. US Census cartographic boundary GeoJSON), downloaded at build time.

Raw downloads (if any) are in data/raw/ with data/raw/SOURCES.md describing where each file came from. Transform what the app needs into compact JSON in data/embed/ (write a small script in scripts/ to do it, so the transformation is reproducible). Every dataset the app shows must be credited in a visible "Data & sources" section of the app, with links. If a dataset is illustrative/synthetic, label it clearly as such in the UI — never present made-up numbers as real University of Idaho figures.

Required features

  • Map of Idaho drawn in SVG/canvas (simple equirectangular or Albers projection coded by hand) with county lines, quakes as circles sized by magnitude and colored by depth

  • Timeline slider + play button that animates the catalog through time, with a cumulative-count chart that follows

  • Brush a region on the map to filter; details on hover/click (time, magnitude, depth, place)

  • Gutenberg–Richter magnitude-frequency plot with a fitted b-value for the current selection

  • Spotlight button for the 2020 Stanley sequence (Omori aftershock decay chart)

  • Source/citation panel

Quality bar

This app is going into a University of Idaho gallery that shows faculty, staff and students what they can build with AI coding agents. It must look polished and modern (think a well-designed data product, not a homework demo), feel alive (smooth transitions, responsive hover/selection states, animated where it helps understanding), and do real, useful work — every control should change something meaningful. It must work well on a laptop and on a phone. Include a short "How to use" hint for first-time visitors and a helpful empty state wherever the user must supply data.

Known limitations

Reported by the independent critic. ✎ written by the AI critic

  • With the Stanley spotlight on, scrubbing or playing to a date before the 2020 mainshock leaves the map empty ('0 shown'), because the spotlight only draws events after the mainshock. There is no hint explaining this.
  • Gutenberg–Richter chart: the cumulative dots are drawn at bin centre (M+0.05) while the fit line uses the Mc−0.05 convention, so dots and line are 0.1 magnitude apart. The y-axis also runs a decade above the data (to 100,000).
  • The Omori fit uses every event within 60 km over about 6 years, so background seismicity pulls p down to ≈0.61 (typical aftershock sequences are near 1). The card does not mention this.
  • The GR chart and b-value follow the region selection but not the timeline position or the spotlight. This matches the spec's 'current selection' wording, but visitors may expect the spotlight to show the Stanley sequence's own b-value.
  • Small desktop map artefact: a faint lighter strip along the bottom-left edge of the map frame.
  • On the phone the 'Selected quake' card sits below the map, so a tapped quake's details start near the bottom of the screen.
  • Harness weakness: Goose looped on one repeated grep in rounds 14 (236 repeats, no edits) and 15 (191 repeats after its edits), and I killed both as stuck. Round 15's fix also broke the 90-day Stanley replay; round 16 fixed it.
  • verify_app still reports empty_canvases: 1. That is a false positive: it samples a transparent corner of the quake overlay canvas.