seriffic's picture
docs: replace handoff bundle with v0.4.5 spec
79cf005

Handoff: Riprap Findings Region

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.

Read order for the implementer:

  1. CLAUDE_CODE_PROMPT.md β€” paste into Claude Code
  2. V0.4.5_SPEC.md β€” the nine fixes (now realized in v0.4.5.html)
  3. README.md (this file) β€” v0.4.4 reference
  4. design_files/ β€” prototypes (both v0.4.4 and v0.4.5 HTMLs are present)

Overview

Riprap is a citation-grounded Flood Exposure Briefing tool for New York City. A user enters an address, neighborhood, or proposed development; Riprap returns a written briefing where every numeric claim links to its primary public-record source (FEMA, USGS, NYC DOITT, FloodNet, NYC OpenData, etc.).

This handoff covers the Findings region β€” the structured-data sibling of the briefing prose. It groups model outputs into five named "Stones" (cognitive roles), each rendered as a card stack with explicit epistemic tiering, smart provenance traces, and cross-linking to the map.

The target codebase is SvelteKit (Svelte 5 with runes). The files in design_files/ are React-based prototypes β€” design references, not production code. Recreate them as Svelte components.

About the Design Files

The files in design_files/ are design references created in HTML + React β€” high-fidelity prototypes showing the intended look and behavior. They are not production code to copy directly.

Your task is to recreate these designs in the existing Svelte codebase, using its established patterns (Svelte 5 runes, scoped styles, the project's existing route structure and data layer). Lift visual values, copy, and interaction logic from the references; do not transpile JSX to Svelte.

File pairing (each prototype area has one or two source files):

  • Riprap Stone-Grouped UI v0.4.4.html β€” main prototype, the v0.4.4 Findings region in context
  • findings.jsx β€” Findings region: stones, cards, run-health, grammar reference (the centerpiece of this handoff)
  • briefing.jsx β€” long-form briefing prose with inline citations
  • map.jsx β€” mini-map with FEMA AE / HWM / FloodNet / 311 / address layers and link highlighting
  • trace.jsx, stones-trace.jsx, stone-evidence.jsx β€” provenance trace variants
  • shell.jsx β€” app header, footer, cold-start state
  • glyphs.jsx β€” four tier glyphs as inline SVG
  • tokens.css β€” design tokens (colors, type, spacing). Port verbatim.
  • styles.css β€” component CSS. Reference only; rewrite as scoped Svelte styles.
  • tweaks-panel.jsx, design-canvas.jsx, landing-variants.* β€” prototype-time tooling. Ignore.

Fidelity

High-fidelity (hifi). Pixel-perfect mockups with final colors, typography, spacing, glyphs, and interactions. Recreate the UI pixel-perfectly using the codebase's existing libraries and patterns. The tier color values, the IBM Plex font stack, the 4/8/12/16/24/32/48/64/96 spacing scale, and the four-tier glyph system are all final.

The Findings region in particular has been through several iterations (v0.4.0 β†’ v0.4.4) and is settled. Don't redesign card layouts; port them as-is.

Screens / Views

The product is a single-page app with two states: cold-start (no query) and briefing (query active).

1. Cold-start

Empty state with the wordmark, a deck explaining what Riprap is, a query input, three sample-query buttons, and a "How Riprap is built" trust band. See shell.jsx β†’ <ColdStart>.

  • Layout: centered single column, max-width ~720px, paper background (--paper)
  • Wordmark: riprap lowercase, IBM Plex Mono 14px / 600, with a 0.85em accent bar β–Œ prefix
  • Deck: serif paragraph, 18px, line-height 1.55, ink-secondary
  • Query input: full width, 1px ink border, mono placeholder, 16px
  • Submit: ink fill, paper text, mono caps, 13px
  • Sample queries: three buttons in a column, each shows mode (caps mono) / query (sans 16px) / sub (mono 12px tertiary)
  • Trust band: section-label heading, italic-serif "Cornerstone remembers. Keystone tallies. Touchstone watches. Lodestone projects. Capstone writes it all down with citations." then a bullet list

2. Briefing (active query)

Three-region layout, vertical stack:

  1. App header (sticky top): wordmark Β· context Β· query pill Β· methodology / export PDF / live status
  2. Briefing prose region: long-form text with inline [N] citations, three side-by-side panes (excerpt Β· evidence cards Β· mini map)
  3. Findings region: the v0.4.4 Stone-grouped card stack (this handoff's focus)
  4. Footer: tier legend + build line

The Findings region is the substantial new surface. Everything below documents it.

Findings Region (v0.4.4) β€” detailed spec

Composition

<FindingsRegion>
  <RunHealthStrip />               ← 1 row, top, summarizes all 25 model calls
  <StoneRegion stone="cornerstone" />
  <StoneRegion stone="keystone" />
  <StoneRegion stone="touchstone" />
  <StoneRegion stone="lodestone" />
  <StoneRegion stone="capstone" />
  <CardGrammarReference />         ← optional, on by default; one stub per variant
</FindingsRegion>

Stones

Five fixed roles, in order:

key name role tag
cornerstone Cornerstone the hazard reader what NYC's ground remembers
keystone Keystone the asset register what's exposed
touchstone Touchstone the present-tense witness what's happening now
lodestone Lodestone the future-pointer what's coming
capstone Capstone the writer what we say, with citations

Each <StoneRegion> has:

  • Header: Stone name (IBM Plex Serif 26px italic for the name, sans for the role) Β· role tagline Β· <StoneTally> chip showing N/M cards fired Β· provenance toggle button
  • Provenance trace: smart-default expansion (see below). Renders specialist tree with status pips.
  • Card grid: 12-column grid, each card spans 4 cols by default (3 per row); register/timeseries/raster cards may span 6.

Card data schema

type Card = {
  stone: "cornerstone" | "keystone" | "touchstone" | "lodestone" | "capstone";
  tier: "empirical" | "modeled" | "proxy" | "synthetic";
  variant: CardVariant;       // see below
  source: string;             // short label, e.g. "FEMA"
  agency: string;             // long form, e.g. "FEMA preliminary FIRM, panel 36047C..."
  vintage: string;            // e.g. "2024-Q3" or "2007–present"
  title: string;              // card title
  // variant-specific fields:
  headline?: string; subhead?: string;
  columns?: string[]; rows?: (string | number)[][];
  scalars?: { label: string; value: string; unit?: string }[];
  spark?: number[]; histogram?: number[];
  timeseries?: { hours: number[]; values: number[]; threshold?: number };
  forecast?: { years: number[]; p10: number[]; p50: number[]; p90: number[] };
  raster?: "stormwater" | "fema-ae" | "hwm" | "floodnet-density" | ...;
  register?: { tag: string; label: string; sourceId: string; detail: string }[];
  comparison?: { left: ScalarSet; right: ScalarSet; delta: string };
  meta?: Record<string, string>;
  // citation fan-out:
  cites?: { id: string; label: string; href?: string }[];
  // map link:
  mapLayer?: "fema-ae" | "hwm" | "floodnet" | "nycha" | "address" | ...;
};

See findings.jsx lines 12–230 for the canonical CARDS table populated for the Red Hook query.

Card grammar (12 variants)

Every card uses the same chrome β€” title row (sans 14/600), source Β· agency Β· vintage row (mono 11/tertiary), a body block, and a footer with the tier badge (3-letter caps mono) and a "cite" button. The body block is one of:

variant shape use
headline one big number/label, scenario-tagged subhead single-fact cards: "Zone AE"
tabular small NΓ—3 table, mono observation lists: HWM marks
scalars 2–3 labeled scalars in a row "1.2 m Β· 0.18 mi Β· 2012"
spark 60Γ—24 inline sparkline, no axes trend at a glance
histogram 8–12 bar histogram, mono labels distributions
timeseries 240Γ—84 line chart with threshold rule hourly water level, 311 calls
forecast 240Γ—88 fan chart (p10/p50/p90) 2050/2080 SLR, surge
raster 240Γ—120 stylized raster thumbnail FEMA AE polygon, stormwater extent, HWM contour
raster-pred same shape with dashed top-rule (synthetic tier) TerraMind 2050 prediction
register 3-col dense list (tag Β· label Β· sourceId), detail in title= tooltip NYCHA buildings, schools
comparison side-by-side scalar columns with delta FEMA-AE vs Prithvi-2050
meta definition list of run metadata model id, prompt hash, latency

Synthetic tier cards (TerraMind predictions, prior-only Lodestone outputs) get a dashed top-rule (1px dashed --tier-synthetic-line) to telegraph "no observed data here." Comparison cards always render synthetic.

The <CardGrammarReference> region renders one stub per variant in findings.jsx lines 529–581 β€” a visual catalog the design team uses to spot-check fidelity. Keep it, gate it on showGrammar prop (default true in dev, false in prod).

Tier system

Four epistemic tiers, encoded redundantly:

tier color glyph badge meaning
empirical #0B5394 (8.59:1) filled square EMP observed, ground-truth
modeled #2A6FA8 (5.41:1) open square MOD computed from observations
proxy #6B6B6B (5.74:1) dotted ring PRX indirect signal, e.g. 311 calls
synthetic #2A6FA8 + dashed hatched square SYN model prior, no observation

Glyphs are inline SVG, 12Γ—12, black-stroke. See glyphs.jsx for the four shapes.

Accessibility: tier is always encoded by color + glyph + label, never color alone. Modeled and synthetic share a hue; the dashed top-rule and glyph carry the difference.

Provenance trace

Every Stone has a tree of specialists ("CORN-001: pull FEMA NFHL β†’ CORN-002: parse panel index β†’ ..."). Each specialist has a status: ok / warn / error / silent.

Smart-default rule (provenanceMode = "smart"):

  • All-ok Stone β†’ collapsed by default, single-line summary "12/12 specialists fired clean"
  • Any warn or error β†’ expanded, full tree visible
  • All silent β†’ collapsed, single-line "no firings (section omitted from briefing)"

provenanceMode = "expanded" forces all expanded; "collapsed" forces all collapsed (warn/error still get a count chip).

The trace UI: indented tree, mono ids, status pip (●/β–²/β– /β—‹) in tier-color or warn/error colors. Specialist names italic-serif. Hovering a specialist row dims the rest. See trace.jsx for the leaf and stones-trace.jsx for the per-Stone composition.

Run-health strip

Single row above the Stones, full-width. Shows:

  • Total specialists fired / total specialists registered (e.g. 83/100)
  • Per-tier breakdown as four chips: EMP 41 Β· MOD 28 Β· PRX 12 Β· SYN 2
  • Total runtime (e.g. 3.4s)
  • Cache-hit ratio (e.g. 92%)

Mono throughout. Background --paper-deep, 1px ink-soft top + bottom rule.

Hover linking

Every card with a mapLayer is hoverable. On hover (or focus), the card's key becomes the page-level linkedKey. The briefing's map frame reads linkedKey and:

  • Adds is-link-{layer} class to its root, which lights up that layer (see map.jsx for the CSS rules)
  • Renders a small label badge bottom-right: "linked: {layer}"

The same applies in reverse: hovering a layer in the map sets linkedKey to the corresponding card key, which gets a 2px accent outline.

In Svelte: lift linkedKey to +page.svelte as let linkedKey = $state(null), pass it down both branches, and have the cards / layers update it on pointerenter / focus / pointerleave / blur.

Density

density: "compact" | "comfortable" (default comfortable).

  • Comfortable: 16px card padding, 14px line-height multiplier 1.4
  • Compact: 10px card padding, 12px line-height multiplier 1.25, register-card row height 18px (vs 24px)

Pass through to all card bodies; only register/tabular/meta visibly change.

Interactions & Behavior

  • Card hover: 200ms background-color transition to --paper-deep, 2px accent outline if linked
  • Cite button: opens a small popover with the full citation list (cites[]), each row a link to the source PDF/page
  • Provenance toggle: button with aria-expanded, animates the tree open/closed via details/summary or scoped max-height transition (≀200ms)
  • Map layer hover: sets linkedKey, 100ms layer fill opacity transition
  • Reduced motion: all transitions become 0.01ms via the global rule in tokens.css. Don't add motion on top.
  • Keyboard: every card is tabindex=0 with aria-label="{tier} card Β· {title} Β· {source}". Cite button is real <button>. Provenance toggle is real <button>.
  • Loading: cards show a 1px ink-soft skeleton (no spinner); replace with content when the specialist returns
  • Error / silent: cards with status error render a 1-line "specialist failed: {reason}" in tier-error color and stay; cards with status silent are omitted entirely (silence over confabulation β€” design tenet)

State Management

Svelte 5 runes; lift to +page.svelte:

<script>
  let query = $state(null);
  let density = $state("comfortable");
  let provenanceMode = $state("smart");
  let showComparison = $state(false);
  let showGrammar = $state(false);   // dev-only toggle
  let linkedKey = $state(null);

  // Data: load once per query, hydrate from server
  let cardsByStone = $derived(loadCards(query));
  let runHealth = $derived(summarize(cardsByStone));
</script>

Children take props via $props(). No Svelte stores unless cross-route.

Data fetching: assume the existing codebase has a +page.server.ts load function that runs the 25 specialists and returns the Card[] payload. This handoff doesn't change the data layer; it changes the rendering.

Design Tokens

Port tokens.css verbatim. Key values:

Tier colors (all WCAG AA on white):

  • --tier-empirical: #0B5394 (8.59:1)
  • --tier-modeled: #2A6FA8 (5.41:1)
  • --tier-proxy: #6B6B6B (5.74:1)
  • --tier-synthetic: #2A6FA8 (pattern-differentiated)

Neutrals:

  • --paper: #FAFAF7 (warm near-white, USGS-report register)
  • --paper-deep: #F2F2EE
  • --ink: #1A1A1A Β· --ink-secondary: #4A4A4A Β· --ink-tertiary: #6B6B6B
  • --rule: #1A1A1A Β· --rule-soft: #C9C9C5

Accent:

  • --accent: #B8620A (for text, AA)
  • --accent-graphical: #D17C00 (for shapes/lines, β‰₯3:1)

Type:

  • --font-sans: "IBM Plex Sans" (UI, body)
  • --font-serif: "IBM Plex Serif" (Stone names, hero italic emphasis, oversized stone numerals)
  • --font-mono: "IBM Plex Mono" (labels, source ids, badges, table cells)

All three are SIL OFL / Apache; self-host or load from Google Fonts. No system fallbacks for branding β€” always specify the Plex stack, fall through only on load failure.

Spacing (--s-1 through --s-9): 4 / 8 / 12 / 16 / 24 / 32 / 48 / 64 / 96 px. Use these tokens; don't hand-roll values.

Type scale (suggested, not enforced):

  • 11px mono labels (section-label)
  • 12–13px mono row text
  • 14px sans card titles (600)
  • 16px sans body
  • 18px sans deck text
  • 26px serif italic Stone names
  • 36–52px serif headlines (italic for emphasis)

Radius: 0 throughout (this is a civic-tech-clean, USGS-report register; no rounded corners except 1px on focus rings).

Shadows: none. Differentiation by 1px rules and --paper-deep fills only.

Assets

  • Fonts: IBM Plex Sans / Serif / Mono. Self-host woff2 or Google Fonts.
  • Wordmark: text + accent bar β–Œ prefix, no logo file
  • Tier glyphs: inline SVG, see glyphs.jsx
  • Map raster thumbnails: hand-drawn SVG approximations using each layer's conventional palette. See findings.jsx β†’ RasterThumb. Replace with real raster previews if/when MapLibre tile snapshots are wired up.
  • Real map: the production map should use MapLibre GL with a custom style.json that respects the tier palette. Style fragments are sketched in shell.jsx comments.
  • No icon library. No Lucide, no Heroicons. SVG glyphs for tiers, mono characters (βŒ•, β†—, β–Œ) for chrome.
  • No emoji.

Files in this bundle

design_handoff_riprap_findings/
β”œβ”€β”€ CLAUDE_CODE_PROMPT.md        ← paste this into Claude Code first
β”œβ”€β”€ README.md                     ← you are here
└── design_files/
    β”œβ”€β”€ Riprap Stone-Grouped UI v0.4.4.html   ← main prototype, open in browser
    β”œβ”€β”€ Riprap Landing.html
    β”œβ”€β”€ Riprap Landing Variants.html
    β”œβ”€β”€ tokens.css                ← port verbatim
    β”œβ”€β”€ styles.css                ← reference only
    β”œβ”€β”€ findings.jsx              ← Findings region (this handoff's centerpiece)
    β”œβ”€β”€ briefing.jsx              ← long-form prose
    β”œβ”€β”€ evidence.jsx              ← evidence card stack used in briefing
    β”œβ”€β”€ map.jsx                   ← mini-map with link highlighting
    β”œβ”€β”€ trace.jsx
    β”œβ”€β”€ stones-trace.jsx
    β”œβ”€β”€ stone-evidence.jsx
    β”œβ”€β”€ shell.jsx                 ← header, footer, cold-start
    β”œβ”€β”€ glyphs.jsx                ← four tier glyphs
    β”œβ”€β”€ tweaks-panel.jsx          ← prototype tooling, ignore
    β”œβ”€β”€ design-canvas.jsx         ← prototype tooling, ignore
    β”œβ”€β”€ landing-variants.css      ← marketing-page exploration, ignore unless asked
    └── landing-variants.jsx      ← marketing-page exploration, ignore unless asked

Scope

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, to the extent they connect to Findings via linkedKey
  • The marketing landing page (see Β§"Landing page" below)
  • v0.4.5 deltas in V0.4.5_SPEC.md

Out of scope: the methodology PDF, the export-PDF flow.

Landing page

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/:

  • Riprap Landing.html β€” final shipping landing page
  • Riprap Landing Variants.html β€” three exploratory variants on a design canvas (kept for reference; do not port)

Landing structure (port Riprap Landing.html)

Four-section vertical scroll, max-width 1200px, paper background:

  1. Hero

    • Wordmark top-left (with β–Œ accent prefix)
    • Headline (52px serif, italic emphasis on any place, line-broken into two lines)
    • Deck (18px sans, max 70ch, ink-secondary)
    • Big query box: "Try:" label + cycling example queries on a fixed dotted-underline rail (rail width pinned, ellipsis fallback for overflow)
    • Submit button (ink fill, paper text, mono caps)
  2. "What you'll get back" preview β€” 3-pane grid (1.4fr / 1fr / 1fr), bottoms equalized:

    • Excerpt pane: serif briefing snippet with inline [N] citation pins; compact source list at bottom (tier-coded)
    • Evidence cards pane: 2x2 grid of compact evidence cards, each with tier-coded left rule (2px), claim, source
    • 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
  3. Five Stones strip β€” explanation grid: 5 columns (repeat(5, 1fr)), one cell per Stone:

    • Oversized italic-serif numerals 01..05 top-right of each cell (in --rule-soft, decorative)
    • Stone name (serif 22px / 500)
    • Role tagline (sans 13px / ink-secondary)
    • Italic-serif tag ("what NYC's ground remembers" etc.)
    • Dashed rule + mono source list at bottom
  4. Footer β€” earns its keep:

    • Tier legend (4 swatches: empirical / modeled / proxy / synthetic)
    • Build line (mono, ink-tertiary)

Landing β€” what's settled (don't redesign)

  • The hero dropped the redundant "Riprap" eyebrow; the wordmark already says it. Headline carries the lede.
  • Cycling examples ride a single fixed dotted underline (no jump on length change). Pin the rail width; ellipsis-truncate overflow.
  • Pane heights are equalized via a flex-1 spacer in the cards pane and flex: 1 on the map pane.
  • Map texture is a subtle 8px paper-grain grid behind the colored layers β€” gives it a map register, not a diagram register.
  • Stones strip uses oversized italic numerals as a typographic register (decorative, in soft-rule color).
  • Italic serif is intentional: hero emphasis + Stone tags + Stone numerals. Reads as a deliberate third voice.

Landing β€” em-dashes

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.

Landing β€” out of scope variants

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

Open questions for the design team

These are deliberately not resolved in the prototype; raise them with the designer before locking implementation:

  1. Should register cards paginate when N > 20, or stay scrollable in a fixed-height card?
  2. The comparison card is currently always synthetic (FEMA-AE vs Prithvi-2050). Will there be empirical-vs-empirical comparisons (e.g. FEMA-AE vs HWM observed)?
  3. Run-health cache-hit ratio β€” is this surfaced to end users, or is it dev-mode only?
  4. Provenance trace expansion: should expanded state survive across query changes, or reset?