seriffic Claude Opus 4.7 (1M context) commited on
Commit
79cf005
Β·
1 Parent(s): fb54991

docs: replace handoff bundle with v0.4.5 spec

Browse files

Bumps docs/design_handoff/ from v0.4.4 to v0.4.5. Drops in:

V0.4.5_SPEC.md β€” nine polish deltas on top of shipped v0.4.4:
1. Status semantics β€” split `error` into 5 states
(fired / silent_by_design / warned / errored / not_invoked)
2. Capstone meta-card field-mapping (rerolls, grounding pass
count, citations resolved, wall-clock)
3. Provenance roster: full inventory per Stone, not_invoked
slots rendered explicitly
4. Touchstone gains TerraMind LULC + Prithvi-NYC-Pluvial cards
5. Lodestone gains the fine-tuned TTM card alongside zero-shot
6. Drop the "anomaly" tag (redundant after #1)
7. LAYERS panel restructured by Stone
8. Card-to-map hover linking + fitBounds() on register click
9. Stone-tinted accent tokens (5 muted hint colors, 3px left-rule
on Stone headers, 6px dot in cold-start, print β†’ #999)

CLAUDE_CODE_PROMPT.md β€” points at v0.4.5 spec; v0.4.4 README stays
as reference for everything not changing.

Riprap Stone-Grouped UI v0.4.5.html β€” the new visual target.
v0.4.4 HTML kept alongside for diff/reference only.

findings.jsx / stones-trace.jsx / map.jsx / shell.jsx / styles.css
/ tokens.css updated to match v0.4.5 deltas.

The implementation pass lands in subsequent commits on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

docs/design_handoff/CLAUDE_CODE_PROMPT.md CHANGED
@@ -1,39 +1,43 @@
1
- # Prompt for Claude Code
2
 
3
- Copy-paste this whole block into Claude Code as your opening message. It frames the task and points at the files in this bundle.
4
 
5
  ---
6
 
7
- You're implementing a high-fidelity design into the existing Riprap codebase, which is **SvelteKit**. The design is a citation-grounded Flood Exposure Briefing UI for NYC, with the new "Findings region" as the centerpiece.
8
 
9
- The design lives in `design_handoff_riprap_findings/`. **Read `README.md` first**, then the `design_files/` directory. The HTML/JSX prototypes there are React-based references β€” your job is to **port them to Svelte 5 components** using runes (`$state`, `$derived`, `$props`) and the project's existing patterns. Do not copy JSX or React idioms; recreate the same visual + interaction grammar in idiomatic Svelte.
10
 
11
- **Order of work:**
12
 
13
- 1. **Tokens first.** Port `design_files/tokens.css` verbatim into the app's global stylesheet (or wherever design tokens live in the existing codebase). The four epistemic tier colors (`--tier-empirical/modeled/proxy/synthetic`), the paper-register neutrals, the IBM Plex font stack, and the spacing scale are non-negotiable β€” every component below references them.
 
 
14
 
15
- 2. **Card grammar.** Build the 12 card-body variants documented in Β§"Card grammar" of the README as small leaf components: `<HeadlineCard>`, `<TabularCard>`, `<ScalarsCard>`, `<SparkCard>`, `<HistogramCard>`, `<TimeseriesCard>`, `<ForecastCard>`, `<RasterCard>`, `<RasterPredCard>`, `<RegisterCard>`, `<ComparisonCard>`, `<MetaCard>`. Each takes a `card` prop matching the schema in Β§"Card data schema". A wrapper `<FindingCard>` renders the chrome (header strip, tier glyph, source, agency, footer with tier badge) and slots the body. Synthetic cards get a dashed top-rule.
16
 
17
- 3. **Stone region.** A `<StoneRegion>` component groups cards by Stone (cornerstone / keystone / touchstone / lodestone / capstone). Header has the Stone name (serif italic), role tagline, run-tally chip, and a provenance toggle. Provenance trace renders below the header per the smart-default rules in Β§"Provenance trace".
 
 
 
18
 
19
- 4. **Findings region.** Composes 5 `<StoneRegion>`s in fixed order, with a `<RunHealthStrip>` above them and (optionally) `<CardGrammarReference>` below. Wire props through: `density`, `provenanceMode`, `queryKey`, `showComparison`, `showGrammar`.
20
 
21
- 5. **Cross-component linking.** Hovering a card sets `linkedKey` (Svelte store or `$state` lifted to the page). The briefing's map frame highlights the matching layer. See Β§"Hover linking" in the README.
 
 
 
 
 
 
 
 
22
 
23
- 6. **Briefing + map + trace.** Once Findings is solid, port `briefing.jsx`, `map.jsx`, and `trace.jsx` similarly. The briefing is a long-form text region with inline citations whose hover state lights up the corresponding map layer.
24
 
25
- **Conventions to follow (read these before writing code):**
 
 
 
26
 
27
- - **Svelte 5 runes only.** No legacy `let`-as-state, no stores unless cross-route. Lift state to `+page.svelte` and pass via `$props()`.
28
- - **No emoji.** No icon font. Tier glyphs are inline SVG; see `glyphs.jsx` for the four shapes (filled square / open square / dotted ring / hatched square).
29
- - **No Tailwind.** This codebase uses scoped `<style>` blocks per component. Reuse the token CSS variables from step 1 β€” don't redeclare colors or spacing.
30
- - **No animation libraries.** All transitions are CSS, ≀200ms, and respect `prefers-reduced-motion` (already covered by the global rule in `tokens.css`).
31
- - **Accessibility is a hard requirement.** Tier is encoded by *color + glyph + label* (never color alone). Focus rings are 3px accent. Every interactive card needs `aria-label`. Provenance toggles are buttons with `aria-expanded`.
32
-
33
- **What to ignore in the bundle:**
34
-
35
- - The `Riprap Landing*.html` files are exploratory marketing-page variants, not part of this handoff. Port them only if explicitly asked.
36
- - `design-canvas.jsx`, `tweaks-panel.jsx`, `landing-variants.*` are prototype-time tooling, not product code.
37
- - The React component files (`*.jsx`) are reference implementations. Use them to read structure, copy strings/numbers, and verify visual fidelity β€” don't transpile them.
38
-
39
- **When in doubt, open the HTML file in a browser** (`Riprap Stone-Grouped UI v0.4.4.html`) and compare your Svelte output side-by-side. Pixel parity on the Findings region is the bar.
 
1
+ # Prompt for Claude Code Β· v0.4.5 polish session
2
 
3
+ Copy-paste this whole block into Claude Code as your opening message.
4
 
5
  ---
6
 
7
+ v0.4.4 has shipped to the local development environment. The Findings region with the Five Stones, evidence cards, and Capstone meta-card are live and rendering against real queries via local uvicorn (`uvicorn web.main:app --host 127.0.0.1 --port 7860`). Screenshots from 80 Pioneer Street, Red Hook confirm the architecture working: four Stones producing genuinely different evidence-card kinds, each carrying its tier badge, the briefing prose with citations resolving cleanly, and the map with three tier-encoded layers.
8
 
9
+ **v0.4.5 is polish.** Nine specific issues observed in production-shaped local runs, plus a new Stone-tinted light-theming layer. **None of the fixes are structural. Don't rebuild anything.** Read the current implementations of `src/lib/components/findings/StoneRegion.svelte`, `src/lib/components/findings/EvidenceCard.svelte`, and `src/lib/components/findings/CapstoneCard.svelte` and apply the deltas described in `V0.4.5_SPEC.md`.
10
 
11
+ **Important context.**
12
 
13
+ - Public mirrors (GitHub, HF Space) were deleted pre-hackathon for caution and re-publish during the hackathon window. **The HF Space link is currently delisted; do not reference it.** All references in this work target local development.
14
+ - The codebase is **SvelteKit (Svelte 5 with runes)**. Stay in idiomatic Svelte: `$state`, `$derived`, `$props`, scoped `<style>` blocks. No React. No Tailwind. No animation libraries.
15
+ - The card grammar (header / body / footer / tier badge / source link), the four-section briefing prose, the Mellea reroll status strip, the four-tier color palette and glyphs, the cold-start state, the trust-signal footer, and the PDF template's core layout are **not changing**. Don't touch them.
16
 
17
+ **Read these files first, in order:**
18
 
19
+ 1. `V0.4.5_SPEC.md` β€” the nine fixes, with file-level deltas, expected outputs, and acceptance criteria.
20
+ 2. `README.md` β€” v0.4.4 spec for context (card grammar, tier system, data schema, hover-link contract). Reference, not rework.
21
+ 3. `design_files/Riprap Stone-Grouped UI v0.4.5.html` β€” open in browser; this is the **visual target** with all nine v0.4.5 deltas applied.
22
+ 4. `design_files/Riprap Stone-Grouped UI v0.4.4.html` β€” kept only for diff/reference against the previously shipped state.
23
 
24
+ **Order of work** (matches `V0.4.5_SPEC.md` priority):
25
 
26
+ 1. **Status semantics first.** Split the FSM specialist status into `fired / silent_by_design / warned / errored / not_invoked`. Update the per-Stone summary and top tally aggregate counts to render the breakdown. Update provenance row visual treatment per status. This is the most important fix β€” it's the one actively misrepresenting system integrity.
27
+ 2. **Capstone meta-card field plumbing.** Wire the four metrics to the reconciler's actual state fields. Acceptance: a clean Red Hook run shows `1 reroll Β· 4/4 grounding Β· 4 citations Β· 24.0s`.
28
+ 3. **Provenance roster completeness.** Each Stone's expander always shows the full specialist inventory, never a filtered subset. Missing specialists render as `not_invoked` with one-line reasons.
29
+ 4. **Touchstone card additions** (TerraMind LULC, Prithvi-NYC-Pluvial). If the specialists aren't firing yet, land the card components anyway so they're ready when the data is.
30
+ 5. **Lodestone fine-tuned TTM card.** Add alongside the existing zero-shot card. Footer must include the HF model-card link, RMSE, and AMD MI300X badge.
31
+ 6. **Drop the "anomaly" tag.** The new status counts make it redundant.
32
+ 7. **LAYERS panel restructure.** Group by Stone, add the four new raster layers (default-off), include the explicit "no map layers β€” see Findings cards" label under Lodestone.
33
+ 8. **Card-to-map hover linking.** Verify and ship the connection. Click-to-fitBounds() on register cards is new in v0.4.5.
34
+ 9. **Stone-tinted accent colors.** Add the five `--stone-*` tokens with the proposed values (designer can adjust within constraints). Apply at the recommended placements: 3px left-rule on Stone region headers, 6px dot beside Stone names in the cold-start list, optional row tint on the methodology matrix. Print-media override drops all five to `#999`.
35
 
36
+ **What to verify before reporting done.**
37
 
38
+ - A query at 80 Pioneer Street, Red Hook produces a Findings region matching the "v0.4.5 ready looks like" section at the bottom of `V0.4.5_SPEC.md`.
39
+ - No regressions in the briefing prose, the Mellea status strip, the cold-start, the footer, the PDF template, or the existing map layers.
40
+ - New status messages match the engineering-honest voice in `V0.4.5_SPEC.md` Β§1 (no euphemism β€” `"no entrances within radius"`, not "no data found").
41
+ - Print stylesheet drops Stone tints to neutral gray.
42
 
43
+ A v0.4.5 implementation session is a few hours of focused polish, not a day. The structural work is already done.
 
 
 
 
 
 
 
 
 
 
 
 
docs/design_handoff/README.md CHANGED
@@ -1,4 +1,12 @@
1
- # Handoff: Riprap Findings Region (v0.4.4)
 
 
 
 
 
 
 
 
2
 
3
  ## Overview
4
 
@@ -323,9 +331,65 @@ design_handoff_riprap_findings/
323
 
324
  ## Scope
325
 
326
- **In scope** for this handoff: the Findings region (5 Stones, 12 card variants, run-health strip, smart provenance, hover linking, card grammar reference). Briefing prose and map are in scope to the extent they connect to Findings via `linkedKey`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
- **Out of scope**: the cold-start state, the marketing landing page, the methodology PDF, the export-PDF flow. Port these later.
329
 
330
  ## Open questions for the design team
331
 
 
1
+ # Handoff: Riprap Findings Region
2
+
3
+ **Current target version: v0.4.5** (polish on v0.4.4 β€” now applied in `Riprap Stone-Grouped UI v0.4.5.html`). See `V0.4.5_SPEC.md` for the nine deltas. This README is the v0.4.4 specification and remains the reference for everything *not* changing in v0.4.5.
4
+
5
+ **Read order for the implementer:**
6
+ 1. `CLAUDE_CODE_PROMPT.md` β€” paste into Claude Code
7
+ 2. `V0.4.5_SPEC.md` β€” the nine fixes (now realized in `v0.4.5.html`)
8
+ 3. `README.md` (this file) β€” v0.4.4 reference
9
+ 4. `design_files/` β€” prototypes (both v0.4.4 and v0.4.5 HTMLs are present)
10
 
11
  ## Overview
12
 
 
331
 
332
  ## Scope
333
 
334
+ **In scope** for this handoff:
335
+
336
+ - The Findings region (5 Stones, 12 card variants, run-health strip, smart provenance, hover linking, card grammar reference)
337
+ - Briefing prose and map, to the extent they connect to Findings via `linkedKey`
338
+ - The marketing **landing page** (see Β§"Landing page" below)
339
+ - v0.4.5 deltas in `V0.4.5_SPEC.md`
340
+
341
+ **Out of scope**: the methodology PDF, the export-PDF flow.
342
+
343
+ ## Landing page
344
+
345
+ The landing page is the public-facing entry point at `/` (separate from the app's cold-start state at `/app`). Two design files in `design_files/`:
346
+
347
+ - `Riprap Landing.html` β€” final shipping landing page
348
+ - `Riprap Landing Variants.html` β€” three exploratory variants on a design canvas (kept for reference; do not port)
349
+
350
+ ### Landing structure (port `Riprap Landing.html`)
351
+
352
+ Four-section vertical scroll, max-width 1200px, paper background:
353
+
354
+ 1. **Hero**
355
+ - Wordmark top-left (with `β–Œ` accent prefix)
356
+ - Headline (52px serif, italic emphasis on *any place*, line-broken into two lines)
357
+ - Deck (18px sans, max 70ch, ink-secondary)
358
+ - Big query box: "Try:" label + cycling example queries on a fixed dotted-underline rail (rail width pinned, ellipsis fallback for overflow)
359
+ - Submit button (ink fill, paper text, mono caps)
360
+
361
+ 2. **"What you'll get back" preview** β€” 3-pane grid (1.4fr / 1fr / 1fr), bottoms equalized:
362
+ - **Excerpt pane**: serif briefing snippet with inline `[N]` citation pins; compact source list at bottom (tier-coded)
363
+ - **Evidence cards pane**: 2x2 grid of compact evidence cards, each with tier-coded left rule (2px), claim, source
364
+ - **Map pane**: mini SVG map (240x200, 8px paper-grain grid texture) with FEMA-AE polygon fill, HWM contour, FloodNet sensor pin, 311 cluster, address pin, and a compact tier-legend overlay bottom-edge
365
+
366
+ 3. **Five Stones strip** β€” explanation grid: 5 columns (`repeat(5, 1fr)`), one cell per Stone:
367
+ - Oversized italic-serif numerals `01..05` top-right of each cell (in `--rule-soft`, decorative)
368
+ - Stone name (serif 22px / 500)
369
+ - Role tagline (sans 13px / ink-secondary)
370
+ - Italic-serif tag ("what NYC's ground remembers" etc.)
371
+ - Dashed rule + mono source list at bottom
372
+
373
+ 4. **Footer** β€” earns its keep:
374
+ - Tier legend (4 swatches: empirical / modeled / proxy / synthetic)
375
+ - Build line (mono, ink-tertiary)
376
+
377
+ ### Landing β€” what's settled (don't redesign)
378
+
379
+ - The hero dropped the redundant "Riprap" eyebrow; the wordmark already says it. Headline carries the lede.
380
+ - Cycling examples ride a single fixed dotted underline (no jump on length change). Pin the rail width; ellipsis-truncate overflow.
381
+ - Pane heights are equalized via a flex-1 spacer in the cards pane and `flex: 1` on the map pane.
382
+ - Map texture is a subtle 8px paper-grain grid behind the colored layers β€” gives it a map register, not a diagram register.
383
+ - Stones strip uses oversized italic numerals as a typographic register (decorative, in soft-rule color).
384
+ - Italic serif is intentional: hero emphasis + Stone tags + Stone numerals. Reads as a deliberate third voice.
385
+
386
+ ### Landing β€” em-dashes
387
+
388
+ All user-facing em-dashes have been purged from `Riprap Landing.html`, `landing-variants.css`, and `landing-variants.jsx`. Replacement convention: `, ` (comma-space) for em-dashes used as parenthetical breaks. **Maintain this convention** when porting copy. Em-dashes in source-code comments (non-rendered) are fine.
389
+
390
+ ### Landing β€” out of scope variants
391
 
392
+ `Riprap Landing Variants.html` contains three earlier explorations on a design canvas: v1 (minimal pushed harder), v2 (example gallery), v3 (methodology-forward). Kept for reference only; **do not port**. The shipping landing page is `Riprap Landing.html` (a refinement of v1).
393
 
394
  ## Open questions for the design team
395
 
docs/design_handoff/V0.4.5_SPEC.md ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Riprap v0.4.5 Β· Polish on v0.4.4 (Findings region)
2
+
3
+ **Status**: design spec, ready for Claude Code implementation.
4
+ **Frame**: surgical polish on the live local SvelteKit app. Nothing structural changes. Reuse v0.4.4 components; apply nine deltas.
5
+
6
+ The v0.4.4 Findings region (five Stones, evidence cards, raster thumbnails, time-series, Capstone meta-card) is shipping correctly. Real queries (e.g., 80 Pioneer Street, Red Hook) render Stones producing genuinely different evidence-card kinds, with provenance collapsing under each Stone, the briefing prose with citations resolving cleanly, and the map with three tier-encoded layers and the address pin. The work below corrects nine specific issues observed in production-shaped local runs.
7
+
8
+ ## What v0.4.5 must NOT change
9
+
10
+ - Card grammar (header / body / footer / tier badge / source link)
11
+ - The four-section briefing prose structure
12
+ - The Mellea reroll status strip ("Regenerating to satisfy citation grounding Β· attempt N of N Β· previous draft dimmed below")
13
+ - The four-tier color palette and glyphs
14
+ - The cold-start state with the v0.4.3 Stones one-liner
15
+ - The trust-signal footer
16
+ - The PDF template's core layout (Stone color print handling is the only new consideration)
17
+ - The map's base style and the existing layer rendering for Sandy / FEMA / 311
18
+
19
+ ## What v0.4.5 must NOT introduce
20
+
21
+ - New card body variants beyond what v0.4.4 specified
22
+ - Card-level decoration competing with tier badges (Stone color hints are at the Stone region level β€” at the card level only as a designer-chosen subtle touch like a tinted left-border)
23
+ - Animations on card render
24
+ - New typography or spacing tokens unless something genuinely new emerges from the fixes
25
+ - Mascots, icons, or thematic visual associations for individual Stones beyond their color hint
26
+
27
+ ---
28
+
29
+ ## 1. Status semantics: split conflated states
30
+
31
+ **Problem.** The top tally currently reads "5 Stones Β· 15/18 functions fired Β· 9 evidence cards Β· 24.0s Β· 3 error" on a query where Mellea grounding passed 4/4 and the briefing came out clean. Two of those three "errors" are not errors at all:
32
+
33
+ - Keystone's `mta_entrance_exposure` returned "no entrances within radius" β€” that's the specialist working correctly. Absence of nearby subway entrances at 80 Pioneer is a true and useful finding, not a failure.
34
+ - Lodestone's `floodnet_forecast` hit its silent-floor (sensor has only 2 historical events, <5 required) β€” the four-tier discipline working as intended.
35
+ - Lodestone's `ttm_311_forecast` actually failed (311 history fetch error). That is the only real error.
36
+
37
+ Three different epistemic states are being rendered as one red square.
38
+
39
+ **Fix.** Split the FSM specialist status into five values:
40
+
41
+ | status | meaning | provenance row treatment | counts toward |
42
+ |---|---|---|---|
43
+ | `fired` | completed and produced output the reconciler used | tier-colored square | "fired" tally |
44
+ | `silent_by_design` | completed and correctly produced no output | open square in neutral tone, italicized message | "silent" tally |
45
+ | `warned` | output produced with a non-fatal warning | tier-colored square + small warn sidemark | "fired" tally + "warnings" count |
46
+ | `errored` | failed to complete, no usable output | red square; 1-line collapsed summary; full trace behind click-to-expand (v0.4.2 drilldown pattern) | "errored" tally |
47
+ | `not_invoked` | FSM skipped the specialist (precondition unmet) | hollow gray square; one-line reason | "not invoked" count |
48
+
49
+ **Aggregate count rules.** Top tally:
50
+ ```
51
+ 5 Stones Β· 15 fired Β· 5 silent Β· 1 errored Β· 24.0s
52
+ ```
53
+ (no more rolled-up "errors" number)
54
+
55
+ Per-Stone summary (replaces "0 cards Β· 0 fired Β· 1 error Β· 30ms"):
56
+ ```
57
+ Keystone Β· 0 cards Β· 5 silent Β· 30ms
58
+ Lodestone Β· 1 card Β· 3 fired Β· 1 silent Β· 1 errored Β· 1.5s
59
+ ```
60
+
61
+ **Status messages β€” voice.** Engineering-honest, no euphemism. Examples to copy:
62
+ - `"no entrances within radius"`
63
+ - `"sensor has only 2 historical events; forecast omitted (silent-floor: 5)"`
64
+ - `"PLUTO join skipped: queried address not in NYC PLUTO dataset"`
65
+ - `"311 history fetch failed: HTTP 503 at NYC OpenData (3 retries)"`
66
+
67
+ Match v0.4.1–v0.4.4 voice β€” precise, slightly understated, comfortable with technical detail, engineer-to-engineer.
68
+
69
+ **This is the most important fix in v0.4.5.** It is the one actively misrepresenting system integrity. Do this first.
70
+
71
+ ---
72
+
73
+ ## 2. Capstone meta-card field-mapping
74
+
75
+ **Problem.** The Capstone meta-card displays:
76
+ ```
77
+ mellea reroll: 0 attempts
78
+ grounding checks: 0/4 passed
79
+ citations resolved: 0
80
+ wall-clock: 24.0 s
81
+ ```
82
+
83
+ But the briefing has 4 resolved citations ([1] Sandy, [2] NYC311, [3] Ida, [4] Microtopo); the Mellea status strip explicitly shows "attempt 2 of 2"; the briefing rendered clean (so grounding passed). Three of four metrics are showing zero on a clean run. State plumbing, not logic.
84
+
85
+ **Fix.** Wire the four metrics to the reconciler's actual state fields:
86
+
87
+ | display | reads from | expected for this query |
88
+ |---|---|---|
89
+ | `mellea reroll: N attempts` | per-query reroll count from Mellea (likely `melleaState.rerollCount`) | `1` (one reroll on top of initial) |
90
+ | `grounding checks: N/4 passed` | per-query grounding-check results array β†’ count true | `4/4` |
91
+ | `citations resolved: N` | count of resolved citations in the final briefing payload | `4` |
92
+ | `wall-clock: NN.N s` | already correct β€” no change | `24.0 s` |
93
+
94
+ **Acceptance.** The Capstone meta-card on a clean Red Hook run shows `1 / 4/4 / 4 / 24.0 s`. On a failed-grounding run it shows the actual numbers (e.g. `2 / 3/4 / 4 / 31.0 s`) so the meta-card honestly reports what happened.
95
+
96
+ The Capstone meta-card is the integrity-narration UI for the entire pipeline. Zero on a clean run undersells the system's integrity. Accurate numbers turn it into a proof point β€” "this query went through 1 reroll to satisfy 4/4 grounding checks, resolved 4 citations to primary sources, and took 24 seconds end-to-end."
97
+
98
+ ---
99
+
100
+ ## 3. Provenance roster: always show full inventory
101
+
102
+ **Problem.** Expanded Keystone provenance shows only `step-16 Β· mta_entrance_exposure β€” no entrances within radius (30ms)`. Keystone fires five specialists (MTA, NYCHA, DOE, DOH, PLUTO). Four are missing from the UI.
103
+
104
+ **Fix.** Each Stone's provenance expander shows the **complete roster** of specialists the Stone *could have fired*, with one row per specialist and its status from Β§1. A reader who expands a Stone sees the full inventory β€” never a filtered subset. This is the auditability contract.
105
+
106
+ If a specialist genuinely didn't run, that's `not_invoked` with a one-line reason. The reader sees the intended roster and understands every absence.
107
+
108
+ **Implementation.** The Stone definition (in `src/lib/data/stones.ts` or wherever the registry lives) should list its full specialist roster. The provenance component renders one row per registry entry, joining against the run's actual specialist outputs. Specialists missing from the run output β†’ `not_invoked` status, one-line reason from the FSM's skip log.
109
+
110
+ ---
111
+
112
+ ## 4. Touchstone β€” five cards, not three
113
+
114
+ **Problem.** Touchstone renders three cards (FloodNet, NYC 311, NOAA CO-OPS). v0.4.4 specified five: those three plus **TerraMind LULC** (SYN tier) and **Prithvi-NYC-Pluvial** (MOD tier).
115
+
116
+ **Fix.** Wire the structured outputs of `step_terramind_lulc` and `step_prithvi_live` (now Prithvi v2) to Touchstone cards.
117
+
118
+ **TerraMind LULC card** (Touchstone, SYN tier β€” synthetic prior, dashed top-rule):
119
+ - Title: `"Land use / land cover Β· TerraMind v1.2"`
120
+ - Body variant: existing `raster` thumbnail (240Γ—120, segmentation overlay) + a **horizontal stacked class-mix bar** below the thumbnail showing percentage by LULC class. Use the conventional LULC palette (urban / water / vegetation / barren / wetland) β€” those colors are visual conventions for the layer itself, not new tier signals.
121
+ - Source: `TerraMind v1.2`
122
+ - Agency: `IBM TerraMind v1.2 Β· Sentinel-2 inputs`
123
+ - Vintage: latest Sentinel scene date for the AOI (e.g. `Sentinel-2 Β· 2024-09-18`)
124
+ - `mapLayer: "terramind-lulc"`
125
+
126
+ **Prithvi-NYC-Pluvial card** (Touchstone, MOD tier):
127
+ - Title: `"Pluvial flood prediction Β· Prithvi-NYC-Pluvial"`
128
+ - Body variant: `raster` thumbnail (flood-mask overlay) **plus a headline scalar above** showing flood percentage of AOI.
129
+ - Source: `Prithvi-NYC-Pluvial`
130
+ - Agency: `NASA-IBM Prithvi v2 Β· NYC fine-tune`
131
+ - Vintage: prediction time + Sentinel scene date
132
+ - `mapLayer: "prithvi-pluvial"`
133
+
134
+ **Layout.** v0.4.4's horizontal scrolling treatment handles five cards on wide viewports; on narrow viewports they stack vertically as designed. No new layout work.
135
+
136
+ **If the specialists aren't firing yet** (UPDATE_STONES.md commits 4 + 5 may still be in flight): surface that as a backend issue, but the card definitions land in v0.4.5 so they're ready when the data is.
137
+
138
+ ---
139
+
140
+ ## 5. Lodestone β€” both TTM cards, not just the zero-shot
141
+
142
+ **Problem.** Lodestone shows one time-series card: zero-shot Granite TTM r2 surge nowcast (9.6h, 6-min cadence). The footer correctly notes "Distinct from the fine-tuned Battery surge nowcast" β€” but that fine-tuned card is missing.
143
+
144
+ **Fix.** Add the fine-tuned TTM card. Both cards live in Lodestone:
145
+
146
+ | | Zero-shot card (existing) | Fine-tuned card (new) |
147
+ |---|---|---|
148
+ | Title | `"Storm surge nowcast at The Battery β€” 9.6 h horizon (regional)"` | `"Storm surge nowcast at The Battery β€” 96 h horizon (NYC-specialized fine-tune)"` |
149
+ | Horizon | 9.6 h | 96 h |
150
+ | Cadence | 6-min | hourly |
151
+ | Tier | MOD | MOD |
152
+ | Body | timeseries (existing) | timeseries (96-h forecast) |
153
+ | Source label | `Granite TTM r2 (zero-shot)` | `msradam/Granite-TTM-r2-Battery-Surge` |
154
+ | Footer extras | "regional disclosure" tag | model-card link to HF artifact: `huggingface.co/msradam/Granite-TTM-r2-Battery-Surge`, RMSE `0.157 m`, `βˆ’35% vs persistence`, AMD MI300X badge |
155
+
156
+ The fine-tuned card is a load-bearing piece of the Riprap-on-AMD story for the hackathon submission. Its presence as a Lodestone card alongside the zero-shot card is the visible "this system uses NYC-specialized fine-tuned models you can verify on HuggingFace" claim that closes the loop.
157
+
158
+ `step_ttm_battery_surge` in production trace fires at ~1.5s with `context_h=1024 Β· horizon_h=96`. If it's firing but not producing a structured output the card layer can render, surface that as a backend issue before v0.4.5 implementation.
159
+
160
+ ---
161
+
162
+ ## 6. The "anomaly" tag
163
+
164
+ **Problem.** Provenance expanders show `Hide provenance Β· 1 function Β· anomaly` (Keystone) and `Hide provenance Β· 5 functions Β· anomaly` (Lodestone). Overloaded label: doing different work in each context, partially duplicating row-level info.
165
+
166
+ **Decision: drop the "anomaly" tag.**
167
+
168
+ After Β§1 lands, Stone summaries say `Keystone Β· 0 cards Β· 5 silent Β· 30ms` (expected behavior β€” no exposed assets at this address) or `Lodestone Β· 1 card Β· 3 fired Β· 1 silent Β· 1 errored Β· 1.5s` (one specialist errored, visible in the count). The status counts make "anomaly" redundant. Less is more.
169
+
170
+ (Alternative β€” retained for record but not recommended: keep "anomaly" and define it as "Stone-level outcome differs from typical: 0 cards landed despite specialists firing, OR β‰₯1 specialist errored, OR Stone retried." If chosen, document the rule and apply mechanically. But Β§1's count breakdown carries the same information without the extra label.)
171
+
172
+ ---
173
+
174
+ ## 7. LAYERS panel β€” restructure to mirror Stones
175
+
176
+ **Problem.** Production shows the existing three layers (Sandy / FEMA-DEP / 311) in a flat LAYERS panel without Stone grouping. The four new raster layers (TerraMind LULC, TerraMind Buildings, Prithvi-NYC-Pluvial) don't have a home.
177
+
178
+ **Fix.** Restructure the LAYERS panel to mirror the Findings Stones structure:
179
+
180
+ ```
181
+ LAYERS
182
+
183
+ β–Ύ Cornerstone β€” what NYC's ground remembers
184
+ β—§ Sandy Inundation Zone (2012) EMP [on]
185
+ β—¨ FEMA / DEP scenarios MOD [on]
186
+ β—§ Ida HWM points (2021) EMP [off]
187
+ β–₯ Microtopography (HAND/TWI) PRX [off]
188
+
189
+ β–Ύ Keystone β€” what's exposed
190
+ β—‰ MTA subway entrances EMP [on]
191
+ β–­ NYCHA developments EMP [on]
192
+ βœ• DOE schools EMP [on]
193
+ ● DOH hospitals EMP [on]
194
+ β–¦ TerraMind Buildings (current) SYN [off]
195
+
196
+ β–Ύ Touchstone β€” what's happening now
197
+ ● 311 flood complaints PRX [on]
198
+ β—‰ FloodNet sensors EMP [on]
199
+ β–₯ TerraMind LULC (current) SYN [off]
200
+ β–₯ Prithvi-NYC-Pluvial flood pred. MOD [off]
201
+
202
+ β–Ύ Lodestone β€” what's coming
203
+ (no map layers β€” see Findings cards)
204
+
205
+ β–Ύ Capstone β€” synthesis
206
+ (not a map layer)
207
+ ```
208
+
209
+ **New layer specs**:
210
+ - **TerraMind LULC** (SYN tier): conventional LULC palette (urban/water/vegetation/barren/wetland). Default off. Label includes Sentinel scene date.
211
+ - **TerraMind Buildings** (SYN tier): synthetic-prior glyph. Default off.
212
+ - **Prithvi-NYC-Pluvial** (MOD tier): Modeled-tier color treatment. Label includes Sentinel scene date.
213
+
214
+ The "no map layers β€” see Findings cards" label under Lodestone is explicit by design: the TTM Battery Surge is a Lodestone card, not a map layer. Naming the absence prevents the reader from looking for it.
215
+
216
+ ---
217
+
218
+ ## 8. Card-to-map hover linking
219
+
220
+ **Problem.** v0.4.4 specified hover-to-highlight from card β†’ map element. Production may or may not have this wired.
221
+
222
+ **Fix.** Verify and ship the connection:
223
+
224
+ | Card type | Map link | Hover treatment |
225
+ |---|---|---|
226
+ | FEMA card | FEMA AE polygon | layer fill opacity bump (0.4 β†’ 0.6), 100ms |
227
+ | HWM tabular | HWM contour + points | line weight 1px β†’ 2px |
228
+ | Register cards (NYCHA, schools, etc.) | corresponding pins | pins gain 2px accent ring; on click, fitBounds() |
229
+ | FloodNet sensor card | sensor pin | pin glow + accent outline |
230
+ | Raster prediction (TerraMind, Prithvi) | the matching raster layer | layer fill opacity bump |
231
+ | Address card | address pin | pin pulse (single, 200ms) |
232
+ | TTM Battery Surge | (none β€” not spatial) | no map behavior |
233
+ | Capstone meta-card | (none) | no map behavior |
234
+
235
+ **Implementation.** Page-level `linkedKey` state (already specced in v0.4.4 README). Cards set it on `pointerenter` / `focus`, clear on `pointerleave` / `blur`. Map watches `$derived` of `linkedKey` and applies layer-specific class + label badge ("linked: {layer}") bottom-right.
236
+
237
+ Click-to-fit-bounds for register cards is new in v0.4.5: when clicking a register row inside a register card, the map calls `fitBounds()` on the affected feature(s) with 80px padding, 400ms easing.
238
+
239
+ ---
240
+
241
+ ## 9. Stone-tinted accent colors β€” light theming
242
+
243
+ **Problem.** v0.4.3/v0.4.4 prohibited per-Stone color coding to avoid competing with the four-tier epistemic palette. That prohibition is being relaxed: each Stone gets a single muted accent color the design system can apply as a hint.
244
+
245
+ **Five Stone accent tokens** (proposed values β€” designer can adjust within the constraints below):
246
+
247
+ | token | hex | name | rationale |
248
+ |---|---|---|---|
249
+ | `--stone-cornerstone` | `#7C6F5E` | warm taupe | grounded, soil-adjacent without being literally brown |
250
+ | `--stone-keystone` | `#5E6E7C` | cool slate | structural / architectural reading |
251
+ | `--stone-touchstone` | `#6B7C66` | muted sage | present-tense, alive without being signal-green |
252
+ | `--stone-lodestone` | `#7C6E5E` | softened ochre | forward-pointing warmth without competing with `--accent` |
253
+ | `--stone-capstone` | `#5E5E6E` | neutral indigo-gray | synthetic, cool, narrative |
254
+
255
+ All five sit at Lβ‰ˆ45 in OKLCH, chroma ≀0.04. Verified non-overlap with the four tier hues (which sit at chroma 0–0.10 in different L regions).
256
+
257
+ ### Constraints (non-negotiable)
258
+
259
+ 1. **One color per Stone, five total.**
260
+ 2. **Hint-level, not feature-level.** These are not signal-carrying like the tier palette. They are decoration that helps a reader navigate the five-region structure on first encounter.
261
+ 3. **Must not compete with the four-tier palette.** Tier badges (filled/open square / dotted ring / hatched square in `#0B5394` / `#2A6FA8` / `#6B6B6B` / `#2A6FA8+stripe`) are the load-bearing epistemic signal. Stone colors must be muted enough or applied lightly enough that they read as decoration. If a reader could mistake a Stone color for a tier badge, it's wrong.
262
+ 4. **Not technicolor.** Carto Positron base + IBM Plex + restrained ink palette is quiet and serious. Five primary-bright colors would shatter that.
263
+ 5. **Not mascot.** No Stone gets an icon paired with its color. No "Cornerstone is brown because earthy" thematic mapping. The colors differentiate five regions; they don't express character.
264
+ 6. **Print/PDF**: all five degrade to neutral gray (`#999`) in `@media print`. Each token has a print-media override.
265
+ 7. **WCAG**: any text in or on a Stone color passes contrast. As tints (low-opacity backgrounds), body text stays in standard `--ink`. As foreground accents, contrast against `--paper` is verified.
266
+
267
+ ### Recommended placement (one to three locations β€” pick conservatively)
268
+
269
+ **Primary** (recommend): a **3px left-rule** on each `<StoneRegion>` header strip, in that Stone's color. Subtle, navigational, hard to confuse with tier badges (which are inside cards, not on region headers).
270
+
271
+ **Secondary** (recommend): a **6px colored dot** beside each Stone name in the cold-start "How Riprap is built" five-line list. Helps recognition when the reader later sees the Stones in a query result.
272
+
273
+ **Tertiary** (optional, designer's call): a Stone-tinted **2px left-border on cards within that Stone's strip**. Risk: this brings color to the card chrome, which is where tier badges live. Apply only if the designer judges the chroma low enough that the tint reads as a region cue, not a tier cue. If in doubt, skip.
274
+
275
+ **Methodology page** (recommend): the 5Γ—4 tier-Stone matrix uses Stone tints on its row labels (background tint at ~10% opacity, body text in standard ink). The matrix is where Stones-as-architecture is most visually present, and tints help readers parse the rows.
276
+
277
+ ### Designer's veto
278
+
279
+ If the designer judges that adding Stone colors anywhere makes the UI busier without making it more legible, they may recommend keeping the existing zero-color Stone treatment with documented rationale. This is permission, not requirement.
280
+
281
+ ### Print-media override
282
+
283
+ ```css
284
+ @media print {
285
+ :root {
286
+ --stone-cornerstone: #999;
287
+ --stone-keystone: #999;
288
+ --stone-touchstone: #999;
289
+ --stone-lodestone: #999;
290
+ --stone-capstone: #999;
291
+ }
292
+ }
293
+ ```
294
+
295
+ The PDF template's hierarchy is preserved by structure (Stone headings, type scale, rules), not by color. Stone tints are ignored in print.
296
+
297
+ ---
298
+
299
+ ## "v0.4.5 ready" looks like
300
+
301
+ A query at 80 Pioneer Street, Red Hook produces a Findings region where:
302
+
303
+ - Top tally reads `5 Stones Β· 15 fired Β· 5 silent Β· 1 errored Β· 24.0s` (no more 3-error mismatch).
304
+ - Capstone meta-card shows `1 reroll Β· 4/4 grounding Β· 4 citations Β· 24.0s` (no more zeroes).
305
+ - Every Stone's expanded provenance shows its full specialist roster, with each row carrying one of the five status states.
306
+ - Touchstone shows five cards (FloodNet, NYC 311, NOAA CO-OPS, TerraMind LULC, Prithvi-NYC-Pluvial).
307
+ - Lodestone shows two TTM time-series cards (zero-shot 9.6h, fine-tuned 96h with HF model-card link).
308
+ - LAYERS panel is grouped by Stone, with the four new raster layers wired and default-off.
309
+ - Hovering any spatial card lights up the corresponding map element; hovering a map layer outlines the corresponding card.
310
+ - "anomaly" tag is gone.
311
+ - Each Stone region carries a 3px left-rule in its accent color; the cold-start list shows colored dots beside Stone names. No card-level chrome shouts color.
312
+
313
+ The hackathon submission goes in (when public re-hosting completes) with a system that's not just impressive in capability but **transparent in operational state and visually composed as a deliberate architecture**.
docs/design_handoff/design_files/Riprap Stone-Grouped UI v0.4.5.html ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Riprap Β· Stone-grouped UI mockup v0.4.5</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;1,400;1,500&family=IBM+Plex+Serif:wght@400;500;600&display=swap" rel="stylesheet" />
10
+ <link rel="stylesheet" href="tokens.css" />
11
+ <link rel="stylesheet" href="styles.css" />
12
+ <style>
13
+ .stone-mock-page { background: var(--paper); padding: 24px 32px 64px; }
14
+ .stone-mock-page > .stone-mock-section + .stone-mock-section { margin-top: 32px; }
15
+ .stone-mock-head { display: flex; justify-content: space-between; align-items: baseline; padding-bottom: 12px; border-bottom: 2px solid var(--ink); margin-bottom: 18px; }
16
+ .stone-mock-head h1 { font-family: var(--font-serif); font-size: 26px; font-weight: 600; margin: 0; }
17
+ .stone-mock-head .meta { font-family: var(--font-mono); font-size: 11px; color: var(--ink-tertiary); letter-spacing: 0.06em; }
18
+
19
+ /* Stone-grouped evidence layout */
20
+ .stone-ev-layout-head { display: flex; justify-content: space-between; align-items: baseline; padding-bottom: 10px; margin-bottom: 14px; border-bottom: 1px solid var(--rule-soft); }
21
+ .stone-ev-layout-meta { font-family: var(--font-serif); font-style: italic; font-size: 13px; color: var(--ink-tertiary); }
22
+
23
+ .stone-ev-group { background: white; border: 1px solid var(--rule-soft); border-left: 3px solid var(--ink); margin-bottom: 14px; }
24
+ .stone-ev-head { display: grid; grid-template-columns: 1fr auto; gap: 12px; padding: 12px 18px; background: var(--stone-band-bg); border-bottom: 1px solid var(--rule-soft); align-items: baseline; }
25
+ .stone-ev-name-row { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
26
+ .stone-ev-name { font-family: var(--font-sans); font-size: 16px; font-weight: 600; margin: 0; color: var(--ink); }
27
+ .stone-ev-role { font-size: 13px; color: var(--ink-secondary); }
28
+ .stone-ev-tag { font-family: var(--font-serif); font-style: italic; font-size: 13px; color: var(--ink-tertiary); }
29
+ .stone-ev-meta { display: flex; gap: 10px; align-items: baseline; font-family: var(--font-mono); font-size: 11px; color: var(--ink-secondary); }
30
+ .stone-ev-count { font-weight: 600; color: var(--ink); }
31
+ .stone-ev-tally { display: inline-flex; align-items: center; gap: 4px; }
32
+ .stone-ev-rail { padding: 12px 14px; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; align-items: start; }
33
+ .stone-ev-rail > * { min-width: 0; }
34
+ .stone-ev-empty { padding: 16px 18px; color: var(--ink-tertiary); font-style: italic; font-size: 13px; max-width: 60ch; }
35
+ .stone-ev-empty p { margin: 6px 0 0; line-height: 1.55; }
36
+
37
+ /* Mockup-specific: kill sticky map within the long single-page v0.4.4 layout */
38
+ .stone-mock-page .app-region-map { position: static; max-height: none; }
39
+
40
+ /* Trace row primitives (used inside unified bands) */
41
+ .trace-row { display: grid; grid-template-columns: 16px 1fr 70px 90px 70px; gap: 12px; align-items: baseline; padding: 5px 18px; font-family: var(--font-mono); font-size: 12px; color: var(--ink-secondary); }
42
+ .trace-row-group > summary { display: grid; grid-template-columns: 16px 1fr 70px 90px 70px; gap: 12px; align-items: baseline; cursor: pointer; list-style: none; }
43
+ .trace-row-group > summary::-webkit-details-marker { display: none; }
44
+ .trace-name { color: var(--ink); }
45
+ .trace-status { color: var(--ink-tertiary); font-size: 11px; }
46
+ .trace-status-err { color: var(--status-error); }
47
+ .trace-tier { font-size: 11px; }
48
+ .trace-ms { text-align: right; color: var(--ink); }
49
+ .trace-row-error { background: var(--status-error-soft); }
50
+ .trace-row-warn .trace-bullet { color: #B26500; font-weight: 700; }
51
+ .trace-row-silent { color: var(--ink-tertiary); }
52
+ .trace-warn-note, .trace-note, .trace-error-summary { grid-column: 2 / -1; font-size: 11px; color: var(--ink-tertiary); font-style: italic; padding-top: 2px; }
53
+ .trace-error-summary { color: var(--status-error); font-style: normal; }
54
+
55
+ /* Run-health strip */
56
+ .run-health { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; padding: 10px 18px; margin-bottom: 14px; background: var(--paper-deep); border: 1px solid var(--rule-soft); border-left: 3px solid var(--ink); font-family: var(--font-mono); font-size: 12px; color: var(--ink-secondary); }
57
+ .run-health-item strong { color: var(--ink); font-weight: 600; }
58
+ .run-health-sep { color: var(--ink-tertiary); }
59
+ .run-health-silent { color: var(--ink-tertiary); }
60
+ .run-health-warn { color: #B26500; }
61
+ .run-health-error { color: var(--status-error); font-weight: 600; }
62
+
63
+ /* Unified Stone band */
64
+ .stone-uni { background: white; border: 1px solid var(--rule-soft); border-left: 3px solid var(--ink); margin-bottom: 14px; }
65
+ .stone-uni-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 12px 18px; background: var(--stone-band-bg); border-bottom: 1px solid var(--rule-soft); }
66
+ .stone-uni-head-left { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
67
+ .stone-uni-name { font-family: var(--font-sans); font-size: 16px; font-weight: 600; margin: 0; color: var(--ink); }
68
+ .stone-uni-role { font-size: 13px; color: var(--ink-secondary); }
69
+ .stone-uni-tag { font-family: var(--font-serif); font-style: italic; font-size: 13px; color: var(--ink-tertiary); }
70
+ .stone-uni-agg { font-family: var(--font-mono); font-size: 12px; color: var(--ink-secondary); display: inline-flex; gap: 4px; flex-wrap: wrap; align-items: baseline; }
71
+ .stone-uni-agg-cards { color: var(--ink); font-weight: 600; }
72
+ .stone-uni-agg-num { color: var(--ink); font-weight: 600; }
73
+ .stone-uni-agg-warn { color: #B26500; font-weight: 600; }
74
+ .stone-uni-agg-err { color: var(--status-error); font-weight: 600; }
75
+ .stone-uni-agg-ms { color: var(--ink); font-weight: 600; }
76
+ .stone-uni-agg-sep { color: var(--ink-tertiary); margin: 0 2px; }
77
+ .stone-uni-rail { padding: 12px 14px; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; align-items: start; }
78
+ .stone-uni-rail > * { min-width: 0; }
79
+ .stone-uni-empty { padding: 14px 18px; color: var(--ink-tertiary); font-size: 13px; max-width: 70ch; }
80
+ .stone-uni-empty p { margin: 6px 0 0; line-height: 1.55; font-style: italic; }
81
+ .stone-uni-trace { border-top: 1px dashed var(--rule-soft); }
82
+ .stone-uni-trace-toggle { width: 100%; display: flex; align-items: baseline; gap: 8px; padding: 8px 18px; background: transparent; border: none; cursor: pointer; text-align: left; font-family: var(--font-mono); font-size: 11px; color: var(--ink-tertiary); letter-spacing: 0.04em; }
83
+ .stone-uni-trace-toggle:hover { background: var(--paper-deep); color: var(--ink-secondary); }
84
+ .stone-uni-trace-caret { display: inline-block; width: 10px; }
85
+ .stone-uni-trace-body { padding: 4px 0 8px; background: var(--paper); border-top: 1px solid var(--rule-soft); }
86
+
87
+ /* ════════════════════════════════════════════════════════════════════
88
+ v0.4.4 Β· Findings region
89
+ ════════════════════════════════════════════════════════════════════ */
90
+ .findings { background: var(--paper); }
91
+ .findings-head { display: flex; justify-content: space-between; align-items: baseline; padding-bottom: 12px; border-bottom: 2px solid var(--ink); margin-bottom: 18px; }
92
+ .findings-h2 { font-family: var(--font-serif); font-size: 26px; font-weight: 600; margin: 0; }
93
+ .findings-tagline { font-family: var(--font-mono); font-size: 11px; color: var(--ink-tertiary); letter-spacing: 0.04em; }
94
+
95
+ .f-runhealth { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; padding: 10px 18px; margin-bottom: 14px; background: var(--paper-deep); border: 1px solid var(--rule-soft); border-left: 3px solid var(--ink); font-family: var(--font-mono); font-size: 12px; color: var(--ink-secondary); }
96
+ .f-rh-item strong { color: var(--ink); font-weight: 600; }
97
+ .f-rh-sep { color: var(--ink-tertiary); }
98
+ .f-rh-silent { color: var(--ink-tertiary); }
99
+ .f-rh-warn { color: #B26500; }
100
+ .f-rh-err { color: var(--status-error); font-weight: 600; }
101
+
102
+ .f-region { background: white; border: 1px solid var(--rule-soft); border-left: 3px solid var(--ink); margin-bottom: 14px; }
103
+ .f-region-head { display: flex; justify-content: space-between; gap: 12px; padding: 12px 18px; background: var(--stone-band-bg); border-bottom: 1px solid var(--rule-soft); align-items: baseline; flex-wrap: wrap; }
104
+ .f-region-head-left { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; min-width: 0; }
105
+ .f-region-num { font-family: var(--font-mono); font-size: 11px; color: var(--ink-tertiary); letter-spacing: 0.06em; }
106
+ .f-region-name { font-family: var(--font-sans); font-size: 16px; font-weight: 600; margin: 0; color: var(--ink); }
107
+ .f-region-role { font-size: 13px; color: var(--ink-secondary); }
108
+ .f-region-tag { font-family: var(--font-serif); font-style: italic; font-size: 13px; color: var(--ink-tertiary); }
109
+
110
+ .f-tally { font-family: var(--font-mono); font-size: 12px; color: var(--ink-secondary); display: inline-flex; gap: 6px; flex-wrap: wrap; align-items: baseline; }
111
+ .f-tally-strong { color: var(--ink); font-weight: 600; }
112
+ .f-tally-cards { color: var(--ink); font-weight: 600; }
113
+ .f-tally-sep { color: var(--ink-tertiary); }
114
+ .f-tally-warn { color: #B26500; }
115
+ .f-tally-err { color: var(--status-error); }
116
+ .f-tally-silent { color: var(--ink-tertiary); }
117
+
118
+ .f-rail { padding: 14px; display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; align-items: start; }
119
+ .f-rail > * { min-width: 0; }
120
+ .f-rail-capstone { grid-template-columns: minmax(360px, 480px); }
121
+
122
+ .f-silent { padding: 14px 18px; display: flex; flex-direction: column; gap: 6px; max-width: 70ch; }
123
+ .f-silent-tag { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-tertiary); align-self: flex-start; padding: 2px 6px; border: 1px solid var(--rule-soft); }
124
+ .f-silent-prose { font-family: var(--font-serif); font-style: italic; font-size: 13px; color: var(--ink-tertiary); margin: 0; line-height: 1.55; }
125
+
126
+ .f-prov { border-top: 1px dashed var(--rule-soft); }
127
+ .f-prov-toggle { width: 100%; display: flex; align-items: baseline; gap: 8px; padding: 8px 18px; background: transparent; border: none; cursor: pointer; text-align: left; font-family: var(--font-mono); font-size: 11px; color: var(--ink-tertiary); letter-spacing: 0.04em; }
128
+ .f-prov-toggle:hover { background: var(--paper-deep); color: var(--ink-secondary); }
129
+ .f-prov-caret { display: inline-block; width: 10px; }
130
+ .f-prov-meta { color: var(--ink-tertiary); }
131
+ .f-prov-body { padding: 4px 0 8px; background: var(--paper); border-top: 1px solid var(--rule-soft); }
132
+
133
+ /* ─── Card frame ─── */
134
+ .fc { border: 1px solid var(--rule-soft); background: var(--paper); padding: 12px 14px 10px; display: flex; flex-direction: column; gap: 8px; position: relative; border-top: 2px solid var(--ink); transition: border-color 120ms, box-shadow 120ms; cursor: pointer; }
135
+ .fc-tier-empirical { border-top-color: var(--tier-empirical); }
136
+ .fc-tier-modeled { border-top-color: var(--tier-modeled); }
137
+ .fc-tier-proxy { border-top-color: var(--tier-proxy); }
138
+ .fc-tier-synthetic { border-top-color: var(--tier-synthetic); border-top-style: dashed; }
139
+ .fc:hover, .fc.is-linked { box-shadow: 0 1px 0 var(--ink), 0 0 0 1px var(--ink); }
140
+ .fc.is-compact { padding: 8px 12px 8px; gap: 6px; }
141
+
142
+ .fc-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
143
+ .fc-head-source { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-sans); font-size: 12px; font-weight: 600; color: var(--ink); }
144
+ .fc-head-source-label { letter-spacing: 0.01em; }
145
+ .fc-head-vintage { font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-tertiary); letter-spacing: 0.02em; }
146
+
147
+ .fc-title { font-family: var(--font-sans); font-size: 13.5px; font-weight: 600; line-height: 1.3; margin: 0; color: var(--ink); text-wrap: pretty; }
148
+ .fc.is-compact .fc-title { font-size: 13px; }
149
+
150
+ .fc-body { display: flex; flex-direction: column; gap: 6px; padding: 2px 0; }
151
+ .fc-body-prose { font-size: 12px; line-height: 1.5; color: var(--ink-secondary); margin: 4px 0 0; }
152
+ .fc-body-sub { font-size: 11px; line-height: 1.5; color: var(--ink-tertiary); font-style: italic; }
153
+ .fc.is-compact .fc-body-prose, .fc.is-compact .fc-body-sub { font-size: 11px; }
154
+
155
+ .fc-headline { font-family: var(--font-serif); font-size: 22px; font-weight: 600; line-height: 1.1; }
156
+ .fc.is-compact .fc-headline { font-size: 19px; }
157
+ .fc-subhead { font-family: var(--font-mono); font-size: 11px; color: var(--ink-secondary); letter-spacing: 0.02em; }
158
+
159
+ .fc-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; }
160
+ .fc-table th, .fc-table td { text-align: left; padding: 3px 6px; border-bottom: 1px solid var(--rule-soft); }
161
+ .fc-table th { color: var(--ink-tertiary); font-weight: 500; text-transform: uppercase; letter-spacing: 0.08em; font-size: 9px; }
162
+
163
+ .fc-body-scalars .fc-scalars-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 4px 0; }
164
+ .fc-scalar-cell { display: flex; flex-direction: column; gap: 2px; padding: 6px 4px; background: var(--paper-deep); border: 1px solid var(--rule-soft); }
165
+ .fc-scalar-value { font-family: var(--font-serif); font-size: 18px; font-weight: 600; line-height: 1.05; }
166
+ .fc-scalar-label { font-family: var(--font-mono); font-size: 9.5px; color: var(--ink-tertiary); letter-spacing: 0.04em; }
167
+
168
+ .fc-raster-frame { position: relative; border: 1px solid var(--rule-soft); }
169
+ .fc-illustrative { position: absolute; top: 4px; right: 4px; font-family: var(--font-mono); font-size: 8.5px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--paper); background: rgba(26,26,26,0.7); padding: 1px 5px; }
170
+ .fc-raster-headline { font-family: var(--font-mono); font-size: 11px; color: var(--ink-secondary); }
171
+ .fc-raster-headline span { font-family: var(--font-serif); font-size: 14px; font-weight: 600; }
172
+
173
+ .fc-body-timeseries { gap: 4px; }
174
+ .fc-ts-header { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
175
+ .fc-spatial-note { display: inline-block; font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--accent); border: 1px solid var(--accent); padding: 1px 5px; margin-right: 6px; vertical-align: middle; }
176
+
177
+ /* Register composite, dense row layout (v0.4.4 follow-up: cut card height) */
178
+ .fc-body-register .fc-reg-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0; border: 1px solid var(--rule-soft); }
179
+ .fc-reg-row { display: grid; grid-template-columns: 44px minmax(0, 1fr) auto; gap: 6px 10px; align-items: baseline; padding: 3px 8px; border-bottom: 1px solid var(--rule-soft); font-size: 12px; line-height: 1.35; }
180
+ .fc-reg-row:last-child { border-bottom: none; }
181
+ .fc-reg-row.is-silent { grid-template-columns: 44px minmax(0, 1fr); color: var(--ink-tertiary); font-style: italic; background: var(--paper-deep); padding: 2px 8px; }
182
+ .fc-reg-tag { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.08em; color: var(--ink-tertiary); align-self: center; }
183
+ .fc-reg-label { font-family: var(--font-sans); font-size: 12px; font-weight: 500; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
184
+ .fc-reg-detail { display: none; }
185
+ .fc-reg-tier { display: inline-flex; align-self: center; }
186
+ .fc-reg-source { font-family: var(--font-mono); font-size: 9.5px; color: var(--ink-tertiary); align-self: center; white-space: nowrap; }
187
+ .fc-reg-silent { font-family: var(--font-serif); font-size: 11.5px; color: var(--ink-tertiary); }
188
+ .fc.is-compact .fc-reg-row { padding: 2px 8px; font-size: 11.5px; }
189
+ .fc.is-compact .fc-reg-label { font-size: 11.5px; }
190
+
191
+ /* Comparison */
192
+ .fc-body-comparison .fc-cmp-grid { display: grid; grid-template-columns: 1fr auto 1fr; gap: 8px; align-items: stretch; padding: 4px 0; }
193
+ .fc-cmp-cell { display: flex; flex-direction: column; gap: 4px; padding: 8px 10px; background: var(--paper-deep); border: 1px solid var(--rule-soft); }
194
+ .fc-cmp-cell-tier { display: inline-flex; align-items: center; gap: 5px; font-family: var(--font-mono); font-size: 10px; color: var(--ink-tertiary); letter-spacing: 0.04em; }
195
+ .fc-cmp-cell-label { color: var(--ink-secondary); }
196
+ .fc-cmp-cell-value { font-family: var(--font-serif); font-size: 22px; font-weight: 600; line-height: 1.05; }
197
+ .fc-cmp-cell-aux { font-family: var(--font-mono); font-size: 10px; color: var(--ink-tertiary); }
198
+ .fc-cmp-divider { font-family: var(--font-serif); font-style: italic; font-size: 13px; color: var(--ink-tertiary); align-self: center; padding: 0 4px; }
199
+ .fc-cmp-delta { font-family: var(--font-mono); font-size: 11px; color: var(--accent); padding: 4px 0; border-top: 1px dashed var(--rule-soft); border-bottom: 1px dashed var(--rule-soft); margin: 4px 0; }
200
+
201
+ /* Meta */
202
+ .fc-body-meta .fc-meta-list { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 12px; margin: 0; }
203
+ .fc-meta-row { display: flex; flex-direction: column; gap: 1px; padding: 4px 6px; background: var(--paper-deep); border-left: 2px solid var(--rule-soft); }
204
+ .fc-meta-row dt { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-tertiary); margin: 0; }
205
+ .fc-meta-row dd { font-family: var(--font-sans); font-size: 13px; font-weight: 500; color: var(--ink); margin: 0; }
206
+
207
+ /* Card foot */
208
+ .fc-foot { display: flex; justify-content: space-between; align-items: center; padding-top: 6px; margin-top: auto; border-top: 1px solid var(--rule-soft); }
209
+ .fc-foot-cite { background: transparent; border: 0; padding: 2px 0; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; color: var(--ink-secondary); }
210
+ .fc-foot-cite:hover { color: var(--accent); }
211
+ .fc-foot-docid { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.02em; }
212
+ .fc-foot-docid-mute { color: var(--ink-tertiary); }
213
+ .fc-foot-arrow { font-family: var(--font-mono); font-size: 11px; color: var(--ink-tertiary); }
214
+
215
+ .fc-tier-badge { display: inline-flex; align-items: center; gap: 4px; font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.08em; padding: 2px 5px; border: 1px solid var(--rule-soft); color: var(--ink-secondary); background: var(--paper); }
216
+ .fc-tier-badge-empirical { border-color: var(--tier-empirical); color: var(--tier-empirical); }
217
+ .fc-tier-badge-modeled { border-color: var(--tier-modeled); color: var(--tier-modeled); }
218
+ .fc-tier-badge-proxy { border-color: var(--tier-proxy); color: var(--tier-proxy); }
219
+ .fc-tier-badge-synthetic { border-color: var(--tier-synthetic); color: var(--tier-synthetic); border-style: dashed; }
220
+
221
+ /* Map highlight on hover-link */
222
+ .map-frame.is-link-floodnet::after,
223
+ .map-frame.is-link-fema-ae::after,
224
+ .map-frame.is-link-hwm::after,
225
+ .map-frame.is-link-stormwater::after,
226
+ .map-frame.is-link-prithvi::after,
227
+ .map-frame.is-link-buildings::after,
228
+ .map-frame.is-link-complaints::after,
229
+ .map-frame.is-link-registers::after {
230
+ content: attr(data-link-label); position: absolute; bottom: 8px; right: 8px;
231
+ background: var(--ink); color: var(--paper); font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
232
+ padding: 4px 8px; pointer-events: none; z-index: 5;
233
+ }
234
+ .map-frame.is-linked { outline: 2px solid var(--accent-graphical); outline-offset: -2px; }
235
+
236
+ </style>
237
+ </head>
238
+ <body>
239
+ <template id="__bundler_thumbnail">
240
+ <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
241
+ <rect width="100" height="100" fill="#FAFAF7"/>
242
+ <g transform="translate(20 28)">
243
+ <rect x="0" y="0" width="60" height="9" fill="#0B5394"/>
244
+ <rect x="0" y="13" width="48" height="9" fill="#2A6FA8"/>
245
+ <rect x="0" y="26" width="38" height="9" fill="#6B6B6B"/>
246
+ <rect x="0" y="39" width="52" height="9" fill="#D17C00"/>
247
+ </g>
248
+ </svg>
249
+ </template>
250
+ <div id="root"></div>
251
+
252
+ <script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
253
+ <script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
254
+ <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
255
+
256
+ <script type="text/babel" src="tweaks-panel.jsx"></script>
257
+ <script type="text/babel" src="glyphs.jsx"></script>
258
+ <script type="text/babel" src="briefing.jsx"></script>
259
+ <script type="text/babel" src="map.jsx"></script>
260
+ <script type="text/babel" src="trace.jsx"></script>
261
+ <script type="text/babel" src="evidence.jsx"></script>
262
+ <script type="text/babel" src="shell.jsx"></script>
263
+ <script type="text/babel" src="stones-trace.jsx"></script>
264
+ <script type="text/babel" src="stone-evidence.jsx"></script>
265
+ <script type="text/babel" src="findings.jsx"></script>
266
+ <script type="text/babel">
267
+ const { useState: useM } = React;
268
+
269
+ const V44_DEFAULTS = /*EDITMODE-BEGIN*/{
270
+ "density": "comfortable",
271
+ "provenance": "smart",
272
+ "query": "redhook",
273
+ "showComparison": false,
274
+ "showGrammar": true
275
+ }/*EDITMODE-END*/;
276
+
277
+ function StoneMock() {
278
+ const [activeCite, setActiveCite] = useM(null);
279
+ const [linkedKey, setLinkedKey] = useM(null);
280
+ const [tweaks, setTweak] = window.useTweaks(V44_DEFAULTS);
281
+ const handleCite = (id) => setActiveCite(id);
282
+ const handleHover = (key) => setLinkedKey(key);
283
+ /* Reflect linkedKey into map-frame data attribute */
284
+ React.useEffect(() => {
285
+ const frame = document.querySelector(".stone-mock-page .map-frame");
286
+ if (!frame) return;
287
+ /* clear */
288
+ ["floodnet","fema-ae","hwm","stormwater","prithvi","buildings","complaints","registers"].forEach(k => frame.classList.remove("is-link-" + k));
289
+ frame.classList.toggle("is-linked", !!linkedKey);
290
+ if (linkedKey) {
291
+ frame.classList.add("is-link-" + linkedKey);
292
+ const labels = { floodnet: "FloodNet sensor", "fema-ae": "FEMA Zone AE", hwm: "USGS HWM points", stormwater: "DEP stormwater", prithvi: "Prithvi flood pred.", buildings: "TerraMind buildings", complaints: "311 complaints", registers: "Asset registers" };
293
+ frame.dataset.linkLabel = labels[linkedKey] || linkedKey;
294
+ }
295
+ }, [linkedKey]);
296
+
297
+ return (
298
+ <div className="riprap-root stone-mock-page">
299
+ <window.AppHeader query={"80 Pioneer Street, Red Hook, Brooklyn"} onResetCold={() => {}} onOpenMethodology={() => {}}/>
300
+
301
+ {/* Β§1 Β· Briefing + map */}
302
+ <section className="stone-mock-section" data-screen-label="01 Β· Briefing + map">
303
+ <div className="stone-mock-head">
304
+ <h1>Riprap Flood Exposure Briefing , 80 Pioneer Street</h1>
305
+ <span className="meta">v0.4.5 Β· Stone-grouped UI mockup</span>
306
+ </div>
307
+ <div className="app-shell app-shell-desktop">
308
+ <main id="region-briefing" className="app-region app-region-brief">
309
+ <header className="region-head"><span className="section-label">Briefing</span></header>
310
+ <h1 className="brief-h1">
311
+ <span className="brief-h1-eyebrow">Riprap Flood Exposure Briefing</span>
312
+ <span className="brief-h1-addr">80 Pioneer Street</span>
313
+ <span className="brief-h1-meta">
314
+ <span className="brief-h1-meta-row"><span className="brief-h1-meta-key">borough</span><span className="brief-h1-meta-val">Brooklyn Β· CB6</span></span>
315
+ <span className="brief-h1-meta-row"><span className="brief-h1-meta-key">tract</span><span className="brief-h1-meta-val">36047008500</span></span>
316
+ <span className="brief-h1-meta-row"><span className="brief-h1-meta-key">generated</span><span className="brief-h1-meta-val">2026-05-05 14:22 ET</span></span>
317
+ </span>
318
+ </h1>
319
+ <window.StreamingBriefing onCite={handleCite} replayKey={0}/>
320
+ </main>
321
+ <aside className="app-region app-region-map" aria-label="Map">
322
+ <header className="region-head">
323
+ <span className="section-label">Map</span>
324
+ <span className="region-head-meta">Carto Positron Β· z16</span>
325
+ </header>
326
+ <div className="map-frame">
327
+ <window.RedHookMapMock activeLayers={{ empirical: true, modeled: true, synthetic: true, proxy: true, "prithvi-pluvial": false }} queriedAddress="80 Pioneer Street"/>
328
+ <window.MapLegend activeLayers={{ empirical: true, modeled: true, synthetic: true, proxy: true, "prithvi-pluvial": false }} onToggle={() => {}}/>
329
+ </div>
330
+ </aside>
331
+ <aside className="app-region app-region-cites" aria-label="Citations">
332
+ <window.CitationDrawer activeId={activeCite}/>
333
+ </aside>
334
+ </div>
335
+ </section>
336
+
337
+ {/* Β§2 Β· Findings region Β· v0.4.4 */}
338
+ <section className="stone-mock-section" data-screen-label="02 Findings">
339
+ <window.FindingsRegion
340
+ density={tweaks.density}
341
+ provenanceMode={tweaks.provenance}
342
+ queryKey={tweaks.query}
343
+ showComparison={tweaks.showComparison}
344
+ showGrammar={tweaks.showGrammar}
345
+ onCite={handleCite}
346
+ onHover={handleHover}
347
+ linkedKey={linkedKey}
348
+ />
349
+ </section>
350
+
351
+ {/* Tweaks panel */}
352
+ <window.TweaksPanel title="Tweaks">
353
+ <window.TweakSection label="Display"/>
354
+ <window.TweakRadio label="Density" value={tweaks.density} onChange={(v) => setTweak("density", v)} options={["comfortable", "compact"]}/>
355
+ <window.TweakSelect label="Query" value={tweaks.query} onChange={(v) => setTweak("query", v)} options={["redhook", "bronx"]}/>
356
+ <window.TweakSection label="Provenance"/>
357
+ <window.TweakSelect label="Default" value={tweaks.provenance} onChange={(v) => setTweak("provenance", v)} options={["smart", "expanded", "collapsed"]}/>
358
+ <window.TweakSection label="Card grammar"/>
359
+ <window.TweakToggle label="Show grammar reference" value={tweaks.showGrammar} onChange={(v) => setTweak("showGrammar", v)}/>
360
+ <window.TweakToggle label="Show comparison card" value={tweaks.showComparison} onChange={(v) => setTweak("showComparison", v)}/>
361
+ </window.TweaksPanel>
362
+ </div>
363
+ );
364
+ }
365
+
366
+ ReactDOM.createRoot(document.getElementById("root")).render(<StoneMock/>);
367
+ </script>
368
+ </body>
369
+ </html>
docs/design_handoff/design_files/findings.jsx CHANGED
@@ -13,8 +13,8 @@ const CARDS_BY_QUERY = {
13
  redhook: {
14
  cornerstone: ["fc-fema", "fc-hwm", "fc-stormwater"],
15
  keystone: ["fc-register-rh"],
16
- touchstone: ["fc-floodnet", "fc-311", "fc-prithvi", "fc-nws"],
17
- lodestone: ["fc-ttm-surge", "fc-npcc4"],
18
  capstone: ["fc-mellea-meta"],
19
  },
20
  bronx: {
@@ -85,14 +85,13 @@ const CARDS = {
85
  source: "NYC OpenData", agency: "NYC OpenData Β· multi-agency join",
86
  title: "Nearby exposed assets",
87
  registers: [
88
- { reg: "MTA", tier: "empirical", label: "Smith–9 St subway entrance", detail: "0.34 mi Β· F Β· G", sourceId: "MTA-ENT-N048", vintage: "2025-11", note: null },
89
- { reg: "NYCHA", tier: "empirical", label: "Red Hook East Houses", detail: "0.41 mi Β· 2,878 res.", sourceId: "NYCHA-RHE", vintage: "2025-Q3", note: null },
90
- { reg: "NYCHA", tier: "empirical", label: "Red Hook West Houses", detail: "0.52 mi Β· 3,142 res.", sourceId: "NYCHA-RHW", vintage: "2025-Q3", note: null },
91
- { reg: "DOE", tier: "empirical", label: "PS 27 Agnes Y. Humphrey", detail: "0.29 mi Β· 271 K-5", sourceId: "DOE-K027", vintage: "2024-25", note: null },
92
- { reg: "DOH", tier: "empirical", label: null, detail: null, sourceId: null, vintage: null, note: "no acute-care hospital within 1.0 mi (silent)" },
93
- { reg: "PLUTO", tier: "empirical", label: "Lot 36047 / 521 / 7", detail: "BIN 3018472 Β· MX-1", sourceId: "PLUTO-2024v2", vintage: "2024-12", note: null },
94
  ],
95
- sub: "5 of 6 registers fired Β· 1 silent Β· joined within 1.0 mi",
96
  docId: "RIPRAP-EXP-RH80", vintage: "2026-05", citeId: "c-reg-rh",
97
  mapKey: "registers",
98
  },
@@ -143,16 +142,33 @@ const CARDS = {
143
  docId: "NYC311-FLD-CB11", vintage: "2025-12", citeId: "cx7",
144
  mapKey: "complaints",
145
  },
146
- "fc-prithvi": {
147
  stone: "touchstone", tier: "modeled", variant: "raster-pred",
148
- source: "Prithvi-NYC", agency: "Prithvi-NYC-Pluvial v2 Β· IBM/NASA Γ— Riprap",
149
- title: "Pluvial flood prediction, current Sentinel-2 chip",
150
  rasterKind: "prithvi",
151
  headline: "0.3% flooded", subhead: "no flooding apparent Β· scene 2026-05-02",
152
  sub: "Model interpretation of imagery, not real-time observation. Confidence-mean 0.84 across non-flooded pixels.",
153
- docId: "PRITHVI-NYC-PLUV-V2-20260502", vintage: "2026-05-02",
154
  illustrative: true, citeId: "c-prithvi",
155
- mapKey: "prithvi",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  },
157
  "fc-nws": {
158
  stone: "touchstone", tier: "empirical", variant: "scalars",
@@ -171,15 +187,31 @@ const CARDS = {
171
  /* ── Lodestone ── */
172
  "fc-ttm-surge": {
173
  stone: "lodestone", tier: "modeled", variant: "timeseries",
174
- source: "Granite TTM r2", agency: "IBM Granite-TimeSeries Β· Riprap fine-tune",
175
- title: "Storm surge nowcast at The Battery, 96-hour horizon",
176
  timeseries: { hours: 96, peak: { x: 38, y: 47 }, peakLabel: "+47 cm @ +38h" },
177
- headline: "+47 cm", subhead: "peak surge residual Β· Wed 04:00 ET",
178
- sub: "Nowcast applies city-wide via NOAA station 8518750. Not localized to query address. Residual above harmonic tide.",
179
- docId: "ttm_battery_surge_v2", vintage: "2026-05-05 12:00 ET",
180
  spatialNote: "regional Β· The Battery, not point-of-query",
181
  citeId: "c-ttm",
182
- mapKey: null, /* TTM does not render on map; lives only as Lodestone card */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  },
184
  "fc-npcc4": {
185
  stone: "lodestone", tier: "modeled", variant: "forecast",
@@ -202,10 +234,10 @@ const CARDS = {
202
  source: "Mellea", agency: "Capstone synthesis Β· grounding check",
203
  title: "Briefing reconciliation",
204
  metaRows: [
205
- { k: "Mellea reroll", v: "1 attempt" },
206
- { k: "Grounding checks", v: "4 / 4 passed" },
207
- { k: "Citations resolved",v: "11 / 11" },
208
- { k: "RAG β†’ GLiNER", v: "9 entities Β· 0 unresolved" },
209
  ],
210
  sub: "Capstone produces prose, not cards. This meta-card summarizes the reconciler chain that wrote the four-section briefing above.",
211
  docId: "RIPRAP-CAP-RH80", vintage: "2026-05-05 14:22 ET", citeId: null,
@@ -679,10 +711,11 @@ const flatten = (members) => members.flatMap((m) => (m.children ? [m, ...flatten
679
 
680
  const StoneTally44 = ({ cardCount, members }) => {
681
  const flat = flatten(members);
682
- const fired = flat.filter((m) => m.status === "ok").length;
683
- const silent = flat.filter((m) => m.status === "silent").length;
684
- const warn = flat.filter((m) => m.status === "warn").length;
685
- const error = flat.filter((m) => m.status === "error").length;
 
686
  const ms = members.reduce((acc, m) => Math.max(acc, m.ms || 0), 0);
687
  const fmtMs = (x) => (x === 0 ? "β€”" : x < 1000 ? x + "ms" : (x / 1000).toFixed(1) + "s");
688
  return (
@@ -692,7 +725,8 @@ const StoneTally44 = ({ cardCount, members }) => {
692
  <span className="f-tally-fired"><span className="f-tally-strong">{fired}</span> fired</span>
693
  {silent > 0 && <><span className="f-tally-sep">Β·</span><span className="f-tally-silent"><span className="f-tally-strong">{silent}</span> silent</span></>}
694
  {warn > 0 && <><span className="f-tally-sep">Β·</span><span className="f-tally-warn"><span className="f-tally-strong">{warn}</span> warn</span></>}
695
- {error > 0 && <><span className="f-tally-sep">Β·</span><span className="f-tally-err"><span className="f-tally-strong">{error}</span> error</span></>}
 
696
  <span className="f-tally-sep">Β·</span>
697
  <span className="f-tally-ms"><span className="f-tally-strong">{fmtMs(ms)}</span></span>
698
  </span>
@@ -704,11 +738,12 @@ const StoneRegion = ({ stone, cardIds, density, provenanceMode, onCite, onHover,
704
  const cards = cardIds.map((id) => CARDS[id]).filter(Boolean);
705
  const traceCount = flatten(stone.members).length;
706
  const flat = flatten(stone.members);
707
- const hasAnomaly = flat.some((m) => m.status === "warn" || m.status === "error" || m.status === "silent");
 
708
  const defaultOpen =
709
  provenanceMode === "all-expanded" ? true :
710
  provenanceMode === "all-collapsed" ? false :
711
- /* smart */ hasAnomaly;
712
  const [traceOpen, setTraceOpen] = useFi(defaultOpen);
713
  /* Re-sync if user toggles tweak */
714
  useFiMemo(() => setTraceOpen(defaultOpen), [provenanceMode]);
@@ -763,7 +798,7 @@ const StoneRegion = ({ stone, cardIds, density, provenanceMode, onCite, onHover,
763
  >
764
  <span className="f-prov-caret" aria-hidden="true">{traceOpen ? "β–Ύ" : "β–Έ"}</span>
765
  <span className="f-prov-label">{traceOpen ? "Hide" : "Show"} provenance</span>
766
- <span className="f-prov-meta">Β· {traceCount} function{traceCount === 1 ? "" : "s"}{hasAnomaly ? " Β· anomaly" : ""}</span>
767
  </button>
768
  {traceOpen && (
769
  <div className="f-prov-body">
@@ -779,11 +814,11 @@ const StoneRegion = ({ stone, cardIds, density, provenanceMode, onCite, onHover,
779
 
780
  const RunHealth44 = ({ totalCards }) => {
781
  const all = window.STONES.flatMap((s) => flatten(s.members));
782
- const fired = all.filter((m) => m.status === "ok").length;
783
  const total = all.length;
784
- const silent = all.filter((m) => m.status === "silent").length;
785
- const warn = all.filter((m) => m.status === "warn").length;
786
- const error = all.filter((m) => m.status === "error").length;
787
  return (
788
  <div className="f-runhealth">
789
  <span className="f-rh-item"><strong>5</strong> Stones</span>
@@ -792,10 +827,10 @@ const RunHealth44 = ({ totalCards }) => {
792
  <span className="f-rh-sep">Β·</span>
793
  <span className="f-rh-item"><strong>{totalCards}</strong> evidence cards</span>
794
  <span className="f-rh-sep">Β·</span>
795
- <span className="f-rh-item"><strong>14.0s</strong> wall-clock</span>
796
  {silent > 0 && <><span className="f-rh-sep">Β·</span><span className="f-rh-item f-rh-silent">{silent} silent</span></>}
797
- {warn > 0 && <><span className="f-rh-sep">Β·</span><span className="f-rh-item f-rh-warn">{warn} warn</span></>}
798
- {error > 0 && <><span className="f-rh-sep">Β·</span><span className="f-rh-item f-rh-err">{error} error</span></>}
799
  </div>
800
  );
801
  };
 
13
  redhook: {
14
  cornerstone: ["fc-fema", "fc-hwm", "fc-stormwater"],
15
  keystone: ["fc-register-rh"],
16
+ touchstone: ["fc-floodnet", "fc-311", "fc-nws", "fc-terramind-lulc", "fc-prithvi-pluvial"],
17
+ lodestone: ["fc-ttm-surge", "fc-ttm-surge-ft", "fc-npcc4"],
18
  capstone: ["fc-mellea-meta"],
19
  },
20
  bronx: {
 
85
  source: "NYC OpenData", agency: "NYC OpenData Β· multi-agency join",
86
  title: "Nearby exposed assets",
87
  registers: [
88
+ { reg: "MTA", tier: "empirical", label: null, detail: null, sourceId: null, vintage: null, note: "no entrances within radius" },
89
+ { reg: "NYCHA", tier: "empirical", label: null, detail: null, sourceId: null, vintage: null, note: "no NYCHA developments within 1.0 mi" },
90
+ { reg: "DOE", tier: "empirical", label: null, detail: null, sourceId: null, vintage: null, note: "no DOE schools within 1.0 mi" },
91
+ { reg: "DOH", tier: "empirical", label: null, detail: null, sourceId: null, vintage: null, note: "no acute-care hospitals within 1.0 mi" },
92
+ { reg: "PLUTO", tier: "empirical", label: null, detail: null, sourceId: null, vintage: null, note: "PLUTO join skipped: queried address not in NYC PLUTO dataset" },
 
93
  ],
94
+ sub: "5 specialists Β· 5 silent_by_design Β· 0 cards landed (full inventory shown)",
95
  docId: "RIPRAP-EXP-RH80", vintage: "2026-05", citeId: "c-reg-rh",
96
  mapKey: "registers",
97
  },
 
142
  docId: "NYC311-FLD-CB11", vintage: "2025-12", citeId: "cx7",
143
  mapKey: "complaints",
144
  },
145
+ "fc-prithvi-pluvial": {
146
  stone: "touchstone", tier: "modeled", variant: "raster-pred",
147
+ source: "Prithvi-NYC-Pluvial", agency: "NASA-IBM Prithvi v2 Β· NYC fine-tune",
148
+ title: "Pluvial flood prediction Β· Prithvi-NYC-Pluvial",
149
  rasterKind: "prithvi",
150
  headline: "0.3% flooded", subhead: "no flooding apparent Β· scene 2026-05-02",
151
  sub: "Model interpretation of imagery, not real-time observation. Confidence-mean 0.84 across non-flooded pixels.",
152
+ docId: "PRITHVI-NYC-PLUV-V2-20260502", vintage: "2026-05-02 Β· Sentinel-2",
153
  illustrative: true, citeId: "c-prithvi",
154
+ mapKey: "prithvi-pluvial",
155
+ },
156
+ "fc-terramind-lulc": {
157
+ stone: "touchstone", tier: "synthetic", variant: "lulc",
158
+ source: "TerraMind v1.2", agency: "IBM TerraMind v1.2 Β· Sentinel-2 inputs",
159
+ title: "Land use / land cover Β· TerraMind v1.2",
160
+ rasterKind: "lulc",
161
+ classMix: [
162
+ { k: "urban", pct: 62, color: "#C66" },
163
+ { k: "water", pct: 18, color: "#5B7FB4" },
164
+ { k: "vegetation", pct: 12, color: "#5B8A4A" },
165
+ { k: "barren", pct: 6, color: "#A89A78" },
166
+ { k: "wetland", pct: 2, color: "#D9C75A" },
167
+ ],
168
+ sub: "Synthetic prior. LULC palette is a layer convention, not a tier signal.",
169
+ docId: "TERRAMIND-LULC-20240918", vintage: "Sentinel-2 Β· 2024-09-18",
170
+ citeId: "c-tm-lulc",
171
+ mapKey: "terramind-lulc",
172
  },
173
  "fc-nws": {
174
  stone: "touchstone", tier: "empirical", variant: "scalars",
 
187
  /* ── Lodestone ── */
188
  "fc-ttm-surge": {
189
  stone: "lodestone", tier: "modeled", variant: "timeseries",
190
+ source: "Granite TTM r2 (zero-shot)", agency: "IBM Granite-TimeSeries Β· regional",
191
+ title: "Storm surge nowcast at The Battery β€” 9.6 h horizon (regional)",
192
  timeseries: { hours: 96, peak: { x: 38, y: 47 }, peakLabel: "+47 cm @ +38h" },
193
+ headline: "+47 cm", subhead: "peak surge residual Β· 9.6h horizon Β· 6-min cadence",
194
+ sub: "Regional disclosure. Nowcast applies city-wide via NOAA station 8518750. Distinct from the fine-tuned Battery surge nowcast.",
195
+ docId: "ttm_battery_surge_zeroshot", vintage: "2026-05-05 12:00 ET",
196
  spatialNote: "regional Β· The Battery, not point-of-query",
197
  citeId: "c-ttm",
198
+ mapKey: null,
199
+ },
200
+ "fc-ttm-surge-ft": {
201
+ stone: "lodestone", tier: "modeled", variant: "timeseries-ft",
202
+ source: "msradam/Granite-TTM-r2-Battery-Surge", agency: "Granite TTM r2 Β· NYC-specialized fine-tune",
203
+ title: "Storm surge nowcast at The Battery β€” 96 h horizon (NYC-specialized fine-tune)",
204
+ timeseries: { hours: 96, peak: { x: 38, y: 53 }, peakLabel: "+53 cm @ +38h" },
205
+ headline: "+53 cm", subhead: "peak surge Β· 96h horizon Β· hourly cadence",
206
+ sub: "Fine-tuned on NYC tide-gauge history. Trained on AMD MI300X.",
207
+ docId: "ttm_battery_surge_finetune", vintage: "2026-05-05 12:00 ET",
208
+ spatialNote: "regional Β· The Battery, not point-of-query",
209
+ hfModelCard: "huggingface.co/msradam/Granite-TTM-r2-Battery-Surge",
210
+ rmse: "0.157 m",
211
+ skillVsPersistence: "βˆ’35% vs persistence",
212
+ hardwareBadge: "MI300X",
213
+ citeId: "c-ttm-ft",
214
+ mapKey: null,
215
  },
216
  "fc-npcc4": {
217
  stone: "lodestone", tier: "modeled", variant: "forecast",
 
234
  source: "Mellea", agency: "Capstone synthesis Β· grounding check",
235
  title: "Briefing reconciliation",
236
  metaRows: [
237
+ { k: "Mellea reroll", v: "1 reroll" },
238
+ { k: "Grounding checks", v: "4 / 4 passed" },
239
+ { k: "Citations resolved", v: "4" },
240
+ { k: "Wall-clock", v: "24.0 s" },
241
  ],
242
  sub: "Capstone produces prose, not cards. This meta-card summarizes the reconciler chain that wrote the four-section briefing above.",
243
  docId: "RIPRAP-CAP-RH80", vintage: "2026-05-05 14:22 ET", citeId: null,
 
711
 
712
  const StoneTally44 = ({ cardCount, members }) => {
713
  const flat = flatten(members);
714
+ const fired = flat.filter((m) => m.status === "fired" || m.status === "warned").length;
715
+ const silent = flat.filter((m) => m.status === "silent_by_design").length;
716
+ const warn = flat.filter((m) => m.status === "warned").length;
717
+ const error = flat.filter((m) => m.status === "errored").length;
718
+ const notInvoked = flat.filter((m) => m.status === "not_invoked").length;
719
  const ms = members.reduce((acc, m) => Math.max(acc, m.ms || 0), 0);
720
  const fmtMs = (x) => (x === 0 ? "β€”" : x < 1000 ? x + "ms" : (x / 1000).toFixed(1) + "s");
721
  return (
 
725
  <span className="f-tally-fired"><span className="f-tally-strong">{fired}</span> fired</span>
726
  {silent > 0 && <><span className="f-tally-sep">Β·</span><span className="f-tally-silent"><span className="f-tally-strong">{silent}</span> silent</span></>}
727
  {warn > 0 && <><span className="f-tally-sep">Β·</span><span className="f-tally-warn"><span className="f-tally-strong">{warn}</span> warn</span></>}
728
+ {error > 0 && <><span className="f-tally-sep">Β·</span><span className="f-tally-err"><span className="f-tally-strong">{error}</span> errored</span></>}
729
+ {notInvoked > 0 && <><span className="f-tally-sep">Β·</span><span className="f-tally-notinvoked"><span className="f-tally-strong">{notInvoked}</span> not invoked</span></>}
730
  <span className="f-tally-sep">Β·</span>
731
  <span className="f-tally-ms"><span className="f-tally-strong">{fmtMs(ms)}</span></span>
732
  </span>
 
738
  const cards = cardIds.map((id) => CARDS[id]).filter(Boolean);
739
  const traceCount = flatten(stone.members).length;
740
  const flat = flatten(stone.members);
741
+ const hasError = flat.some((m) => m.status === "errored");
742
+ const hasWarn = flat.some((m) => m.status === "warned");
743
  const defaultOpen =
744
  provenanceMode === "all-expanded" ? true :
745
  provenanceMode === "all-collapsed" ? false :
746
+ /* smart */ hasError || hasWarn;
747
  const [traceOpen, setTraceOpen] = useFi(defaultOpen);
748
  /* Re-sync if user toggles tweak */
749
  useFiMemo(() => setTraceOpen(defaultOpen), [provenanceMode]);
 
798
  >
799
  <span className="f-prov-caret" aria-hidden="true">{traceOpen ? "β–Ύ" : "β–Έ"}</span>
800
  <span className="f-prov-label">{traceOpen ? "Hide" : "Show"} provenance</span>
801
+ <span className="f-prov-meta">Β· {traceCount} function{traceCount === 1 ? "" : "s"}{hasError ? " Β· errored" : hasWarn ? " Β· warned" : ""}</span>
802
  </button>
803
  {traceOpen && (
804
  <div className="f-prov-body">
 
814
 
815
  const RunHealth44 = ({ totalCards }) => {
816
  const all = window.STONES.flatMap((s) => flatten(s.members));
817
+ const fired = all.filter((m) => m.status === "fired" || m.status === "warned").length;
818
  const total = all.length;
819
+ const silent = all.filter((m) => m.status === "silent_by_design").length;
820
+ const warn = all.filter((m) => m.status === "warned").length;
821
+ const error = all.filter((m) => m.status === "errored").length;
822
  return (
823
  <div className="f-runhealth">
824
  <span className="f-rh-item"><strong>5</strong> Stones</span>
 
827
  <span className="f-rh-sep">Β·</span>
828
  <span className="f-rh-item"><strong>{totalCards}</strong> evidence cards</span>
829
  <span className="f-rh-sep">Β·</span>
830
+ <span className="f-rh-item"><strong>24.0s</strong> wall-clock</span>
831
  {silent > 0 && <><span className="f-rh-sep">Β·</span><span className="f-rh-item f-rh-silent">{silent} silent</span></>}
832
+ {warn > 0 && <><span className="f-rh-sep">Β·</span><span className="f-rh-item f-rh-warn">{warn} warned</span></>}
833
+ {error > 0 && <><span className="f-rh-sep">Β·</span><span className="f-rh-item f-rh-err">{error} errored</span></>}
834
  </div>
835
  );
836
  };
docs/design_handoff/design_files/map.jsx CHANGED
@@ -175,34 +175,51 @@ const RedHookMapMock = ({ activeLayers, queriedAddress }) => {
175
  };
176
 
177
  const MapLegend = ({ activeLayers, onToggle }) => {
 
 
178
  const layers = [
179
- { key: "empirical", tier: "empirical", label: "Sandy Inundation Zone (2012)", source: "NYC OEM" },
180
- { key: "modeled", tier: "modeled", label: "FEMA Zone AE , preliminary FIRM", source: "FEMA" },
181
- { key: "synthetic", tier: "synthetic", label: "Synthetic SAR (2025-09-14)", source: "TerraMind v1.2" },
182
- { key: "proxy", tier: "proxy", label: "311 flood complaints, 2019–25", source: "NYC 311" },
 
183
  ];
 
 
 
 
 
184
  return (
185
- <div className="map-legend" role="group" aria-label="Map layer toggles">
186
  <div className="map-legend-head">
187
- <span className="section-label">Layers</span>
188
  </div>
189
- {layers.map((l) => (
190
- <button
191
- key={l.key}
192
- type="button"
193
- className={`map-legend-item ${activeLayers[l.key] ? "is-on" : "is-off"}`}
194
- onClick={() => onToggle(l.key)}
195
- aria-pressed={activeLayers[l.key]}
196
- >
197
- <span className="map-legend-swatch" aria-hidden="true">
198
- <TierGlyph tier={l.tier} size={11} color={`var(--tier-${l.tier})`} />
199
- </span>
200
- <span className="map-legend-text">
201
- <span className="map-legend-label">{l.label}</span>
202
- <span className="map-legend-source">{l.source} Β· <TierBadge tier={l.tier} compact /></span>
203
- </span>
204
- <span className="map-legend-toggle" aria-hidden="true">{activeLayers[l.key] ? "ON" : "OFF"}</span>
205
- </button>
 
 
 
 
 
 
 
 
 
206
  ))}
207
  </div>
208
  );
 
175
  };
176
 
177
  const MapLegend = ({ activeLayers, onToggle }) => {
178
+ /* v0.4.5: restructured by Stone. Each row carries its source-Stone so the
179
+ panel visually mirrors the Findings stack. The tier swatch is unchanged. */
180
  const layers = [
181
+ { key: "empirical", tier: "empirical", stone: "cornerstone", label: "Sandy Inundation Zone (2012)", source: "NYC OEM" },
182
+ { key: "modeled", tier: "modeled", stone: "cornerstone", label: "FEMA Zone AE Β· preliminary FIRM", source: "FEMA" },
183
+ { key: "proxy", tier: "proxy", stone: "touchstone", label: "311 flood complaints, 2019–25", source: "NYC 311" },
184
+ { key: "synthetic", tier: "synthetic", stone: "touchstone", label: "Synthetic LULC (2025-09-14)", source: "TerraMind v1.2" },
185
+ { key: "prithvi-pluvial", tier: "modeled", stone: "touchstone", label: "Prithvi pluvial prediction", source: "Prithvi-NYC v2" },
186
  ];
187
+ const stoneOrder = ["cornerstone", "touchstone"];
188
+ const stoneMeta = {
189
+ cornerstone: { name: "Cornerstone", role: "what NYC's ground remembers" },
190
+ touchstone: { name: "Touchstone", role: "what's happening now" },
191
+ };
192
  return (
193
+ <div className="map-legend" role="group" aria-label="Map layer toggles, grouped by Stone">
194
  <div className="map-legend-head">
195
+ <span className="section-label">Layers Β· by Stone</span>
196
  </div>
197
+ {stoneOrder.map((sk) => (
198
+ <div key={sk} className={`map-legend-stone map-legend-stone-${sk}`} data-stone={sk}>
199
+ <div className="map-legend-stone-head">
200
+ <span className={`map-legend-stone-dot map-legend-stone-dot-${sk}`} aria-hidden="true"></span>
201
+ <span className="map-legend-stone-name">{stoneMeta[sk].name}</span>
202
+ <span className="map-legend-stone-role">Β· {stoneMeta[sk].role}</span>
203
+ </div>
204
+ {layers.filter((l) => l.stone === sk).map((l) => (
205
+ <button
206
+ key={l.key}
207
+ type="button"
208
+ className={`map-legend-item ${activeLayers[l.key] ? "is-on" : "is-off"}`}
209
+ onClick={() => onToggle(l.key)}
210
+ aria-pressed={activeLayers[l.key]}
211
+ >
212
+ <span className="map-legend-swatch" aria-hidden="true">
213
+ <TierGlyph tier={l.tier} size={11} color={`var(--tier-${l.tier})`} />
214
+ </span>
215
+ <span className="map-legend-text">
216
+ <span className="map-legend-label">{l.label}</span>
217
+ <span className="map-legend-source">{l.source} Β· <TierBadge tier={l.tier} compact /></span>
218
+ </span>
219
+ <span className="map-legend-toggle" aria-hidden="true">{activeLayers[l.key] ? "ON" : "OFF"}</span>
220
+ </button>
221
+ ))}
222
+ </div>
223
  ))}
224
  </div>
225
  );
docs/design_handoff/design_files/shell.jsx CHANGED
@@ -73,9 +73,11 @@ const ColdStart = ({ onPick, onSubmit }) => {
73
  <div className="cold-start-trust">
74
  <span className="section-label">How Riprap is built</span>
75
  <p className="cold-start-thesis">
76
- <strong>Cornerstone</strong> remembers. <strong>Keystone</strong> tallies.
77
- {" "}<strong>Touchstone</strong> watches. <strong>Lodestone</strong> projects.
78
- {" "}<strong>Capstone</strong> writes it all down with citations.
 
 
79
  </p>
80
  <ul className="cold-start-trust-list">
81
  <li>Five named cognitive roles compose ~25 atomic specialists. <a href="#spec-stones">Architecture β†’</a></li>
 
73
  <div className="cold-start-trust">
74
  <span className="section-label">How Riprap is built</span>
75
  <p className="cold-start-thesis">
76
+ <span className="cold-start-thesis-stone-dot cold-start-thesis-stone-dot-cornerstone" aria-hidden="true"></span><strong>Cornerstone</strong> remembers.{" "}
77
+ <span className="cold-start-thesis-stone-dot cold-start-thesis-stone-dot-keystone" aria-hidden="true"></span><strong>Keystone</strong> tallies.
78
+ {" "}<span className="cold-start-thesis-stone-dot cold-start-thesis-stone-dot-touchstone" aria-hidden="true"></span><strong>Touchstone</strong> watches.{" "}
79
+ <span className="cold-start-thesis-stone-dot cold-start-thesis-stone-dot-lodestone" aria-hidden="true"></span><strong>Lodestone</strong> projects.
80
+ {" "}<span className="cold-start-thesis-stone-dot cold-start-thesis-stone-dot-capstone" aria-hidden="true"></span><strong>Capstone</strong> writes it all down with citations.
81
  </p>
82
  <ul className="cold-start-trust-list">
83
  <li>Five named cognitive roles compose ~25 atomic specialists. <a href="#spec-stones">Architecture β†’</a></li>
docs/design_handoff/design_files/stones-trace.jsx CHANGED
@@ -1,5 +1,6 @@
1
- /* Riprap v0.4.4 , Stone-banded trace (Treatment A).
2
- Pulled out of the deleted spec-v043.jsx; this is what the mockup uses.
 
3
  */
4
 
5
  const { useState: useStV44, useEffect: useEfV44 } = React;
@@ -9,11 +10,11 @@ const STONES = [
9
  key: "cornerstone", name: "Cornerstone", role: "the hazard reader",
10
  tag: "what NYC's ground remembers",
11
  members: [
12
- { id: "c1", name: "sandy_inundation.lookup", status: "ok", ms: 380, tier: "empirical" },
13
- { id: "c2", name: "usgs_hwm.spatial_join", status: "ok", ms: 460, tier: "empirical" },
14
- { id: "c3", name: "fema_firm.lookup", status: "ok", ms: 290, tier: "modeled" },
15
- { id: "c4", name: "dep_stormwater.lookup", status: "ok", ms: 540, tier: "modeled" },
16
- { id: "c5", name: "prithvi.historical_segment",status: "warn", ms: 1240, tier: "modeled",
17
  warning: "deprecation: Prithvi-100M v1 β†’ v2 migration scheduled 2026-Q3" },
18
  ],
19
  },
@@ -21,68 +22,73 @@ const STONES = [
21
  key: "keystone", name: "Keystone", role: "the asset register",
22
  tag: "what's exposed",
23
  members: [
24
- { id: "k1", name: "mta.entrance_join", status: "ok", ms: 220, tier: "empirical" },
25
- { id: "k2", name: "nycha.development_join", status: "ok", ms: 538, tier: "empirical" },
26
- { id: "k3", name: "doe.school_join", status: "ok", ms: 180, tier: "empirical" },
27
- { id: "k4", name: "doh.facility_join", status: "ok", ms: 210, tier: "empirical" },
28
- { id: "k5", name: "pluto.lot_lookup", status: "ok", ms: 142, tier: "empirical" },
 
 
 
 
 
29
  ],
30
  },
31
  {
32
  key: "touchstone", name: "Touchstone", role: "the live observer",
33
  tag: "what's happening now",
34
  members: [
35
- { id: "t1", name: "floodnet.history", status: "ok", ms: 1240, tier: "empirical" },
36
- { id: "t2", name: "nyc311.flood_complaints", status: "ok", ms: 880, tier: "proxy" },
37
- { id: "t3", name: "tidalgauge.recent", status: "silent", ms: 0, tier: "empirical",
38
- note: "out of range (gauge >2km from address)" },
 
39
  ],
40
  },
41
  {
42
  key: "lodestone", name: "Lodestone", role: "the projector",
43
  tag: "what's coming",
44
  members: [
45
- { id: "l1", name: "npcc4.slr_projection", status: "ok", ms: 320, tier: "modeled" },
46
- { id: "l2", name: "ttm.foundation_run", status: "ok", ms: 14000, tier: "modeled",
47
- children: [
48
- { id: "l2a", name: "ttm.zarr_load", status: "ok", ms: 2400, tier: "modeled" },
49
- { id: "l2b", name: "ttm.checkpoint", status: "error", ms: 10750, tier: "modeled",
50
- error: "checkpoint architecture mismatch (ttm-r2 vs ttm-r1 weights)" },
51
- { id: "l2c", name: "ttm.cpu_inference", status: "ok", ms: 850, tier: "modeled" },
52
- ],
53
- },
54
- { id: "l3", name: "terramind.synthetic_sar", status: "ok", ms: 8200, tier: "synthetic" },
55
- { id: "l4", name: "nfip.claims_aggregation", status: "ok", ms: 460, tier: "proxy" },
56
  ],
57
  },
58
  {
59
  key: "capstone", name: "Capstone", role: "the synthesizer",
60
  tag: "writes it all down with citations",
61
  members: [
62
- { id: "p1", name: "granite.compose_briefing", status: "ok", ms: 3200, tier: "modeled" },
63
- { id: "p2", name: "mellea.grounding_check", status: "ok", ms: 480, tier: "modeled" },
64
- { id: "p3", name: "weasyprint.render_artifact",status: "ok", ms: 920, tier: null },
65
  ],
66
  },
67
  ];
68
 
69
- const fmtMs = (ms) => ms === 0 ? ", " : ms < 1000 ? ms + "ms" : (ms / 1000).toFixed(1) + "s";
70
  const tierColor = (t) => t ? `var(--tier-${t})` : "var(--ink-tertiary)";
71
 
 
 
72
  const StoneAggregate = ({ stone }) => {
73
- const flat = (members) => members.flatMap(m => m.children ? [m, ...flat(m.children)] : [m]);
74
- const all = flat(stone.members);
75
- const fired = all.filter(m => m.status === "ok").length;
76
- const silent = all.filter(m => m.status === "silent").length;
77
- const warn = all.filter(m => m.status === "warn").length;
78
- const error = all.filter(m => m.status === "error").length;
79
  const ms = stone.members.reduce((acc, m) => Math.max(acc, m.ms || 0), 0);
80
  return (
81
  <span className="stone-band-agg">
82
  <span className="stone-band-agg-num">{fired}</span> fired
83
  {silent > 0 && <> Β· <span className="stone-band-agg-num">{silent}</span> silent</>}
84
  {warn > 0 && <> Β· <span className="stone-band-agg-warn">{warn} warn</span></>}
85
- {error > 0 && <> Β· <span className="stone-band-agg-err">{error} error</span></>}
 
86
  {" Β· "}<span className="stone-band-agg-ms">{fmtMs(ms)}</span>
87
  </span>
88
  );
@@ -103,26 +109,71 @@ const TraceRow = ({ m, indent = 16 }) => {
103
  </details>
104
  );
105
  }
106
- if (m.status === "error") {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  return (
108
- <div className="trace-row trace-row-error" style={{ paddingLeft: indent }}>
109
- <span className="trace-bullet">●</span>
110
  <span className="trace-name">{m.name}</span>
111
- <span className="trace-status trace-status-err">error</span>
112
  <span className="trace-tier" style={{ color: tierColor(m.tier) }}>{m.tier || ""}</span>
113
  <span className="trace-ms">{fmtMs(m.ms)}</span>
114
- <span className="trace-error-summary">{m.error}</span>
 
115
  </div>
116
  );
117
  }
 
118
  return (
119
- <div className={`trace-row trace-row-${m.status}`} style={{ paddingLeft: indent }}>
120
- <span className="trace-bullet">{m.status === "silent" ? "β–‘" : m.status === "warn" ? "!" : "Β·"}</span>
121
  <span className="trace-name">{m.name}</span>
122
- <span className="trace-status">{m.status}</span>
123
  <span className="trace-tier" style={{ color: tierColor(m.tier) }}>{m.tier || ""}</span>
124
  <span className="trace-ms">{fmtMs(m.ms)}</span>
125
- {m.warning && <span className="trace-warn-note">{m.warning}</span>}
126
  {m.note && <span className="trace-note">{m.note}</span>}
127
  </div>
128
  );
@@ -131,11 +182,11 @@ const TraceRow = ({ m, indent = 16 }) => {
131
  const StoneBand = ({ stone }) => {
132
  const [open, setOpen] = useStV44(true);
133
  return (
134
- <section className={`stone-band stone-band-${stone.key}`} aria-labelledby={`band-h-${stone.key}`}>
135
  <button className="stone-band-head" aria-expanded={open} onClick={() => setOpen(o => !o)}>
136
  <span className="stone-band-head-left">
137
  <span id={`band-h-${stone.key}`} className="stone-band-name">{stone.name}</span>
138
- <span className="stone-band-role">, {stone.role}</span>
139
  <span className="stone-band-tag">{stone.tag}</span>
140
  </span>
141
  <StoneAggregate stone={stone}/>
@@ -150,11 +201,15 @@ const StoneBand = ({ stone }) => {
150
  };
151
 
152
  const StoneTrace = () => {
 
 
 
 
153
  return (
154
  <div className="trace-ui-v44">
155
  <header className="stone-trace-head">
156
  <span className="section-label">Run trace Β· 5 Stones</span>
157
- <span className="stone-trace-tally">17 fired Β· 1 silent Β· 1 warn Β· 1 error Β· 14.0s</span>
158
  </header>
159
  {STONES.map(s => <StoneBand key={s.key} stone={s}/>)}
160
  </div>
 
1
+ /* Riprap v0.4.5 Β· Stone-banded trace.
2
+ Status enum (v0.4.5): fired / silent_by_design / warned / errored / not_invoked.
3
+ See V0.4.5_SPEC.md Β§1 for the rationale.
4
  */
5
 
6
  const { useState: useStV44, useEffect: useEfV44 } = React;
 
10
  key: "cornerstone", name: "Cornerstone", role: "the hazard reader",
11
  tag: "what NYC's ground remembers",
12
  members: [
13
+ { id: "c1", name: "sandy_inundation.lookup", status: "fired", ms: 380, tier: "empirical" },
14
+ { id: "c2", name: "usgs_hwm.spatial_join", status: "fired", ms: 460, tier: "empirical" },
15
+ { id: "c3", name: "fema_firm.lookup", status: "fired", ms: 290, tier: "modeled" },
16
+ { id: "c4", name: "dep_stormwater.lookup", status: "fired", ms: 540, tier: "modeled" },
17
+ { id: "c5", name: "prithvi.historical_segment",status: "warned", ms: 1240, tier: "modeled",
18
  warning: "deprecation: Prithvi-100M v1 β†’ v2 migration scheduled 2026-Q3" },
19
  ],
20
  },
 
22
  key: "keystone", name: "Keystone", role: "the asset register",
23
  tag: "what's exposed",
24
  members: [
25
+ { id: "k1", name: "mta_entrance_exposure", status: "silent_by_design", ms: 30, tier: "empirical",
26
+ note: "no entrances within radius" },
27
+ { id: "k2", name: "nycha.development_join", status: "silent_by_design", ms: 28, tier: "empirical",
28
+ note: "no NYCHA developments within 1.0 mi" },
29
+ { id: "k3", name: "doe.school_join", status: "silent_by_design", ms: 24, tier: "empirical",
30
+ note: "no DOE schools within 1.0 mi" },
31
+ { id: "k4", name: "doh.facility_join", status: "silent_by_design", ms: 22, tier: "empirical",
32
+ note: "no acute-care hospitals within 1.0 mi" },
33
+ { id: "k5", name: "pluto.lot_lookup", status: "silent_by_design", ms: 18, tier: "empirical",
34
+ note: "PLUTO join skipped: queried address not in NYC PLUTO dataset" },
35
  ],
36
  },
37
  {
38
  key: "touchstone", name: "Touchstone", role: "the live observer",
39
  tag: "what's happening now",
40
  members: [
41
+ { id: "t1", name: "floodnet.history", status: "fired", ms: 1240, tier: "empirical" },
42
+ { id: "t2", name: "nyc311.flood_complaints", status: "fired", ms: 880, tier: "proxy" },
43
+ { id: "t3", name: "noaa_coops.recent", status: "fired", ms: 410, tier: "empirical" },
44
+ { id: "t4", name: "terramind.lulc", status: "fired", ms: 2100, tier: "synthetic" },
45
+ { id: "t5", name: "prithvi_nyc_pluvial", status: "fired", ms: 1820, tier: "modeled" },
46
  ],
47
  },
48
  {
49
  key: "lodestone", name: "Lodestone", role: "the projector",
50
  tag: "what's coming",
51
  members: [
52
+ { id: "l1", name: "npcc4.slr_projection", status: "fired", ms: 320, tier: "modeled" },
53
+ { id: "l2", name: "ttm_battery_surge.zero_shot",status: "fired", ms: 1500, tier: "modeled" },
54
+ { id: "l3", name: "ttm_battery_surge.fine_tune",status: "fired", ms: 1480, tier: "modeled" },
55
+ { id: "l4", name: "floodnet_forecast", status: "silent_by_design", ms: 14, tier: "modeled",
56
+ note: "sensor has only 2 historical events; forecast omitted (silent-floor: 5)" },
57
+ { id: "l5", name: "ttm_311_forecast", status: "errored", ms: 0, tier: "modeled",
58
+ error: "311 history fetch failed: HTTP 503 at NYC OpenData (3 retries)" },
 
 
 
 
59
  ],
60
  },
61
  {
62
  key: "capstone", name: "Capstone", role: "the synthesizer",
63
  tag: "writes it all down with citations",
64
  members: [
65
+ { id: "p1", name: "granite.compose_briefing", status: "fired", ms: 3200, tier: "modeled" },
66
+ { id: "p2", name: "mellea.grounding_check", status: "fired", ms: 480, tier: "modeled" },
67
+ { id: "p3", name: "weasyprint.render_artifact",status: "fired", ms: 920, tier: null },
68
  ],
69
  },
70
  ];
71
 
72
+ const fmtMs = (ms) => ms === 0 ? "β€”" : ms < 1000 ? ms + "ms" : (ms / 1000).toFixed(1) + "s";
73
  const tierColor = (t) => t ? `var(--tier-${t})` : "var(--ink-tertiary)";
74
 
75
+ const flat04 = (members) => members.flatMap(m => m.children ? [m, ...flat04(m.children)] : [m]);
76
+
77
  const StoneAggregate = ({ stone }) => {
78
+ const all = flat04(stone.members);
79
+ const fired = all.filter(m => m.status === "fired" || m.status === "warned").length;
80
+ const silent = all.filter(m => m.status === "silent_by_design").length;
81
+ const warn = all.filter(m => m.status === "warned").length;
82
+ const error = all.filter(m => m.status === "errored").length;
83
+ const notInvoked = all.filter(m => m.status === "not_invoked").length;
84
  const ms = stone.members.reduce((acc, m) => Math.max(acc, m.ms || 0), 0);
85
  return (
86
  <span className="stone-band-agg">
87
  <span className="stone-band-agg-num">{fired}</span> fired
88
  {silent > 0 && <> Β· <span className="stone-band-agg-num">{silent}</span> silent</>}
89
  {warn > 0 && <> Β· <span className="stone-band-agg-warn">{warn} warn</span></>}
90
+ {error > 0 && <> Β· <span className="stone-band-agg-err">{error} errored</span></>}
91
+ {notInvoked > 0 && <> Β· <span className="stone-band-agg-num">{notInvoked}</span> not invoked</>}
92
  {" Β· "}<span className="stone-band-agg-ms">{fmtMs(ms)}</span>
93
  </span>
94
  );
 
109
  </details>
110
  );
111
  }
112
+ if (m.status === "errored") {
113
+ return (
114
+ <details className="trace-row trace-row-error trace-row-errored" style={{ paddingLeft: indent }}>
115
+ <summary>
116
+ <span className="trace-bullet trace-bullet-errored">β– </span>
117
+ <span className="trace-name">{m.name}</span>
118
+ <span className="trace-status trace-status-err">errored</span>
119
+ <span className="trace-tier" style={{ color: tierColor(m.tier) }}>{m.tier || ""}</span>
120
+ <span className="trace-ms">{fmtMs(m.ms)}</span>
121
+ <span className="trace-error-summary">{m.error}</span>
122
+ <span className="trace-error-expand" aria-hidden="true">click to expand</span>
123
+ </summary>
124
+ <div className="trace-error-body">
125
+ <div className="trace-error-line"><span className="trace-error-k">error</span><span>{m.error}</span></div>
126
+ <div className="trace-error-line"><span className="trace-error-k">retries</span><span>3</span></div>
127
+ <div className="trace-error-line"><span className="trace-error-k">elapsed</span><span>{fmtMs(m.ms)}</span></div>
128
+ </div>
129
+ </details>
130
+ );
131
+ }
132
+ if (m.status === "silent_by_design") {
133
+ return (
134
+ <div className="trace-row trace-row-silent-bd" style={{ paddingLeft: indent }}>
135
+ <span className="trace-bullet trace-bullet-silent">β–’</span>
136
+ <span className="trace-name">{m.name}</span>
137
+ <span className="trace-status">silent</span>
138
+ <span className="trace-tier" style={{ color: tierColor(m.tier) }}>{m.tier || ""}</span>
139
+ <span className="trace-ms">{fmtMs(m.ms)}</span>
140
+ {m.note && <span className="trace-silent-note">{m.note}</span>}
141
+ </div>
142
+ );
143
+ }
144
+ if (m.status === "not_invoked") {
145
+ return (
146
+ <div className="trace-row trace-row-not-invoked" style={{ paddingLeft: indent }}>
147
+ <span className="trace-bullet trace-bullet-notinvoked">β–«</span>
148
+ <span className="trace-name">{m.name}</span>
149
+ <span className="trace-status">not invoked</span>
150
+ <span className="trace-tier" style={{ color: tierColor(m.tier) }}>{m.tier || ""}</span>
151
+ <span className="trace-ms">β€”</span>
152
+ {m.note && <span className="trace-note">{m.note}</span>}
153
+ </div>
154
+ );
155
+ }
156
+ if (m.status === "warned") {
157
  return (
158
+ <div className="trace-row trace-row-warned" style={{ paddingLeft: indent }}>
159
+ <span className="trace-bullet trace-bullet-warned" style={{ color: tierColor(m.tier) }}>β– </span>
160
  <span className="trace-name">{m.name}</span>
161
+ <span className="trace-status trace-status-warn">warned</span>
162
  <span className="trace-tier" style={{ color: tierColor(m.tier) }}>{m.tier || ""}</span>
163
  <span className="trace-ms">{fmtMs(m.ms)}</span>
164
+ <span className="trace-warn-sidemark" aria-hidden="true">!</span>
165
+ {m.warning && <span className="trace-warn-note">{m.warning}</span>}
166
  </div>
167
  );
168
  }
169
+ /* fired */
170
  return (
171
+ <div className="trace-row trace-row-fired" style={{ paddingLeft: indent }}>
172
+ <span className="trace-bullet trace-bullet-fired" style={{ color: tierColor(m.tier), background: tierColor(m.tier) }}>β– </span>
173
  <span className="trace-name">{m.name}</span>
174
+ <span className="trace-status">fired</span>
175
  <span className="trace-tier" style={{ color: tierColor(m.tier) }}>{m.tier || ""}</span>
176
  <span className="trace-ms">{fmtMs(m.ms)}</span>
 
177
  {m.note && <span className="trace-note">{m.note}</span>}
178
  </div>
179
  );
 
182
  const StoneBand = ({ stone }) => {
183
  const [open, setOpen] = useStV44(true);
184
  return (
185
+ <section className={`stone-band stone-band-${stone.key}`} aria-labelledby={`band-h-${stone.key}`} data-stone={stone.key}>
186
  <button className="stone-band-head" aria-expanded={open} onClick={() => setOpen(o => !o)}>
187
  <span className="stone-band-head-left">
188
  <span id={`band-h-${stone.key}`} className="stone-band-name">{stone.name}</span>
189
+ <span className="stone-band-role"> Β· {stone.role}</span>
190
  <span className="stone-band-tag">{stone.tag}</span>
191
  </span>
192
  <StoneAggregate stone={stone}/>
 
201
  };
202
 
203
  const StoneTrace = () => {
204
+ const all = STONES.flatMap(s => flat04(s.members));
205
+ const fired = all.filter(m => m.status === "fired" || m.status === "warned").length;
206
+ const silent = all.filter(m => m.status === "silent_by_design").length;
207
+ const error = all.filter(m => m.status === "errored").length;
208
  return (
209
  <div className="trace-ui-v44">
210
  <header className="stone-trace-head">
211
  <span className="section-label">Run trace Β· 5 Stones</span>
212
+ <span className="stone-trace-tally">{fired} fired Β· {silent} silent Β· {error} errored Β· 24.0s</span>
213
  </header>
214
  {STONES.map(s => <StoneBand key={s.key} stone={s}/>)}
215
  </div>
docs/design_handoff/design_files/styles.css CHANGED
@@ -1083,6 +1083,158 @@
1083
  .stone-band, .stone-band-head, .treatment-tab { transition: none; }
1084
  }
1085
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1086
  /* ─────────── original v0.4.2 mobile rules ────────── */
1087
  @media (max-width: 720px) {
1088
  .app-header-inner { grid-template-columns: 1fr; gap: 8px; }
 
1083
  .stone-band, .stone-band-head, .treatment-tab { transition: none; }
1084
  }
1085
 
1086
+ /* ============================================================
1087
+ v0.4.5 β€” Stone accent layer + new status enum rules
1088
+ Hint-only treatment: a 3-px left-rule per Stone, scoped to
1089
+ .stone-band[data-stone] and .f-region[data-stone]. Tier
1090
+ swatches inside cards/legend rows are unchanged.
1091
+ See V0.4.5_SPEC.md Β§2 for the full rationale.
1092
+ ============================================================ */
1093
+
1094
+ /* 3-px left rule on Stone-banded sections */
1095
+ .stone-band[data-stone="cornerstone"] { border-left: 3px solid var(--stone-cornerstone); }
1096
+ .stone-band[data-stone="keystone"] { border-left: 3px solid var(--stone-keystone); }
1097
+ .stone-band[data-stone="touchstone"] { border-left: 3px solid var(--stone-touchstone); }
1098
+ .stone-band[data-stone="lodestone"] { border-left: 3px solid var(--stone-lodestone); }
1099
+ .stone-band[data-stone="capstone"] { border-left: 3px solid var(--stone-capstone); }
1100
+
1101
+ .f-region[data-stone="cornerstone"] { border-left: 3px solid var(--stone-cornerstone); }
1102
+ .f-region[data-stone="keystone"] { border-left: 3px solid var(--stone-keystone); }
1103
+ .f-region[data-stone="touchstone"] { border-left: 3px solid var(--stone-touchstone); }
1104
+ .f-region[data-stone="lodestone"] { border-left: 3px solid var(--stone-lodestone); }
1105
+ .f-region[data-stone="capstone"] { border-left: 3px solid var(--stone-capstone); }
1106
+
1107
+ /* Map Layers panel β€” Stone group dots + soft inset rule */
1108
+ .map-legend-stone {
1109
+ border-left: 2px solid transparent;
1110
+ padding-left: 10px;
1111
+ margin-bottom: 8px;
1112
+ }
1113
+ .map-legend-stone-cornerstone { border-left-color: var(--stone-cornerstone); }
1114
+ .map-legend-stone-keystone { border-left-color: var(--stone-keystone); }
1115
+ .map-legend-stone-touchstone { border-left-color: var(--stone-touchstone); }
1116
+ .map-legend-stone-lodestone { border-left-color: var(--stone-lodestone); }
1117
+ .map-legend-stone-capstone { border-left-color: var(--stone-capstone); }
1118
+ .map-legend-stone-head {
1119
+ display: flex; align-items: baseline; gap: 6px;
1120
+ margin: 8px 0 4px;
1121
+ font-family: var(--font-sans); font-size: 11px; letter-spacing: 0.04em;
1122
+ }
1123
+ .map-legend-stone-dot {
1124
+ width: 8px; height: 8px; border-radius: 50%;
1125
+ display: inline-block; align-self: center;
1126
+ }
1127
+ .map-legend-stone-dot-cornerstone { background: var(--stone-cornerstone); }
1128
+ .map-legend-stone-dot-keystone { background: var(--stone-keystone); }
1129
+ .map-legend-stone-dot-touchstone { background: var(--stone-touchstone); }
1130
+ .map-legend-stone-dot-lodestone { background: var(--stone-lodestone); }
1131
+ .map-legend-stone-dot-capstone { background: var(--stone-capstone); }
1132
+ .map-legend-stone-name { font-weight: 600; color: var(--ink); text-transform: uppercase; letter-spacing: 0.06em; }
1133
+ .map-legend-stone-role { color: var(--ink-tertiary); font-size: 11px; font-style: italic; font-family: var(--font-serif); }
1134
+
1135
+ /* Cold-start thesis row β€” colored dots beside Stone names */
1136
+ .cold-start-thesis-stone-dot {
1137
+ width: 7px; height: 7px; border-radius: 50%;
1138
+ display: inline-block; vertical-align: middle;
1139
+ margin-right: 5px; transform: translateY(-1px);
1140
+ }
1141
+ .cold-start-thesis-stone-dot-cornerstone { background: var(--stone-cornerstone); }
1142
+ .cold-start-thesis-stone-dot-keystone { background: var(--stone-keystone); }
1143
+ .cold-start-thesis-stone-dot-touchstone { background: var(--stone-touchstone); }
1144
+ .cold-start-thesis-stone-dot-lodestone { background: var(--stone-lodestone); }
1145
+ .cold-start-thesis-stone-dot-capstone { background: var(--stone-capstone); }
1146
+
1147
+ /* New status enum β€” replaces 0.4.4 anomaly/silent/warn/error rules.
1148
+ v0.4.5 statuses: fired, silent_by_design, warned, errored, not_invoked. */
1149
+ .trace-row { padding: 8px 0; font-family: var(--font-mono); font-size: 12px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
1150
+ .trace-row .trace-name { color: var(--ink); }
1151
+ .trace-row .trace-status { font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-tertiary); }
1152
+ .trace-row .trace-tier { font-size: 10px; letter-spacing: 0.04em; }
1153
+ .trace-row .trace-ms { color: var(--ink-tertiary); margin-left: auto; }
1154
+ .trace-row .trace-bullet { width: 12px; display: inline-block; text-align: center; }
1155
+
1156
+ .trace-row-fired { /* default β€” bullet color carries tier */ }
1157
+
1158
+ .trace-row-silent-bd .trace-name { color: var(--ink-secondary); }
1159
+ .trace-row-silent-bd .trace-bullet-silent { color: var(--ink-tertiary); }
1160
+ .trace-row-silent-bd .trace-silent-note { color: var(--ink-tertiary); font-style: italic; font-family: var(--font-serif); font-size: 12px; }
1161
+
1162
+ .trace-row-not-invoked .trace-name { color: var(--ink-tertiary); }
1163
+ .trace-row-not-invoked .trace-bullet-notinvoked { color: var(--ink-tertiary); }
1164
+
1165
+ .trace-row-warned {
1166
+ background: var(--status-warning-soft);
1167
+ /* keep stone left-rule; do NOT set border-left here */
1168
+ position: relative;
1169
+ padding-left: 8px;
1170
+ }
1171
+ .trace-row-warned .trace-status-warn { color: var(--status-warning); font-weight: 600; }
1172
+ .trace-row-warned .trace-warn-sidemark {
1173
+ color: var(--status-warning);
1174
+ font-weight: 700;
1175
+ margin-left: 4px;
1176
+ }
1177
+ .trace-row-warned .trace-warn-note { color: var(--ink-secondary); font-family: var(--font-serif); font-style: italic; font-size: 12px; }
1178
+
1179
+ .trace-row-errored {
1180
+ background: var(--status-error-soft);
1181
+ padding-left: 8px;
1182
+ }
1183
+ .trace-row-errored > summary { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; cursor: pointer; list-style: none; }
1184
+ .trace-row-errored > summary::-webkit-details-marker { display: none; }
1185
+ .trace-row-errored .trace-status-err { color: var(--status-error); font-weight: 600; }
1186
+ .trace-row-errored .trace-error-summary { color: var(--ink-secondary); font-style: italic; font-family: var(--font-serif); font-size: 12px; }
1187
+ .trace-row-errored .trace-error-expand { color: var(--ink-tertiary); font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; }
1188
+ .trace-row-errored[open] .trace-error-expand { display: none; }
1189
+ .trace-row-errored .trace-error-body {
1190
+ margin-top: 6px;
1191
+ padding: 8px 10px;
1192
+ background: rgba(255,255,255,0.6);
1193
+ border-left: 2px solid var(--status-error);
1194
+ font-size: 11px;
1195
+ display: grid; gap: 4px;
1196
+ }
1197
+ .trace-error-line { display: grid; grid-template-columns: 80px 1fr; gap: 8px; }
1198
+ .trace-error-k { color: var(--ink-tertiary); }
1199
+
1200
+ /* Tally text colors */
1201
+ .f-tally-warn { color: var(--status-warning); }
1202
+ .f-tally-err { color: var(--status-error); }
1203
+ .f-tally-notinvoked { color: var(--ink-tertiary); }
1204
+
1205
+ /* Hover link β€” when an evidence card is "linked" to a hovered map glyph */
1206
+ .f-card.is-linked,
1207
+ .finding-card.is-linked {
1208
+ outline: 2px solid var(--ink);
1209
+ outline-offset: 2px;
1210
+ }
1211
+
1212
+ /* TerraMind LULC class-mix bar (Touchstone synthetic-prior card) */
1213
+ .lulc-bar { display: flex; height: 12px; border-radius: 2px; overflow: hidden; margin-top: 6px; }
1214
+ .lulc-bar-seg { height: 100%; }
1215
+ .lulc-legend { display: flex; flex-wrap: wrap; gap: 8px 14px; margin-top: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--ink-secondary); }
1216
+ .lulc-legend-swatch { display: inline-block; width: 8px; height: 8px; margin-right: 4px; border-radius: 1px; vertical-align: middle; }
1217
+
1218
+ /* Fine-tuned TTM trim line + hardware badge */
1219
+ .ttm-ft-trim { border-top: 1px solid var(--rule-soft); margin-top: 10px; padding-top: 8px; display: grid; gap: 4px; font-family: var(--font-mono); font-size: 11px; color: var(--ink-secondary); }
1220
+ .ttm-ft-row { display: grid; grid-template-columns: 110px 1fr; gap: 8px; }
1221
+ .ttm-ft-k { color: var(--ink-tertiary); }
1222
+ .ttm-hw-badge {
1223
+ display: inline-block; padding: 1px 6px; border: 1px solid var(--rule-soft);
1224
+ border-radius: 2px; font-size: 10px; letter-spacing: 0.04em;
1225
+ font-family: var(--font-mono); color: var(--ink-secondary); background: white;
1226
+ }
1227
+
1228
+ /* Print: drop accents to neutral, drop hover/error tints */
1229
+ @media print {
1230
+ .stone-band[data-stone],
1231
+ .f-region[data-stone],
1232
+ .map-legend-stone { border-left-color: #999 !important; }
1233
+ .cold-start-thesis-stone-dot,
1234
+ .map-legend-stone-dot { background: #999 !important; }
1235
+ .trace-row-warned, .trace-row-errored { background: transparent !important; }
1236
+ }
1237
+
1238
  /* ─────────── original v0.4.2 mobile rules ────────── */
1239
  @media (max-width: 720px) {
1240
  .app-header-inner { grid-template-columns: 1fr; gap: 8px; }
docs/design_handoff/design_files/tokens.css CHANGED
@@ -51,6 +51,16 @@
51
  --leading-prose: 1.55;
52
  --leading-tight: 1.25;
53
 
 
 
 
 
 
 
 
 
 
 
54
  /* ── Spacing ── */
55
  --s-1: 4px;
56
  --s-2: 8px;
@@ -84,6 +94,16 @@ html, body {
84
  border-radius: 1px;
85
  }
86
 
 
 
 
 
 
 
 
 
 
 
87
  @media (prefers-reduced-motion: reduce) {
88
  *, *::before, *::after {
89
  animation-duration: 0.01ms !important;
 
51
  --leading-prose: 1.55;
52
  --leading-tight: 1.25;
53
 
54
+ /* ── Stone accent tokens (v0.4.5) ──
55
+ Five muted hint-colors keyed to Stones. Lβ‰ˆ45 OKLCH, chroma ≀0.04.
56
+ Hint-level decoration; never competes with the four-tier epistemic palette.
57
+ All five degrade to neutral gray in @media print (see below). */
58
+ --stone-cornerstone: #7C6F5E; /* warm taupe */
59
+ --stone-keystone: #5E6E7C; /* cool slate */
60
+ --stone-touchstone: #6B7C66; /* muted sage */
61
+ --stone-lodestone: #7C6E5E; /* softened ochre */
62
+ --stone-capstone: #5E5E6E; /* neutral indigo-gray */
63
+
64
  /* ── Spacing ── */
65
  --s-1: 4px;
66
  --s-2: 8px;
 
94
  border-radius: 1px;
95
  }
96
 
97
+ @media print {
98
+ :root {
99
+ --stone-cornerstone: #999;
100
+ --stone-keystone: #999;
101
+ --stone-touchstone: #999;
102
+ --stone-lodestone: #999;
103
+ --stone-capstone: #999;
104
+ }
105
+ }
106
+
107
  @media (prefers-reduced-motion: reduce) {
108
  *, *::before, *::after {
109
  animation-duration: 0.01ms !important;