● Zion Boggan zionboggan.com ↗

Remove internal dev and QA scratch from public surface

60917fc   Zion Boggan committed on Jun 27, 2026 (1 month ago)
.gitignore +2 -0
@@ -30,3 +30,5 @@ data/cache/
.exectest.sh
.vscode/
.idea/
+
+_ops/
_ops/accent-options.html +0 -45
@@ -1,45 +0,0 @@
-<!doctype html><html><head><meta charset="utf-8">
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;600;800&family=Atkinson+Hyperlegible:wght@400;700&display=swap" rel="stylesheet">
-<style>
- * { box-sizing: border-box; }
- body { margin:0; background:#ECEAE5; font-family:'Atkinson Hyperlegible',system-ui,sans-serif; color:#1A1A1A; padding:28px; }
- h1.board { font-family:'Public Sans'; font-weight:800; font-size:26px; margin:0 0 6px; }
- p.boardsub { font-size:17px; color:#44423D; margin:0 0 24px; }
- .grid { display:grid; grid-template-columns:repeat(2,1fr); gap:22px; }
- .card { background:#FFFFFF; border:2px solid #CFCBC2; border-radius:10px; padding:22px; }
- .name { font-family:'Public Sans'; font-weight:800; font-size:15px; letter-spacing:.04em; text-transform:uppercase; }
- .hex { font-size:13px; color:#6b6862; margin:2px 0 16px; }
- .eyebrow { font-family:'Public Sans'; font-weight:600; font-size:12px; letter-spacing:.12em; text-transform:uppercase; }
- .h { font-family:'Public Sans'; font-weight:800; font-size:28px; line-height:1.05; margin:6px 0 10px; }
- .lead { font-size:18px; line-height:1.5; margin:0 0 18px; }
- .btn { display:inline-block; font-family:'Public Sans'; font-weight:700; font-size:18px; padding:14px 22px; border-radius:8px; color:#1A1A1A; }
- .link { font-size:18px; font-weight:700; text-decoration:underline; text-underline-offset:3px; display:inline-block; margin-top:16px; }
- .pill { display:inline-block; font-family:'Public Sans'; font-weight:700; font-size:14px; padding:8px 14px; border-radius:999px; margin-top:16px; }
- .swatch { width:34px; height:34px; border-radius:7px; display:inline-block; vertical-align:middle; margin-right:8px; border:1px solid rgba(0,0,0,.15);}
-</style></head>
-<body>
-<h1 class="board">Docket - accent options (large-print look, white background)</h1>
-<p class="boardsub">Same neutral layout, five accents. Each shows a heading, a primary button, a link, and an "active page" pill.</p>
-<div class="grid" id="grid"></div>
-<script>
-const opts = [
- {name:'1 · Brass / Amber', t:'#8A6A12', s:'#D8A93A', w:'#FBF3DF', hex:'text #8A6A12 · fill #D8A93A'},
- {name:'2 · Deep Teal', t:'#0E6E63', s:'#14857A', w:'#E1F2EF', hex:'text #0E6E63 · fill #14857A'},
- {name:'3 · Forest Green', t:'#2E6B3E', s:'#3A8A4E', w:'#E6F2E8', hex:'text #2E6B3E · fill #3A8A4E'},
- {name:'4 · Bronze / Copper',t:'#94531F', s:'#C0712E', w:'#F8ECE1', hex:'text #94531F · fill #C0712E'},
- {name:'5 · Ink only (minimal)',t:'#1A1A1A', s:'#1A1A1A', w:'#EEEDE9', hex:'near-black, accent only on the button'},
-];
-document.getElementById('grid').innerHTML = opts.map(o=>`
- <div class="card">
- <div class="name"><span class="swatch" style="background:${o.s}"></span>${o.name}</div>
- <div class="hex">${o.hex}</div>
- <div class="eyebrow" style="color:${o.t}">ROLL-CALL REGISTER</div>
- <div class="h">Votes, on the record</div>
- <div class="lead">Each row is a recorded vote in the House or Senate - what it was about, the tally, and a link.</div>
- <a class="btn" style="background:${o.s}">Search the record &rarr;</a>
- <br><a class="link" style="color:${o.t}">H.R. 22 - view this bill</a>
- <br><span class="pill" style="background:${o.w};color:${o.t}">Bills (active)</span>
- </div>`).join('');
-</script>
-</body></html>
_ops/diag.py +0 -22
@@ -1,22 +0,0 @@
-import sqlite3, os
-DB = os.environ.get("DOCKET_DB_PATH", "/opt/docket/data/docket.db")
-c = sqlite3.connect(DB, timeout=30)
-print("== DB ==")
-pc = c.execute("pragma page_count").fetchone()[0]
-ps = c.execute("pragma page_size").fetchone()[0]
-print("size_mb:", round(pc * ps / 1e6, 1), "journal_mode:", c.execute("pragma journal_mode").fetchone()[0])
-print("wal_autocheckpoint:", c.execute("pragma wal_autocheckpoint").fetchone()[0])
-print("== #11 districts ==")
-print("district top:", c.execute("select district,count(*) cc from members group by district order by cc desc limit 8").fetchall())
-cur = c.execute("select name,state,district,chamber from members where chamber='house' limit 5").fetchall()
-print("house sample:", cur)
-print("== #2 bill text/summary availability ==")
-cols = [r[1] for r in c.execute("pragma table_info(bills)")]
-print("bills has summary col:", "summary" in cols, "| cols:", cols)
-print("bill_versions count:", c.execute("select count(*) from bill_versions").fetchone()[0])
-print("== #5 vote source urls (sample) ==")
-for (u,) in c.execute("select source_url from votes where source_url is not null order by id desc limit 4"):
- print("VOTEURL:", u)
-print("== row counts ==")
-for t in ("votes","vote_positions","bills","members","awards"):
- print(t, c.execute("select count(*) from " + t).fetchone()[0])
_ops/optimize_db.py +0 -10
@@ -1,10 +0,0 @@
-import sqlite3, os
-DB = os.environ.get("DOCKET_DB_PATH", "/opt/docket/data/docket.db")
-c = sqlite3.connect(DB, timeout=60)
-print("checkpoint:", c.execute("pragma wal_checkpoint(TRUNCATE)").fetchone())
-c.execute("pragma optimize")
-c.execute("analyze")
-print("optimized + analyzed")
-pc = c.execute("pragma page_count").fetchone()[0]
-ps = c.execute("pragma page_size").fetchone()[0]
-print("db size mb:", round(pc * ps / 1e6, 1))
_ops/qa-crawl.js +0 -75
@@ -1,75 +0,0 @@
-// Docket mobile QA crawl - drives chromium at iPhone viewport across every
-// route, screenshots, and auto-detects horizontal overflow + console errors.
-const { chromium } = require("playwright");
-const fs = require("fs");
-
-const BASE = process.env.QA_BASE || "http://localhost:3000";
-const OUT = "/opt/docket/_ops/qa";
-fs.mkdirSync(OUT, { recursive: true });
-
-const routes = [
- "/", "/register", "/votes", "/members", "/bills", "/laws", "/spending",
- "/executive", "/states", "/schedule", "/calendar", "/search",
- "/how-we-pick", "/who-pays",
- "/votes/6017", "/votes/6002", "/members/A000055", "/bill/2136",
- "/bill/1", "/states/CA",
-];
-
-(async () => {
- const browser = await chromium.launch();
- const ctx = await browser.newContext({
- viewport: { width: 390, height: 844 },
- deviceScaleFactor: 2,
- isMobile: true,
- hasTouch: true,
- userAgent:
- "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
- });
- const report = [];
- for (const r of routes) {
- const page = await ctx.newPage();
- const errors = [];
- page.on("console", (m) => { if (m.type() === "error") errors.push(m.text().slice(0, 160)); });
- page.on("pageerror", (e) => errors.push("PAGEERR " + e.message.slice(0, 160)));
- let status = 0;
- try {
- const resp = await page.goto(BASE + r, { waitUntil: "networkidle", timeout: 35000 });
- status = resp ? resp.status() : 0;
- } catch (e) { errors.push("GOTO " + e.message.slice(0, 120)); }
- await page.waitForTimeout(700);
- const m = await page.evaluate(() => {
- const vw = window.innerWidth;
- const sw = document.documentElement.scrollWidth;
- const over = [];
- document.querySelectorAll("body *").forEach((el) => {
- const rb = el.getBoundingClientRect();
- if (rb.right > vw + 2 && rb.width > 40 && el.children.length < 8) {
- const cls = (typeof el.className === "string" ? el.className : "").slice(0, 34);
- over.push(el.tagName + (cls ? "." + cls : "") + " →" + Math.round(rb.right));
- }
- });
- // tap-target check: interactive els < 40px tall
- const small = [];
- document.querySelectorAll("a,button,[role=button],summary").forEach((el) => {
- const rb = el.getBoundingClientRect();
- if (rb.height > 0 && rb.height < 40 && rb.width > 0) small.push((el.textContent || "").trim().slice(0, 18));
- });
- return { vw, sw, overflow: sw > vw + 2, offenders: [...new Set(over)].slice(0, 8), smallTargets: [...new Set(small)].slice(0, 8) };
- });
- const name = (r === "/" ? "home" : r.replace(/^\//, "").replace(/\//g, "_"));
- await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: false });
- report.push({ route: r, status, ...m, errors: errors.slice(0, 5) });
- await page.close();
- }
- await browser.close();
- fs.writeFileSync(`${OUT}/report.json`, JSON.stringify(report, null, 2));
- console.log("==== DOCKET MOBILE QA (390px) ====");
- for (const x of report) {
- console.log(
- `[${x.status}] ovf=${x.overflow ? "YES(" + x.sw + ")" : "no"} ${x.route}` +
- (x.offenders.length ? `\n OVERFLOW: ${x.offenders.join(" | ")}` : "") +
- (x.smallTargets.length ? `\n SMALL-TAP: ${x.smallTargets.join(", ")}` : "") +
- (x.errors.length ? `\n ERRORS: ${x.errors.join(" ;; ")}` : "")
- );
- }
-})();
_ops/qa-view/REF-treetrace-desktop.png +0 -0
Binary file not shown
_ops/qa-view/REF-zionboggan-desktop.png +0 -0
Binary file not shown
_ops/qa-view/accent-options.png +0 -0
Binary file not shown
_ops/qa-view/calendar.png +0 -0
Binary file not shown
_ops/qa-view/home-landing-r3.png +0 -0
Binary file not shown
_ops/qa-view/members.png +0 -0
Binary file not shown
_ops/qa-view/members_A000055.png +0 -0
Binary file not shown
_ops/qa-view/members_r2.png +0 -0
Binary file not shown
_ops/qa-view/register.png +0 -0
Binary file not shown
_ops/qa-view/states.png +0 -0
Binary file not shown
_ops/qa-view/v2-bill-desktop.png +0 -0
Binary file not shown
_ops/qa-view/v2-bill-mobile.png +0 -0
Binary file not shown
_ops/qa-view/v2-bills-desktop.png +0 -0
Binary file not shown
_ops/qa-view/v2-bills-mobile.png +0 -0
Binary file not shown
_ops/qa-view/v2-home-desktop.png +0 -0
Binary file not shown
_ops/qa-view/v2-home-mobile.png +0 -0
Binary file not shown
_ops/qa-view/v3-bill-desktop.png +0 -0
Binary file not shown
_ops/qa-view/v3-bill-mobile.png +0 -0
Binary file not shown
_ops/qa-view/v3-bills-desktop.png +0 -0
Binary file not shown
_ops/qa-view/v3-bills-mobile.png +0 -0
Binary file not shown
_ops/qa-view/v3-home-desktop.png +0 -0
Binary file not shown
_ops/qa-view/v3-home-mobile.png +0 -0
Binary file not shown
_ops/qa-view/v3b-home-dark.png +0 -0
Binary file not shown
_ops/qa-view/v3b-home-light.png +0 -0
Binary file not shown
_ops/qa-view/vote-detail-r3.png +0 -0
Binary file not shown
_ops/qa-view/votes-with-bill-r3.png +0 -0
Binary file not shown
_ops/qa-view/votes.png +0 -0
Binary file not shown
_ops/qa/members_A000055.png +0 -0
Binary file not shown
_ops/rebuild-qa.sh +0 -34
@@ -1,34 +0,0 @@
-#!/bin/bash
-# Build + verify helper for Docket frontend work.
-# Syncs the share source -> /opt build dir, rebuilds, restarts the prod server,
-# runs the mobile QA crawl, and prints the overflow/hydration summary.
-# Run ON CT247: bash /shared/projects/docket/_ops/rebuild-qa.sh
-set -e
-D=/shared/projects/docket
-O=/opt/docket
-# sync frontend source share -> /opt (data, node_modules, .next stay local)
-tar -C "$D" --exclude=node_modules --exclude=.git --exclude=.next --exclude=data --exclude=_ops/qa --exclude=_ops/logs -cf - \
- app components lib public next.config.js tailwind.config.ts tsconfig.json postcss.config.js middleware.ts 2>/dev/null \
- | tar -C "$O" -xf -
-cd "$O"
-echo "== building =="
-npm run build 2>&1 | tail -6
-echo "== restart =="
-for pid in $(ss -ltnp 2>/dev/null | grep :3000 | grep -oE 'pid=[0-9]+' | cut -d= -f2); do kill -9 "$pid"; done
-sleep 2
-DOCKET_DB_PATH="$O/data/docket.db" nohup ./node_modules/.bin/next start -H 0.0.0.0 -p 3000 > "$O/_ops/logs/server.log" 2>&1 < /dev/null &
-sleep 8
-echo "== crawl =="
-node "$O/_ops/qa-crawl.js" > "$O/_ops/qa/crawl.log" 2>&1 || true
-python3 - <<'PY'
-import json
-d = json.load(open("/opt/docket/_ops/qa/report.json"))
-bad = [x for x in d if x.get("overflow") or any("418" in e for e in x.get("errors", []))]
-if not bad:
- print("QA CLEAN - no overflow, no hydration errors across", len(d), "routes")
-else:
- for x in bad:
- print("ISSUE", x["route"], "overflow" if x.get("overflow") else "",
- "#418" if any("418" in e for e in x.get("errors", [])) else "",
- "| offenders:", " ".join(x.get("offenders", [])[:4]))
-PY
_ops/screenshot-v2.js +0 -73
@@ -1,73 +0,0 @@
-// Docket v2 design screenshots - takes home + bills + bill-detail
-// at 390px (mobile) and 1024px (desktop), saves to qa-view/ as v2-*.png
-const { chromium } = require("/opt/docket/node_modules/playwright");
-const fs = require("fs");
-const path = require("path");
-
-const BASE = process.env.QA_BASE || "http://localhost:3000";
-const OUT = "/shared/projects/docket/_ops/qa-view";
-fs.mkdirSync(OUT, { recursive: true });
-
-// Find a real bill ID to screenshot - look for any real URL from the /bills page
-async function findBillId(page) {
- await page.goto(BASE + "/bills", { waitUntil: "networkidle", timeout: 35000 });
- // Try to find the first bill card link
- const href = await page.$eval(
- 'a[href^="/bill/"]',
- (el) => el.getAttribute("href")
- ).catch(() => null);
- return href || "/bill/2136"; // fallback to known test ID
-}
-
-(async () => {
- const browser = await chromium.launch();
- const viewports = [
- { name: "mobile", width: 390, height: 844, mobile: true, dpr: 2 },
- { name: "desktop", width: 1024, height: 768, mobile: false, dpr: 1 },
- ];
-
- let billPath = "/bill/2136";
-
- for (const vp of viewports) {
- const ctx = await browser.newContext({
- viewport: { width: vp.width, height: vp.height },
- deviceScaleFactor: vp.dpr,
- isMobile: vp.mobile,
- hasTouch: vp.mobile,
- });
- const page = await ctx.newPage();
-
- // Find a real bill for the first viewport pass
- if (vp.name === "mobile") {
- try {
- billPath = await findBillId(page);
- console.log("Bill path:", billPath);
- } catch (e) {
- console.warn("Could not auto-detect bill path, using default");
- }
- }
-
- const shots = [
- { route: "/", name: "v2-home" },
- { route: "/bills", name: "v2-bills" },
- { route: billPath, name: "v2-bill" },
- ];
-
- for (const s of shots) {
- try {
- await page.goto(BASE + s.route, { waitUntil: "networkidle", timeout: 35000 });
- await page.waitForTimeout(1000);
- const outPath = path.join(OUT, `${s.name}-${vp.name}.png`);
- await page.screenshot({ path: outPath, fullPage: false });
- console.log(`OK ${outPath}`);
- } catch (e) {
- console.error(`FAIL ${s.route}: ${e.message}`);
- }
- }
-
- await ctx.close();
- }
-
- await browser.close();
- console.log("\nScreenshots saved to", OUT);
-})();
_ops/screenshot-v3.js +0 -81
@@ -1,81 +0,0 @@
-// Docket V3 design screenshots - captures dark-default theme
-// Home + /bills + bill-detail at 390px (mobile) and 1280px (desktop)
-// Saves to qa-view/ as v3-*.png
-const { chromium } = require("/opt/docket/node_modules/playwright");
-const fs = require("fs");
-const path = require("path");
-
-const BASE = process.env.QA_BASE || "http://localhost:3000";
-const OUT = "/shared/projects/docket/_ops/qa-view";
-fs.mkdirSync(OUT, { recursive: true });
-
-// Find a real bill ID from the /bills page
-async function findBillId(page) {
- await page.goto(BASE + "/bills", { waitUntil: "networkidle", timeout: 35000 });
- const href = await page.$eval(
- 'a[href^="/bill/"]',
- (el) => el.getAttribute("href")
- ).catch(() => null);
- return href || "/bill/2136";
-}
-
-(async () => {
- const browser = await chromium.launch();
- const viewports = [
- { name: "mobile", width: 390, height: 844, mobile: true, dpr: 2 },
- { name: "desktop", width: 1280, height: 900, mobile: false, dpr: 1 },
- ];
-
- let billPath = "/bill/2136";
-
- for (const vp of viewports) {
- const ctx = await browser.newContext({
- viewport: { width: vp.width, height: vp.height },
- deviceScaleFactor: vp.dpr,
- isMobile: vp.mobile,
- hasTouch: vp.mobile,
- // Force dark mode preference so the site stays in dark default
- colorScheme: "dark",
- });
- const page = await ctx.newPage();
-
- // Clear any persisted display-mode so we get the true default (dark)
- await page.addInitScript(() => {
- localStorage.removeItem("docket-display");
- });
-
- // Find a real bill on mobile pass
- if (vp.name === "mobile") {
- try {
- billPath = await findBillId(page);
- console.log("Bill path:", billPath);
- } catch (e) {
- console.warn("Could not auto-detect bill path, using default");
- }
- }
-
- const shots = [
- { route: "/", name: "v3-home" },
- { route: "/bills", name: "v3-bills" },
- { route: billPath, name: "v3-bill" },
- ];
-
- for (const s of shots) {
- try {
- await page.goto(BASE + s.route, { waitUntil: "networkidle", timeout: 35000 });
- // Wait for fonts + transitions
- await page.waitForTimeout(1200);
- const outPath = path.join(OUT, `${s.name}-${vp.name}.png`);
- await page.screenshot({ path: outPath, fullPage: false });
- console.log("OK ", outPath);
- } catch (e) {
- console.error("FAIL", s.route + ":", e.message);
- }
- }
-
- await ctx.close();
- }
-
- await browser.close();
- console.log("\nV3 screenshots saved to", OUT);
-})();
_ops/screenshot-v3b.js +0 -49
@@ -1,49 +0,0 @@
-// Docket V3b screenshots - dark + light mode, home page, header switcher visible.
-// Outputs: v3b-home-dark.png and v3b-home-light.png to qa-view/
-const { chromium } = require("/opt/docket/node_modules/playwright");
-const fs = require("fs");
-const path = require("path");
-
-const BASE = process.env.QA_BASE || "http://localhost:3000";
-const OUT = "/shared/projects/docket/_ops/qa-view";
-fs.mkdirSync(OUT, { recursive: true });
-
-(async () => {
- const browser = await chromium.launch();
-
- const modes = [
- { key: "dark", storage: "dark", colorScheme: "dark" },
- { key: "light", storage: "light", colorScheme: "light" },
- ];
-
- for (const m of modes) {
- const ctx = await browser.newContext({
- viewport: { width: 1280, height: 900 },
- deviceScaleFactor: 1,
- isMobile: false,
- colorScheme: m.colorScheme,
- });
- const page = await ctx.newPage();
-
- // Set the persisted mode so the pre-paint script and React both see the right value
- await page.addInitScript((mode) => {
- localStorage.setItem("docket-display", mode);
- }, m.storage);
-
- try {
- await page.goto(BASE + "/", { waitUntil: "networkidle", timeout: 35000 });
- // Wait for fonts, transitions, and the icon switcher to mount
- await page.waitForTimeout(1500);
- const outPath = path.join(OUT, `v3b-home-${m.key}.png`);
- await page.screenshot({ path: outPath, fullPage: false });
- console.log("OK ", outPath);
- } catch (e) {
- console.error("FAIL", m.key + ":", e.message);
- }
-
- await ctx.close();
- }
-
- await browser.close();
- console.log("\nV3b screenshots saved to", OUT);
-})();
docs/DESIGN_V3.md +0 -49
@@ -1,49 +0,0 @@
-# Docket - Design V3: MATCH ZION'S LIVE SITES (LOCKED 2026-06-27)
-
-The light V2 (Atkinson, then Archivo/Fraunces light + serif body) is REJECTED - it reads as a generic AI look. Replicate the theme of Zion's actual live sites. DO NOT invent or use any generated/AI design pack.
-
-## GROUND TRUTH (study these, replicate them)
-- Reference screenshots (captured from the live sites): `_ops/qa-view/REF-treetrace-desktop.png`, `_ops/qa-view/REF-zionboggan-desktop.png`.
-- Their live CSS: treetrace.dev `/assets/css/{tokens,shell,glass}.css`; zionboggan.com `/assets/css/{main,flare,sections}.css`.
-- What both share (the aesthetic to match):
- - **DARK** near-black backgrounds (ink ~#0B0C0E, raised surface ~#121316/#1A1C1F).
- - **Archivo 800-900, UPPERCASE, big, tight tracking** for display headlines (e.g. "RECORD THE PATH", "ZION BOGGAN").
- - **JetBrains Mono, uppercase, tracked** for kicker labels, nav, tags, data, terminal cards.
- - A **serif** (Fraunces) for paragraph/body prose (warm, readable) - verify against their CSS and match their body role.
- - **ONE bold accent per view** (treetrace cobalt #2E5BFF, zionboggan red ~#A40000) - used sparingly (buttons, kickers, highlights), bordered/outline buttons.
- - Composition: small mono kicker → HUGE Archivo headline → serif paragraph → outline buttons + tag row. Generous whitespace; it BREATHES (do not crowd).
-
-## Docket V3 tokens (his treetrace dark treatment + NEUTRAL accent)
-```
---bg: #0B0C0E /* ink */
---surface: #121316 /* cards/chrome */
---raised: #1A1C20 /* hover/raised */
---text: #F2EFE9 /* warm paper */
---text-soft: #9BA0A6 /* ash secondary */
---border: rgba(155,160,166,.18)
---accent: #E0A53B /* BRASS - neutral (cobalt/red are partisan-coded for a gov site); rationed, one signal per view */
---accent-dim:#B07F25
---accent-wash:rgba(224,165,59,.10)
---yea: #7FA88C /* muted sage, labeled */ --nay: #B98A8A /* muted clay, labeled */
-```
-Keep Light + Plain as optional toggles; **DEFAULT = this dark theme** (his look).
-
-## Fonts (his trio - match his ROLES exactly)
-- Archivo (display, 800-900, uppercase headlines/labels). JetBrains Mono (kickers, nav, tags, data tokens - readable size >=14px, NOT tiny). Fraunces (serif paragraph/body prose, comfortable >=17px). Drop Atkinson/Public-Sans entirely.
-
-## FIX what was wrong (within the dark aesthetic)
-1. **Crowding** → generous whitespace + clear vertical rhythm; fewer elements per screen; let headlines breathe like his sites do.
-2. **Legibility/size** → bigger than his dense portfolio: body >=17px, kicker/labels >=14px (not 11-12px), big tap targets >=44px.
-3. **Navigation** → simple 6-item nav (Home · Bills · Votes · Representatives · Spending · States) + prominent Search; clear active state; clean mobile menu. Breadcrumbs + big Back.
-4. **Responsive** → big simple cards (NOT dense tables); zero overflow at 390px (keep the responsive-card work).
-5. **Plain language** → plain dates/words, expand acronyms.
-
-## Keep (don't lose)
-All functional work: perf/ISR (no force-dynamic), search+sort+filters, Cite this, vote→bill display, state data, photos, neutrality gate (no adjectives, source on every fact, no comments, muted/never-saturated party colors). The federal-vs-state bill fix.
-
-## Build order
-1. Read his live CSS + the REF screenshots. Set the dark theme tokens + font roles to match.
-2. Rebuild HOME + /bills + /bill/[id] as the TEMPLATE in his theme (dark, Archivo-uppercase, mono labels, brass, breathing whitespace, big text, simple nav).
-3. STOP - screenshot (dark default) → overseer shows Zion for approval.
-4. After approval: propagate to all pages + the federal/state bill distinction.
-5. QA crawl CLEAN; purge Cloudflare cache so it goes live.
docs/REDESIGN.md +0 -43
@@ -1,43 +0,0 @@
-# Docket - UX/QoL Redesign Spec (2026-06-27)
-
-"Back to the drawing board" for usability. Audience = an **elderly citizen** (low tech-literacy, low vision) AND a **high-school/college student citing Docket as an educational source**. Decisions below are LOCKED by Zion. Keep strict neutrality (no lean, no adjectives) and keep all data/sourcing.
-
-## Decisions (locked)
-1. **Default look = legible/light "Reference" theme.** The dark "forensic terminal" becomes an OPTIONAL mode, not the default.
-2. **"Cite this" = first-class feature** (APA/MLA/Chicago + permalink + accessed-date) on every record + page.
-3. Search/filter on every list page. De-clutter IA. Clear labels. Performance already fixed.
-
-## Theme system (3 modes; default changes)
-Mode toggle stays, but the DEFAULT becomes **Reference**:
-- **Reference (NEW DEFAULT - light):** background `#F8FAFC`, surface `#FFFFFF`, text `#0F172A`/`#020617`, muted `#475569`, border `#E2E8F0`. **Accent = brass `#B07F25`** (the brand thread; brass darkened for AA on light - NEVER use blue/red as accent, neutrality). Body font **Atkinson Hyperlegible** (engineered for low vision). Headings **Fraunces** (keep, scholarly/citable). Mono **JetBrains Mono ONLY for true data tokens** (vote tallies, bill numbers, dates) - not for prose. Body ≥18px, line-height 1.6, generous whitespace, single-column, max-w ~720-860px for reading.
-- **Dark (forensic - now optional):** the existing dark brand (ink/brass/mono). Preserved for power users.
-- **Plain (high-contrast):** existing max-contrast mode (system font, ≥20px) for max accessibility.
-All three: WCAG AA min (AAA target), ≥44px tap targets, visible focus rings, underlined links, no hover-only, prefers-reduced-motion respected.
-
-## IA / declutter (each page does ONE job)
-- **Register (home):** the ranked feed, period. Remove the crowding side cards (composition chart, awards card, topic chips, changes feed) from the main view - move them to a compact, collapsed "More" area or their own pages. Lead with a short plain-language explainer + the feed. One primary action.
-- **Every list page (Register, Bills, Votes, Members, Spending, Executive, States):** a prominent labeled **search/filter bar at the top** (mobile + desktop) - search box ≥44px tall, plus the relevant dropdown filters. Results as the existing responsive cards.
-- **Detail pages (bill, vote, member):** clear heading → key facts → source → "Cite this". No crowding.
-- Consistent header on every page: wordmark, a clear page-nav (hamburger on mobile / simple bar on desktop) with active-page indicator, the mode toggle as a small distinct control, and a global search entry.
-
-## Search/filter (item #1)
-- **/members:** search by name; filter by state, chamber, party, current-vs-historical. (Already defaults current-first.)
-- **/bills:** keep existing filters + ensure the search box is prominent.
-- **/votes:** add search/filter (by chamber, result, date/keyword).
-- Reuse a shared SearchFilterBar component. Server-side filtering where possible (perf), client-side for already-loaded sets.
-
-## "Cite this" (item #2 - first-class)
-- A `CiteThis` component on every record (bill, vote, member, award) and every page.
-- Generates a ready citation in **APA, MLA, and Chicago**, plus a **permalink** and the **accessed date** (today). Example (APA): *Docket. (2026). H.R. 22 - [title] [Data set]. U.S. Congress. Retrieved Month D, YYYY, from https://usdocket.org/bill/...* - but ALWAYS point the underlying source to the official .gov record too (neutrality rule #1).
-- Copy-to-clipboard buttons. Neutral, factual, no adjectives.
-- This is the differentiator for the student/educator audience.
-
-## Labels / clarity (the "51" problem)
-- **States page:** show **"50 states"** as the count. **DC and the 5 territories are their own clearly-labeled rows/section - DC is a federal district, NOT a state; never produce a "51 states" number.** Coverage framed plainly.
-- Plain dates ("June 26, 2026"), plain status words, expand acronyms on first use, ≤8th-grade reading level for any explanatory copy (docs/ACCESSIBILITY.md).
-
-## Out of scope / keep
-- Keep all data, sources, neutrality gate, the 3-mode toggle mechanism. Keep the brass brand thread. Don't regress the responsive cards or the perf fixes (PERF_NOTES.md).
-
-## Verify
-Build + run the mobile QA crawl (`_ops/rebuild-qa.sh`) → must stay QA CLEAN (no overflow/hydration). Then check the new default renders light + legible, search works on list pages, Cite this works, states labeled correctly. Screenshots at 390px + desktop.