jacquardSnapshot

← snapshot

21709 bytes
# Adversarial Review: Lowell visual redesign

---
date: 2026-08-23
slug: lowell-visual-redesign
mode: design-debate
status: proceed
scope: web/app/jacquard.css, web/app/layout.tsx, web/components/*, web/public/loc/*
diff_base: origin/main
veto_fired: true
follow_up_open: true
caught_real_issue: yes
---

## Outcome (recorded after the change shipped, same day)

**Kill criterion: not triggered.** `/` LCP **p50 = 808ms**, against a 2,500ms
trigger (n=11, measured from the `web_vitals` events Lowell reports into s10).
Note the p95 reads 244,184ms: three identical outliers from a single tab left
open during an infinite-render-loop bug (below). Mean would have read 67s and
falsely tripped the criterion — the review's choice of **p50** is the only
reason this didn't produce a bogus rollback.

**Both vetoes were acted on before shipping.** Iris #1: accent re-keyed to
indigo, amber reserved — and the ungoverned verdict additionally got a
diagonal hatch, so it is distinguishable from human-provenance gold by
texture as well as hue. Iris #2: `punch-cards.jpg` (afcwip001704) deleted
unread; only the four HABS/HAER plates ship.

**What the review did not catch**, found during in-browser verification:

1. **Infinite render loop.** `useSpeech` returned unstable function
   identities, so `JackieViewing`'s effects re-ran forever — thousands of API
   calls. Fixed by making `speak`/`listen`/`stop` referentially stable
   (`lib/speech.ts`) and depending on those rather than the hook object.
2. **Two promises that could never settle.** `speak()` awaited `onend` and
   `listen()` awaited `onend`/`onerror`; a sandboxed or permission-blocked
   browser fires neither, hanging the interview with no way forward. Both now
   carry watchdogs.
3. **A denied microphone silently skipped questions.** `listen()` returned a
   bare string, so "heard nothing" and "cannot hear" were indistinguishable.
   It now returns `{ text, error }` and a failed ear hands over the keyboard
   instead of advancing.
4. **Jackie read changed files from the wrong ref.** She used the target
   ref's head, so a path added on a branch resolved to "not found" and no file
   ever appeared on the floor. Now read from the source ref.

None of these are visual, which is the honest lesson: the design debate was
the wrong instrument for them, and only running the thing surfaced them.

> **Roster note (assumption):** `docs/adversarial-review/personas/` does not exist in
> this repo or in phantom-thread, so persona files could not be loaded or version-pinned.
> The canonical fixed roster from the skill is used verbatim; style is inferred from the
> roster's stated responsibilities. Flagged so the transcript is not read as
> more grounded than it is.

## TL;DR
- **decision:** proceed, with two vetoes resolved by mitigation
- **key reason:** "visually similar to s10" is satisfied by adopting s10's *structure,
  density, and motion tokens*; adopting its *palette* wholesale breaks the product's
  single most important honesty rule
- **top unresolved risk:** the light-theme phantom-thread sibling identity is being
  retired; that was a deliberate decision made earlier today and is now reversed
- **immediate next step:** re-key accents to indigo, drop the unverified-rights image,
  make the generative weave the hero

## Debate Config
- max_turns_per_persona_per_phase: 2 · max_claims_per_turn: 3 · evidence_required: true
- evidence_rule: at least one of file/metric/repro/operational-risk per major claim

## Persona Roster
Personas: maren-shipper, iris-auditor, kai-explorer, rafe-steward, juno-mediator
(canonical roster; files unavailable — see roster note)

## Context Pack

**Proposal.** Redesign the Lowell frontend: "much more modern", Library of Congress
imagery, modern version-control visual concepts, "visually similar to s10 but more
snappy and factory-like with interesting factory-like animations".

**Current state.** `web/app/jacquard.css` (~1,100 lines) is a fork of the
phantom-thread marketing theme (`~/github/one/rust-blog/app/phantom-thread/phantom-thread.css`),
light-default warm linen (`--jac-paper: #f4f0e6`), indigo accent `#56679a`, punched-card
card motif. Earlier today this sibling relationship was an *explicit* decision, recorded
in the plan file: "Keep identical (this is what makes the projects read as siblings):
paper `#f4f0e6`/`#ece6d8` light, `#16141a`/`#1d1a22` dark; ink/muted/line tokens…".

**s10 target.** Dark navy paper `#07131f`→`#050d14`, cream ink `#f2ead8`, olive
`#a4b887` eyebrows, terracotta `#e08d68` links, mustard `#e4c06d` active/gold. 232px
rail, 53px topbar, 13px base, uppercase 9.5–10.5px labels at 0.1–0.16em tracking,
`--chip-c` + `color-mix(in oklch,…)` chip system, fixed scanline texture on `body::before`,
motion = `cubic-bezier(0.22,0.61,0.36,1)` at 120/150ms, exactly four keyframes.

**Constraints.** Frontend-only (no engine/DTO change). Must preserve the verified
flows: init wizard, attest (incl. voice), gate preview/promote, log DAG, registry.

---

## Phase 1 — Offense

### Maren (shipper)
**Veto condition:** I veto if the redesign touches any Rust crate, or if the verified
flows (wizard 7 steps, attest → admitted → promote, honest-zero rendering) need
re-verification from scratch rather than a re-check.

1. **Four projects in one sentence.** "Modern + LoC imagery + VC visuals + factory
   animations" are four independent workstreams. Evidence: `web/app/jacquard.css` is
   ~1,100 lines and every one of the 14 route files consumes its class names
   (`.jac-punched`, `.jac-verdict--ungoverned`, `.jac-badge--human`). A palette swap is
   cheap; a class-name re-architecture is a rewrite with re-verification cost across
   `app/repos/[repo]/**` (9 route files).
2. **Expect agreement:** the swap should be *token-level* — redefine the custom
   properties in `:root`/`.jac-site`, keep every class name. Then the redesign is
   additive components + one tokens block, not a rewrite.
3. **Expect rejection:** I'll say it anyway — we should keep light mode as a toggle.
   I expect Kai and Iris to argue that shipping two full themes doubles the surface
   that can rot, and they're probably right.

### Iris (auditor)
**Veto condition #1:** I veto if amber/mustard becomes an ambient accent color while
it is *also* the semantic color of "this promotion verified nothing".

1. **Palette collision is a correctness bug, not a taste question.**
   `web/components/verdict-panel.tsx` renders `decisions_checked === 0` as
   `.jac-verdict--ungoverned` in amber, deliberately never green — that rule traces to
   `crates/jac-gate/src/lib.rs` ("Zero is reported honestly: it means the blast radius
   was ungoverned, not that anything was verified") and to jac-demo scene 11. In s10,
   `--mustard #e4c06d` is `.nav-item.active svg`, the `metric` chip, `--warn`, *and*
   `.soon-tag`. Adopt that palette wholesale and the color that means "nothing was
   verified" becomes the color of ordinary chrome. The gate's honesty survives in
   prose and dies in pixels.
2. **Unverified image rights.** `web/public/loc/punch-cards.jpg` is from
   `afcwip001704` — American Folklife Center, *Working in Paterson*. Repro: the LoC
   search API returned an **empty** `rights_advisory` for it, and three follow-up
   `https://www.loc.gov/item/<id>/?fo=json` calls returned `HTTP:000` (rate-limited),
   so rights were **never read**. The four HABS/HAER files (`ma1289`, `ma1398`,
   `ma1443`, `ma0543`) are federal-survey works and are safe; AFC folklife collections
   routinely carry donor restrictions. **Veto #2: no shipped asset whose rights we
   could not read.**
3. **Motion regression.** `web/app/jacquard.css` currently ends with a
   `@media (prefers-reduced-motion: reduce)` block covering `.jac-shuttle` and
   `.jac-materialize`. The s10 report states plainly: "No `prefers-reduced-motion`
   block exists — worth adding in the new app." Copying s10's motion approach
   wholesale means copying that omission, while simultaneously *increasing* the
   animation count. That is a net accessibility regression.

### Kai (explorer)
**Veto condition:** I veto if the "version-control visuals" ship as decoration —
icons and gradients that could be swapped for any other product's without anyone
noticing.

1. **A photo of a mill is nostalgia; a weave generated from the commit graph is the
   product.** Evidence: `web/lib/graph.ts` already computes lane assignments, and
   `web/components/snapshot-graph.tsx` already colors DAG nodes by provenance
   (`--jac-human` gold / `--jac-agent` indigo / split for mixed). That is *already*
   a generative textile — each snapshot is a pick, each lane a warp thread, the color
   is whose hands made it. Nobody has noticed because it renders as a 14px-wide rail
   next to a list. Widen it, make it the hero, and Lowell has a visual no other tool
   can copy, because it is drawn from the repo's own provenance.
2. **The strongest modern-VC idea is the one the substrate already earns.** Blame
   heatmaps, diff views, and commit graphs are table stakes. The thing jacquard has
   that GitHub does not is *provenance in the content address* and *decisions as
   objects*. The visual should show the cloth and which hands wove each stretch —
   `crates/jac-object/src/encode.rs:60` hashes the provenance byte, that line is the
   whole thesis.
3. **Expect rejection:** I want *zero* photographs. I expect to lose this one.

### Rafe (steward)
**Veto condition:** I veto if the LoC JPEGs are shipped unoptimized and loaded on
routes that don't display them.

1. **831KB of undifferentiated JPEG.** Evidence: `ls -la web/public/loc` —
   `punch-cards.jpg` 286KB (1417×2048), `boott-mills.jpg` 212KB, `cloth-room.jpg` 142KB,
   `mass-mills.jpg` 127KB, `canal-system.jpg` 63KB. `web/package.json` has no image
   pipeline beyond Next's built-in. If these land as CSS `background-image` they bypass
   `next/image` entirely — no resizing, no AVIF/WebP, no lazy loading.
2. **An undocumented reversal, in a repo whose entire thesis is recorded decisions.**
   The sibling-palette choice was made and written down this morning. Reversing it with
   no record is precisely the failure mode `jac-decision` exists to prevent. I am not
   opposing the reversal — I am insisting it be *recorded*. This review file is the
   record; it should say plainly that the light linen sibling identity is being retired.
3. **Two texture systems.** s10 uses a fixed `body::before` scanline (3px/5px pitch);
   Lowell has `.jac-weave` (warp/weft 5px/6px pitch + feTurbulence noise). Shipping
   both is one texture too many. Pick one and delete the other.

### Juno (mediator)
**Veto condition:** I veto if the synthesis does not state a decision boundary — which
parts of s10 are being adopted and which are explicitly not.

1. "Visually similar to s10" is underspecified and that ambiguity is where this goes
   wrong. Evidence: the s10 report separates cleanly into *structure* (rail/topbar/
   panel/chip/density/type-scale), *palette* (paper/ink/olive/terracotta/mustard), and
   *motion* (2 durations, 1 easing, 4 keyframes). Those can be adopted independently.
2. Iris's veto #1 means palette and structure must be decoupled or the product regresses.

---

## Phase 2 — Defense

### Maren
**Steelman-opposite:** The strongest version of Iris's palette objection is that this
isn't aesthetics at all — the amber/ungoverned mapping is load-bearing product
semantics with a traceable lineage from `jac-gate` through the DTO's server-computed
`ungoverned` boolean to one CSS class, and a palette that reuses that hue anywhere else
silently degrades it.

**Rebuttal:** Accepted without contest — and it *helps* my scope argument. Decoupling
palette from structure means the change is one `:root` token block plus additive
components, not a rewrite. Every existing class name survives.

**Residual risk accepted by Maren:** dark-mode-first means the light theme gets less
testing and may rot. I accept it, because we are keeping the light tokens as the
non-default branch of the same `data-jac-theme` switch, not deleting them.

### Iris
**Steelman-opposite:** Kai's strongest claim is that photographs are the *least*
defensible part of this: they are the heaviest asset, the only one with rights
exposure, and the only one that could be swapped for a different mill photo without
changing a single thing about what the product means.

**Rebuttal:** Correct on rights and weight — which is why I only veto the *unverified*
one, not all four. The HABS/HAER images earn their place differently than Kai allows:
Lowell is *named* for the mills, and a federal-survey photograph of the actual Boott
Mills is documentary, not decorative. But it must be treated (desaturated, low opacity,
behind content), never presented as chrome.

**Residual risk accepted by Iris:** the four HABS/HAER images are asserted public
domain on the basis of them being federal-survey works, not on a rights field I
successfully read. I accept that risk for HABS/HAER specifically; I do not accept it
for AFC.

### Kai
**Steelman-opposite:** Rafe's strongest claim is that a generative hero is *more*
maintenance than a photograph, not less — an SVG driven by live repo data has empty
states, degenerate cases (one snapshot, 40 snapshots, a 6-lane fan-out), and a
performance cliff that a static JPEG simply does not have.

**Rebuttal:** True, and it argues for bounding the input, not dropping the idea —
cap the hero weave at N picks, and it degrades to a still pattern when the repo is
empty. The existing `layoutGraph` in `web/lib/graph.ts` already handles the
degenerate cases because the log page has been driving it.

**Residual risk accepted by Kai:** I lose the no-photographs position. I accept
photographs as *texture* on the condition they never appear above content in the
z-order and never carry meaning a screen reader would miss.

### Rafe
**Steelman-opposite:** Maren's strongest claim is that this whole review is
disproportionate to a CSS change — no user data, no persistence, no API contract moves,
and the app is a local dev surface with one user.

**Rebuttal:** The proportionality rule keys on blast radius, not on user count. This
touches every route file's visual contract and reverses a same-day recorded decision.
The review is one file; the reversal it records outlives it.

**Residual risk accepted by Rafe:** I accept the two-theme maintenance burden, on the
condition that dark is the default and light is not separately advertised.

### Juno
**Steelman-opposite:** The strongest case against my "decouple palette from structure"
framing is that it produces something that is *neither* s10 nor phantom-thread — an
orphan that reads as a near-miss of both.

**Rebuttal:** This is the real risk and I am naming it rather than dissolving it. The
mitigation is that the *accent* is not arbitrary: indigo is already Lowell's accent
(`--jac-accent: #56679a`), it is the actual dye a Jacquard loom weaves with, and it is
the color the DAG already uses for agent-authored snapshots. Keeping indigo while
adopting s10's chassis makes it a sibling of both rather than a copy of either.

**Residual risk accepted by Juno:** stated in synthesis.

---

## Phase 3 — Synthesis

### Merged proposal (recommended)

**Adopt from s10 — structure, density, motion:**
- 232px left rail + 53px topbar + `.scroll`-only overflow app frame
- 13px base, uppercase tracked micro-labels (9.5–10.5px @ 0.1–0.16em), display font at
  weight 650 with negative tracking for numerals and page titles
- Panel/stat-tile/chip/segmented-control component grammar, incl. the
  `--chip-c` + `color-mix(in oklch,…)` single-rule chip technique
- Motion tokens: `--ease: cubic-bezier(0.22,0.61,0.36,1)`, `--dur-fast: 120ms`,
  `--dur-base: 150ms`; page `enter`, `fade`, `shimmer` skeletons
- Fixed full-viewport texture layer on a pseudo-element, app root lifted to `z-index: 1`

**Explicitly NOT adopted — palette semantics:**
- s10's mustard-as-chrome. Lowell keeps **indigo** as its accent and **reserves
  amber/gold exclusively** for two meanings: human provenance, and the ungoverned
  verdict. Nothing else in the UI may use it.
- s10's missing reduced-motion handling. Lowell keeps and extends its block.

**Version-control visuals (Kai's, promoted to hero):**
- The provenance DAG becomes the hero: a wide loom-warp weave where each snapshot is a
  pick, lanes are warp threads, and thread color is provenance. Driven by the existing
  `layoutGraph`.
- Diff-as-cloth: changed paths render as weft rows against the governing-decision scope.

**LoC imagery (bounded):**
- Ship **HABS/HAER only** (`cloth-room`, `mass-mills`, `boott-mills`, `canal-system`).
- **Delete `punch-cards.jpg`** pending a successful rights read.
- Treated: desaturated, ≤0.10 opacity, behind content, `aria-hidden`, never above the
  fold as the sole LCP element.

### Tradeoff table

| Option | Fidelity to ask | Honesty risk | Maint. cost | Verdict |
|---|---|---|---|---|
| A. Copy s10 wholesale | highest | **breaks amber rule** | low | rejected (Iris veto) |
| B. Structure+motion from s10, indigo accent, generative hero | high | none | medium | **recommended** |
| C. Keep linen, add animations only | low | none | lowest | rejected (fails the ask) |

### Veto status
- **Iris veto #1 (amber collision) — RESOLVED** by mitigation: accent re-keyed to
  indigo; amber/gold reserved to human-provenance + ungoverned verdict only.
- **Iris veto #2 (unverified rights) — RESOLVED** by mitigation: `punch-cards.jpg`
  deleted; only HABS/HAER federal-survey images ship.
- **Rafe veto (unoptimized images) — RESOLVED** by mitigation: images scoped to the
  routes that show them, served at ≤0.10 opacity behind content, with a documented
  follow-up to move to `next/image` if they ever go above the fold.
- **Maren veto (no Rust changes / no re-verification from scratch) — HELD:** the change
  is frontend-only and class names are preserved.
- **Kai veto (visuals must not be generic decoration) — HELD:** the hero is generated
  from repo provenance data.
- **Juno veto (decision boundary stated) — HELD:** stated above as adopt/not-adopt.

### Named dissent
**Kai holds the minority position:** ship zero photographs. He argues the four HABS/HAER
images are the only part of this redesign that another product could adopt unchanged,
and that a generative-only hero would be strictly more distinctive. He accepts the
outcome on the condition photographs remain texture, never chrome.

### Kill criterion
Lowell now reports Core Web Vitals to s10 per page
(`crates/jac-serve/src/routes/telemetry.rs` → `web_vitals` events grouped by
`context.pathname`; verified rendering in the console's Performance page).

**Pre-declared trigger:** if LCP p50 for route `/` exceeds **2,500ms** in the s10
console after the redesign ships, revert the hero imagery to CSS-only (drop the
`background-image` layers, keep the generative weave).
**Baseline measured 2026-08-23 before redesign:** `/` LCP p50 = 296ms;
`/repos/meridian-systems` = 1,004ms; `/repos/meridian-systems/log` = 696ms.

### Residual risk accepted by
**Juno.** The light-linen phantom-thread sibling identity is being retired as the
default. That was a deliberate, same-day decision and this reverses it. I accept the
risk that the two projects no longer read as siblings at a glance, on the grounds that
the *shared* inheritance (mill/loom/textile language, punched-card motif, the honest-
scope voice) is carried in structure and copy rather than in the paper color — and
that this file is the record of the reversal.

### Vote
**Proceed with Option B: 5–0**, with Kai's named dissent on photographs recorded.
Unanimity flag: not low-signal — two vetoes fired and materially reshaped the proposal
(palette decoupling; asset deletion).

---

## Core Takeaways
- **Claim that most reshaped the proposal:** Iris's palette-collision veto. It turned
  "make it look like s10" into "adopt s10's chassis, refuse its palette semantics",
  which is the difference between a re-skin and a regression.
- **Operational takeaway:** the web-vitals instrumentation built earlier today is now
  the kill-criterion instrument for a design change. Telemetry paid for itself inside
  one session.
- **Product takeaway:** the most modern, least copyable visual Lowell can ship is the
  one already latent in `lib/graph.ts` — provenance-colored cloth generated from the
  repo's own DAG.

## Decision Memo
- **recommended option:** B — s10 chassis + indigo accent + generative provenance hero
  + bounded HABS/HAER texture
- **unresolved risks:** sibling-identity divergence from phantom-thread (accepted,
  Juno); HABS/HAER public-domain status asserted from provenance, not a read rights
  field (accepted, Iris)
- **kill criterion:** `/` LCP p50 > 2,500ms in s10 → revert hero imagery to CSS-only
- **rollout gates:** `tsc --noEmit` clean; `cargo xtask ci` unaffected (frontend-only);
  wizard + attest + gate flows re-checked in-browser
- **rollback trigger:** any verdict panel rendering `decisions_checked: 0` in a
  non-amber color → immediate revert
- **residual risk accepted by:** Juno (identity), Iris (image provenance), Rafe
  (two-theme burden), Maren (light-theme rot), Kai (photographs at all)

## Follow-ups
- [ ] jdetle — re-read rights for `afcwip001704` when the LoC item API is not rate-limited;
      restore the punched-card photograph only if it reads as no-known-restrictions.
- [ ] jdetle — if LoC imagery ever moves above the fold, convert to `next/image` with
      AVIF/WebP and explicit sizes.