diff --git a/.gitignore b/.gitignore
index 4fdd2f9f..891972d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,7 @@ playwright-report/
# anyone running a fork. Keep them local; put anything durable in README.md,
# CONTRIBUTING.md, or docs/ops.md instead.
docs/superpowers/
+
+# Design-sweep screenshots: regenerable from docs/design-sweep-2/capture.spec.ts.txt
+# and several megabytes of PNG. The prose reports beside them are tracked.
+docs/design-sweep-2/shots/
diff --git a/AGENTS.md b/AGENTS.md
index d9e5201c..2a9f9592 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -45,3 +45,30 @@ until it comes up green. The rationale is written next to the setting.
`docs/e2e-flake-triage.md` has the full triage workflow and lists the tests
currently exposed to the `useSubmitGuard` latch defect.
+## A test written to prove a fix must be shown to fail without it
+
+A passing new test is not evidence that it tests anything. Assertions written
+alongside a fix pass for two indistinguishable reasons — the fix works, or the
+assertion cannot fail — and a green run reports both identically. This is not a
+rare slip: six assertions written during one sweep of this repo turned out to be
+vacuous, each caught only by this check and none by review.
+
+So, before a test that exists to prove a fix is committed: **take the fix out,
+leave the test in, and watch it fail.** Then put the fix back and watch it pass.
+If the failure message does not describe the defect you set out to fix, the test
+is measuring something else.
+
+The instrument matters as much as the discipline. Reverting *everything*
+uncommitted is not a control when the test is part of the same uncommitted
+change — stashing removes the test alongside the fix, nothing runs, and the
+green result is read as confirmation. Revert only the code under test, by hand,
+and leave the assertion standing.
+
+The failures this catches are the quiet kind. `toBeVisible()` against a
+`.visually-hidden` element passes either way, because a 1px clipped box still
+has a bounding box. A row-count assertion on a table whose empty state is itself
+one `
` passes whether or not the filter ran. A colour assertion parsing
+`getComputedStyle` output passes on garbage, because these tokens serialize as
+`oklch(...)` and a naive parse reads lightness as red without erroring. None of
+those announce themselves; all of them fail loudly the moment the fix is removed.
+
diff --git a/DESIGN.md b/DESIGN.md
index 6b32aa8e..69c4241c 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -58,7 +58,7 @@ lighter structure token carries a little more, up to `--rule-strong`. Do not "re
a tint to `--void` for consistency with the rest of the ramp: the ramp *is* the rule.
Every text token below was measured against all three grounds rather than asserted;
-the worst case is `--ink-faint` at 4.63:1 on a hovered row, and nothing sits under
+the worst case is `--ink-faint` at 4.61:1 on a hovered row, and nothing sits under
4.5:1. Two of these values exist *because* of that measurement rather than taste:
`--signal-bad` is `0.66` because `0.64` measured 4.42 against `--hull-hi` (under the
floor exactly when the pointer is on the row), and `--rule-strong` holds its lightness
@@ -84,7 +84,7 @@ OKLCH is the right space to *tune* in and the wrong one to *judge* in.
| `--hull` | `oklch(0.195 0.001 56)` | Panels, table headers, inset regions. |
| `--hull-hi` | `oklch(0.245 0.002 54)` | Row hover, raised controls, pressed states. |
| `--rule` | `oklch(0.33 0.004 55)` | Hairlines. The primary structural device. |
-| `--rule-strong` | `oklch(0.56 0.008 58)` | Section boundaries, control borders. Lightness is set by WCAG 1.4.11: it measures 4.24 / 3.92 / 3.48 against `--void`, `--hull`, and `--hull-hi`, so a control's edge is identifiable on every ground it can sit on. |
+| `--rule-strong` | `oklch(0.56 0.008 58)` | Section boundaries, control borders. Lightness is set by WCAG 1.4.11: it measures 4.23 / 3.90 / 3.47 against `--void`, `--hull`, and `--hull-hi`, so a control's edge is identifiable on every ground it can sit on. |
### Ink
@@ -102,7 +102,7 @@ Gold is identity and emphasis. The three signals are semantic and never decorati
|---|---|---|
| `--gold` | `oklch(0.83 0.155 88)` | Brand mark, active nav, primary action, Member tier. |
| `--gold-dim` | `oklch(0.72 0.14 84)` | Gold borders, hover on gold surfaces. |
-| `--signal-ok` | `oklch(0.74 0.09 150)` | Reserved for where health is genuinely the subject (`.notice--ok`). **Not** the default `ok` status token, which takes `--ink-dim` — see "Status token" below. |
+| `--signal-ok` | `oklch(0.74 0.09 150)` | **Declared, documented, and used by nothing.** It was meant for where health is genuinely the subject, and the class this row used to name as that place — `.notice--ok` — has never existed. Kept rather than deleted because the notice family (`--warn`, `--bad`) has an obvious gap where it would sit, and because `--tier-alumni` is the same value: deleting the token would leave the next `ok` notice reaching for the alumni tier's colour, which is worse than an unused declaration. **Not** the default `ok` status token, which takes `--ink-dim` — see "Status token" below. |
| `--signal-warn` | `oklch(0.80 0.15 50)` | Needs re-auth, scope shortfall, cryo (admin table only — an admin scans for it; the member's own account page reads cryo in `--ink-dim` instead, since it's a pause the member asked for, not a fault). Hue 50 rather than the 70 this started at: at 70 the warn signal sat 18° from `--gold`/`--tier-member` at near-identical chroma and lightness — 0.057 apart in OKLab, not enough to tell a gold Member badge from an amber CRYO token two columns away in the same mono uppercase. 50 nearly doubles that to 0.104 while holding 0.146 from `--signal-bad`. Warn remains nearer to gold than to bad, deliberately: equalising the two means rotating to about hue 37, buying separation from identity by spending it on failure, and mistaking a warning for an error is the worse confusion. The bar is legibility against gold, not equidistance. |
| `--signal-bad` | `oklch(0.66 0.19 26)` | Dead token, failed sync, destructive action. |
@@ -128,8 +128,23 @@ a real tier's colour.
- Colour is never the only carrier of meaning.
- Saturated colour occupies well under 10% of any screen. The gold in particular is
- rationed: one primary action per view, plus the mark.
-- No gradients on text, ever. No decorative gradients at all.
+ rationed: one primary action per view, plus the mark, plus the active nav item
+ and the Member tier badge where those appear — the four uses the `--gold` row
+ above lists, not the two this line used to name. The short version selected for
+ the wrong repair: a reader auditing a screen against "one action plus the mark"
+ finds gold on the nav and on a tier badge and has to decide which of the two is
+ the mistake, when neither is. The focus ring is gold as well and is not part of
+ the ration at all — WCAG 2.4.7 says it appears wherever focus lands, and a
+ budget is not a thing an indicator can be held to.
+- No gradients on text, ever, and none used as decoration. Two `linear-gradient`s
+ exist and both are load-bearing rather than ornamental: the scroller edge fades
+ (`.scroller-fade--start` / `--end`), which are the only rest-state cue that a
+ table has more to the side, and `.status__dot`'s neutral bar, which is a
+ gradient of one colour against itself — a flat fill, used because painting the
+ 0.15em bar into a constant-height box keeps every tone's geometry identical.
+ This line used to read "no decorative gradients at all", which is the intent
+ and reads as a ban; someone enforcing it literally would delete a scroll
+ affordance and misalign a status baseline.
## Typography
@@ -235,8 +250,8 @@ you what it holds.
below in a single measured column; admin tables are allowed to run wider.
- **Nav membership is keyed to the viewer, not the section.** The bar offers every
destination this viewer is *provably authorized* to reach — `Your account` always,
- `Operations` when they can read payouts, `Members`/`Audit log`/`Sync` when they are an
- admin — in one fixed order, broadest access first. Membership does not change as you
+ `Operations` when they can read payouts, `Members`/`Audit log`/`Sync`/`Access lists`
+ when they are an admin — in one fixed order, broadest access first. Membership does not change as you
walk between sections, so no destination is ever two hops away from a viewer who is
entitled to it in one. Note that `isAdmin` and `tier` are independent, so an admin is
not automatically a payouts reader and `Operations` is never unconditional in the admin
@@ -307,6 +322,14 @@ you what it holds.
- Transitions are 140ms on colour and border, 220ms on transforms, both on
`cubic-bezier(0.22, 1, 0.36, 1)` (ease-out-quint). No bounce, no elastic, no spring.
+ The 220ms half of that is a reservation rather than a description. `--dur-move`
+ has no consumers: all ten `transition` declarations in `globals.css` animate a
+ colour, a border or an opacity, and every one of them takes `--dur-color`. The
+ transforms that do exist are `@keyframes` — `seal-settle` at 620ms,
+ `btn-pulse` and `link-pending-pulse` at 900ms — and each names its own duration
+ rather than reading the scale, because a loop and a settle are not the same
+ kind of thing as a hover. Worth knowing before adding a transform transition
+ and finding the token has never been exercised.
- Layout properties are never animated. Transform and opacity only.
- The only entrance animation in the system is the login seal, and it is a single
opacity-and-scale settle.
@@ -346,7 +369,7 @@ configuration, the other two by overwriting the file.
- Disabled controls keep `opacity: 1` and take an explicit `--ink-faint`, rather than
fading. An opacity fade moves with whatever ground it lands on: at 65% the disabled
text measured 3.24:1 on `--void` but 2.88:1 on a hovered admin table row, under the
- 3:1 floor exactly when the pointer is on the row. The explicit colour is 4.85:1 on
+ 3:1 floor exactly when the pointer is on the row. The explicit colour is 4.61:1 on
`--hull-hi` and does not move. **Do not "simplify" this back to an opacity.**
- Hit targets: `36px` for standalone controls (`.btn`), `28px` for the in-row controls
of the admin tables (`.btn--micro`, `.btn--quiet`, `.row-toggle`). Both clear the
diff --git a/PRODUCT.md b/PRODUCT.md
index ccb0aca1..7d88eceb 100644
--- a/PRODUCT.md
+++ b/PRODUCT.md
@@ -95,10 +95,19 @@ what is true. Never exclaims. Never apologizes twice.
ordinary states, not failures. Reserve alarm colour for things the user can and
should fix.
5. **Earn the artwork.** Faoble's illustrations are shown properly or not at all:
- full quality, from an asset cut for the size it is drawn at, never a large file
- scaled down or a fragment cropped out for decoration. The login ground is the
- deliberate exception, where the lander is held far enough back to be texture
- rather than picture.
+ full quality, from a master with pixels to spare, never a fragment cropped out
+ for decoration and never an asset stretched past the size it was cut for. The
+ login ground is the deliberate exception, where the lander is held far enough
+ back to be texture rather than picture.
+
+ This principle used to say "never a large file scaled down", which the app has
+ never obeyed and should not: `hero-account.webp` is a 1120px master rendered at
+ 420px, or at 260px when `.closing--compact` applies on a one-character account.
+ Rendering a high-resolution master smaller is what protects the artwork —
+ oversampling is why it stays crisp on a 2x display. What degrades it is the
+ opposite direction, and that is what the rule is for. Stated as a ban on
+ downscaling, the principle condemned the correct implementation and left the
+ actual failure mode unnamed.
## Accessibility & Inclusion
diff --git a/docs/design-sweep-2/BRIEF.md b/docs/design-sweep-2/BRIEF.md
new file mode 100644
index 00000000..ceb88602
--- /dev/null
+++ b/docs/design-sweep-2/BRIEF.md
@@ -0,0 +1,352 @@
+# Design sweep 2 — dispatch brief
+
+Worktree: `.claude/worktrees/design-sweep-2026-08-10`. Base: `main` @ `a9777e7`.
+
+Screenshots for every surface are in `docs/design-sweep-2/shots/`, captured
+2026-08-09 at 1440×900 (`.wide.png`) and 390×844 (`.narrow.png`), both
+`fullPage`, against a seeded realistic fixture (34 payout operations, 62 audit
+rows, 9 job types × 5 runs, a 16-participant roster, 12 members across four
+tiers) plus the empty and error states.
+
+---
+
+## Preamble — every reviewer gets this verbatim
+
+**Look at your surface's screenshot before you open a file.** Say what you see
+before you explain it. Then read the target and everything it imports, and trace
+at least one full interaction path. Do not review from structure alone — and do
+not review from source alone, which is the same mistake wearing better clothes.
+Both viewports. The narrow one is not an afterthought; several of these pages
+are read on a phone at 1am.
+
+**You are read-only on source.** The single write you may make is your report,
+at the path named in your block. Do not edit, format, or "fix in passing."
+
+### What this sweep is hunting
+
+Three failure patterns were found on `/account` (the reference surface, already
+worked — see `12-account.*.png` and `src/app/account/page.tsx` for what "done"
+looks like). Look for them specifically, and say plainly if your surface does
+not have them:
+
+1. **Unshaped field.** Content occupying a fraction of a 78rem column with the
+ remainder empty, and the page running long instead of wide.
+2. **Total enumeration.** A value repeated identically on every row of a table
+ when it is really one fact about the whole set. `crewNorms` in
+ `src/app/account/page.tsx` is the pattern for fixing it: measure deviation
+ against the set, state the shared fact once, keep both channels in parity.
+ `/admin/sync`'s "Cadence (UTC)" column header is the same fix in miniature —
+ the shared timezone is said once in the header, stripped from every row's
+ visible text, and restored per-row in a `visually-hidden` span so the
+ accessible name does not lose it.
+3. **Repeated identical controls at uniform weight, where nothing directs the
+ eye.**
+
+Also: **an explanatory subtitle under an H1 is a smell.** A caption explaining
+what a table means usually means the table needs work, not that it needs a
+caption.
+
+### Report format — identical for everyone
+
+Findings worst-first. Each one:
+
+- **Severity** — Critical / Serious / Moderate / Minor.
+- **Where** — a `file:line` *when the finding has one*. `**Where:** whole
+ surface` and `**Where:** across surfaces` are first-class values, not
+ fallbacks. See below.
+- **Cost** — one sentence naming who is harmed and what it costs them. Concrete.
+ "A member checking a stale token at 1am scrolls past four screens of identical
+ rows to find the one that is red" is a cost. "Violates hierarchy" is not.
+- **Fix** — concrete, and scoped to what you would actually change.
+- **Principle** — the violated principle where one exists.
+
+Then: **what is genuinely good and should survive** (name it, so the fix pass
+does not break it), and **what you could not evaluate** and why.
+
+### Four things about that format, each of which is load-bearing
+
+- **`file:line` is optional and I mean it.** The findings that cannot produce
+ one are the ones about what a screen *adds up to*: ten individually-defensible
+ decisions composing a page with no focal point, a table whose every row is
+ defensible and whose aggregate is noise. A format that demands a line number
+ selects against exactly those. If you have no location, give the observation
+ that supports the finding instead.
+- **A missing principle citation is not a defect in the finding.** Do not drop a
+ finding because you cannot cite a rule for it, and do not manufacture a
+ criterion that half-fits — a fabricated criterion gets ranked as if it were
+ real.
+- **State colour in rendered sRGB hex, never in the authoring space.** OKLCH is
+ the right space to tune in and the wrong one to judge in. At near-black, judge
+ the red-to-blue *ratio*, not the absolute difference — `#0c0a08` is a gap of 4
+ and visibly brown. Convert before filing and quote the hex.
+- **Counts and comparisons are claims.** "Sixteen branches", "the only client
+ component", "these two rules are identical" — check them before you state
+ them, and cut the ones that carry no decision.
+
+### Settled — constraints (closed, do not re-open)
+
+These are closed by something outside the design. Proposing to change them wastes
+the finding.
+
+- **Dark only.** EVE's client is dark and its players expect dark. A light ground
+ is not on the table. Literal-paper and paper-panel-on-dark were both built and
+ rejected.
+- **WCAG 2.2 AA** is the floor: 4.5:1 text, 3:1 for large text and UI
+ boundaries, 24px hit targets (2.5.8 AA), 320px reflow, 200% zoom, focus never
+ suppressed, colour never the only carrier.
+- **`--void` is chroma 0** (`#0a0a0a`). Two warm grounds shipped and both read as
+ brown. Do not propose restoring a tint "for consistency with the ramp."
+- **Do not change colour tokens.** Explicit instruction from the owner for this
+ sweep. You may report that a token is *used* wrongly; do not propose retuning
+ its value.
+- **Two hit-target grades, 36px and 28px, and no third.** 28px is scoped by the
+ reason for it — rows that each carry a control set and are read many at a
+ time. A disclosure drawer is not in-row for this purpose and takes 36px.
+- **`.st--ok` is `--ink-dim`, not green.** An `ok` that has to shout is an `ok`
+ competing with the one row that isn't. Do not propose restoring the green.
+- **Disabled controls keep `opacity: 1` and take explicit `--ink-faint`.** An
+ opacity fade measured 2.88:1 on a hovered row. Do not simplify back to opacity.
+- **Nav membership is keyed to the viewer, not the section**, derived once in
+ `src/app/_components/nav-items.ts`. The three boundary surfaces run the same
+ rule on the strongest membership the *path* alone proves.
+- **One column origin.** The page box is `--measure-page` on every route;
+ narrow surfaces cap their *contents*, never the column.
+- **Artwork carries empty `alt`**, including the seal, because it sits directly
+ above an `
` holding the same name.
+- **Migrations are generated, never hand-written**, and an applied one is never
+ edited.
+
+### Settled — taste (decided, and open to challenge ONCE)
+
+These were closed by someone's judgement, not by an external constraint. They are
+decided. If you think one is wrong, say so **in a clearly-marked contested
+section at the end of your report**, with your reasoning. Do not spend the body
+of the report re-litigating them.
+
+- Gold as the single emphasis colour, rationed to one primary action per view
+ plus the mark.
+- `--signal-warn` at hue 50 (moved from 70 for separation from gold).
+- The two-family split: Archivo for prose, IBM Plex Mono for all state. "Prose is
+ proportional, state is monospaced."
+- The tight type ramp below `--t-h2` (1.07–1.09 between steps); size carries
+ little signal down there and face/weight/case/colour carry it instead.
+- **No cards.** Structure is hairline rules and section headers. Exactly two
+ exceptions exist — the login panel and `/payouts/new`'s form panel — and a
+ third would mean the rule has stopped being true.
+- Registration ticks on the login panel only.
+- Near-zero radii (2px controls, 0 rules).
+- Deadpan voice: terse, factual, never exclaims, joke lives in artwork and
+ microcopy and never in the controls.
+
+### Do NOT re-report — closed by the Aug-5 sweep
+
+Verified against `docs/design-sweep/SYNTHESIS.md` and `SECOND-PASS.md` this
+session. If you find one of these still broken, that is a *regression* finding
+and worth filing — but say explicitly that you are re-opening a closed item and
+give the evidence.
+
+Re-auth stale state; dead worker reading healthy; rejected inline edit losing
+input; `/admin/accounts` focus + tier disable; four silent `/account` actions;
+`/payouts` "was I paid?"; alt-name audit search; the "Try again" control;
+three `&&`-mounted `Notice`s; Discord unlink self-disarm; `/login`'s scope-list
+`
` inversion; `/admin/accounts` search; 200% zoom portholes; pinned-column
+focus ring; `/admin/sync` enqueue confirmations and the queued/last-run
+separator; `/admin/audit`'s UUID recital and lost timestamp; `.dim` font-size and
+the orphan `.dim-ink`; typographic drift; nav membership between sections
+(resolved 2026-08-06 by the viewer-keyed rule); the `discord-roles` audit gap;
+the `audit_log` action index; `workerHeartbeat`'s null conflation; both
+add-forms' duplicate-submit hazard; `accountsConfirmation`'s loose signature.
+
+### Known-open — report only if you can add something
+
+These are already on the backlog. Do not spend a finding restating them; do file
+if your surface shows a *consequence* the existing entry does not name.
+
+- The first "mark paid" freezes the operation and the warning renders after the
+ press.
+- Members are shown live draft amounts the service refuses to pay.
+- `/payouts`' future-date guard is client-only.
+- Oversized images with no `sizes`/priority on `/account` and `/login` (the
+ 82 KB seal is the LCP element).
+- `
` prose length on `/account`.
+- `.launch__foot`'s sixth type size; `.escalation`'s 1.00:1 ground; `Tone`'s
+ missing docblock; `RuleHead`'s dead `as="span"` default.
+- Duplicate pagers on `/admin/audit`.
+
+### Domain vocabulary — these are correct, do not propose generic replacements
+
+*Corp*, *alt*, *fleet*, *cryo* (a pause the member asked for, not a fault),
+*derole* (drops tier, keeps account — never a deletion), *tier* (Member /
+Associate / Alumni / Pending), *operation* (one fight, one payout row), *ACL*
+(the Wanderer map access list), *ESI* (EVE's API), *scope* (an OAuth grant),
+*Flight log* / *manifest* / mission-patch furniture. "Center for Kids Who Can't
+Fly Good" is the corp's real joke, told straight.
+
+---
+
+## Surface blocks
+
+Every block gets the preamble above. Register determines which impeccable
+reference you work from.
+
+### 1. `/login` — **register: brand**
+
+- Source: `src/app/login/page.tsx`
+- Shots: `01-login.wide.png`, `01-login.narrow.png`
+- The only unauthenticated surface, and the only one where the artwork is the
+ subject. It carries the seal (the LCP element), the login panel (one of the
+ two sanctioned card exceptions), the registration ticks (used here and nowhere
+ else), and the hero line art held back as texture.
+- Judge it on: does it establish the flight-operations-at-night theme in one
+ screen, and does it read as authored rather than generated? A member's first
+ impression of the whole tool is this page.
+- Report: `docs/design-sweep-2/reports/login-critique.md` /
+ `login-audit.md`
+
+### 2. `/payouts` — **register: product**
+
+- Source: `src/app/payouts/page.tsx`, `src/app/payouts/access.ts`, and the
+ co-located components in `src/app/payouts/`
+- Shots: `04-payouts-full.*` (34 operations — the realistic case), and
+ `03-payouts-empty.*` (the empty state)
+- Judge it on: principle 3, scanning is the primary act. And principle 2, state
+ before action — can a member answer "was I paid?" without pressing anything?
+- This is a prime candidate for patterns 1 and 2. Check the column set against
+ what actually varies row to row.
+- Report: `docs/design-sweep-2/reports/payouts-critique.md` / `payouts-audit.md`
+
+### 3. `/payouts/new` — **register: product**
+
+- Source: `src/app/payouts/new/page.tsx` (58 lines) and
+ `src/app/payouts/new/new-operation-form.tsx`
+- Shots: `05-payouts-new.*`
+- The second of the two sanctioned card exceptions (`.form-panel`). The page is
+ `page--narrow` with one short form on it — pattern 1 is the obvious risk, and
+ the panel exists precisely to answer it. Say whether it does.
+- Operator-only: a non-operator is redirected out rather than handed a form that
+ would reject on submit.
+- Report: `docs/design-sweep-2/reports/payouts-new-critique.md` /
+ `payouts-new-audit.md`
+
+### 4. `/payouts/[id]` — **register: product**
+
+- Source: `src/app/payouts/[id]/page.tsx` and every co-located component
+ (`lifecycle-submit.tsx`, `notes-form.tsx`, `flat-pool-form.tsx`,
+ `add-participant-form.tsx`, the appraise form)
+- Shots: `06-payout-detail-draft.*` (a draft with a 16-participant roster and
+ both pool kinds), `07-payout-detail-finalized.*`
+- The densest surface in the app and the one with the most controls. Pattern 3 is
+ the primary risk: count the pressable things and say what directs the eye.
+- `ConfirmCost`'s `"visible"` case lives here (Finalize/Unlock sit alone outside
+ a table, so their permanent caption is wanted copy, not a fault appearing).
+ That is settled — do not propose hiding it.
+- Report: `docs/design-sweep-2/reports/payout-detail-critique.md` /
+ `payout-detail-audit.md`
+
+### 5. `/admin/audit` — **register: product**
+
+- Source: `src/app/admin/audit/page.tsx`, `src/app/admin/audit/summarize.ts`,
+ `src/services/audit.ts`'s `queryAuditLog`
+- Shots: `10-audit-full.*` (62 rows), `09-audit-empty.*`
+- Judge it on the promise in PRODUCT.md: an admin can answer "why is this
+ person's role wrong?" in under a minute. Time that path.
+- Pattern 2 is the standing risk on a log table — check what every row repeats.
+ The duplicate-pager item is already known; do not spend a finding on it.
+- The empty state is also one `
`, so do not assert a filter worked from a row
+ count alone.
+- Report: `docs/design-sweep-2/reports/audit-critique.md` / `audit-audit.md`
+
+### 6. `/admin/sync` — **register: product**
+
+- Source: `src/app/admin/sync/page.tsx` (1143 lines — read all of it),
+ `src/app/admin/sync/actions.ts`, `src/app/admin/sync/view.ts`,
+ `src/core/schedules.ts`
+- Shots: `11-admin-sync.*`
+- This surface has already solved pattern 2 once ("Cadence (UTC)"), and folds
+ housekeeping behind a `Disclosure` keyed to whether the group needs attention.
+ Say what still enumerates.
+- Pattern 3 is live at the bottom control row: `Sync now` (primary) /
+ `Recheck invalid affiliations` / `Refresh`, plus a per-job `Re-run` in every
+ drawer.
+- The `Absent` component pairs an aria-hidden glyph with the words it stands
+ for — that is the R4 parity rule working. Do not break it.
+- Report: `docs/design-sweep-2/reports/sync-critique.md` / `sync-audit.md`
+
+### 7. The boundaries — **register: product**
+
+One reviewer pair covers all three; they are small, related, and share a nav
+derivation.
+
+- Source: `src/app/error.tsx` (317 lines, client component),
+ `src/app/not-found.tsx`, `src/app/payouts/[id]/not-found.tsx`
+- Shots: `13-error-boundary.*`, `02-not-found-root.*`, `08-payout-not-found.*`
+- A user meets these on their worst day with the app, and they are the surfaces
+ least likely to have been designed. That is the whole reason they are in scope.
+- Settled and deliberate: no `global-error.tsx`; gold is *not* spent on "Try
+ again"; `error.tsx` hoists a `` because it beats the segment's static
+ metadata; all three are `page--narrow`.
+- `error.tsx`'s escalation block is the one thing a user is asked to copy. Judge
+ whether it is copyable.
+- Report: `docs/design-sweep-2/reports/boundaries-critique.md` /
+ `boundaries-audit.md`
+
+### 8. `/admin/access-lists` — **register: product**
+
+- Source: `src/app/admin/access-lists/page.tsx`
+- Shots: `14-access-lists.*`
+- **This surface appears in neither the owner's scope list nor the Aug-5 sweep.**
+ It has never been reviewed. Mark your report as covering an out-of-scope
+ addition so its findings can be separated cleanly if the owner does not want
+ them.
+- Report: `docs/design-sweep-2/reports/access-lists-critique.md` /
+ `access-lists-audit.md`
+
+---
+
+## The two surface-less reviewers
+
+### A. Whole-app
+
+Gets **every screenshot in `docs/design-sweep-2/shots/` and `PRODUCT.md`, and
+nothing else.** No source. No file tree. No per-surface block.
+
+Questions, all of which have no location:
+
+- Does this read as authored or as generated?
+- What is its composite character, in one paragraph?
+- Which surfaces disagree with the others about what this product is?
+- Could someone guess the palette and theme from the product category alone?
+- Where does the eye go on each screen, and is that where it should go?
+
+**Exempt from `file:line` entirely.** Do not go looking for source you do not
+have. Prose about the composite is the deliverable; a paper cut is not.
+
+Note the anti-references it is being measured against: Alliance Auth / Django
+admin, neon sci-fi HUD, generic dark SaaS, cartoon-forward UI.
+
+Report: `docs/design-sweep-2/reports/whole-app.md`
+
+### B. Record contradiction
+
+Gets `DESIGN.md`, `PRODUCT.md`, and the code implementing them. One question:
+**where does the code contradict its own written rule?**
+
+This is nearly mechanical and it is most productive in the codebases that look
+best maintained. Two from the last sweep of this project: a status-token rule
+saying "colour only when the state is actionable" while the CSS painted every
+healthy state full-chroma green, and a predicate whose docblock argued the
+opposite of what it did.
+
+Check specifically, and quote the rule text next to the code:
+
+- Every claim in DESIGN.md's tables that names a measured number (contrast
+ ratios, hit-target pixel sizes, type-scale steps, tracking tokens).
+- The label register: is every selector in the `--- Label register ---` list
+ actually inheriting 600, and does anything outside it duplicate the style?
+- "Saturated colour occupies well under 10% of any screen" and "one primary
+ action per view, plus the mark" — check per surface against the shots.
+- The two-card exception, the two hit-target grades, the two type one-offs
+ (`0.625rem`, `0.5625rem`) that are supposed to be used exactly once each.
+- PRODUCT.md's five design principles against what actually ships.
+
+Report: `docs/design-sweep-2/reports/record-contradiction.md`
diff --git a/docs/design-sweep-2/SYNTHESIS.md b/docs/design-sweep-2/SYNTHESIS.md
new file mode 100644
index 00000000..25d30877
--- /dev/null
+++ b/docs/design-sweep-2/SYNTHESIS.md
@@ -0,0 +1,700 @@
+# Design sweep 2 — synthesis and ranked backlog
+
+Sweep of 2026-08-10. Eight in-scope surfaces plus one reviewed unrequested.
+Eighteen reports in `reports/`, 63 screenshots in `shots/` (30 from the sweep
+proper, 33 from the two re-shoots that closed the capture gaps).
+
+**Nothing in `src/` has been modified.** This document is the Phase 3 output and
+the Phase 4 gate: the owner chooses what gets worked before any edit.
+
+Ranked by **what the problem costs a user**, not by how many surfaces it recurs
+on. Recurrence is recorded per item as a separate leverage note.
+
+---
+
+## Rank
+
+### 1 — `/payouts/[id]` does not fit a phone, at the document level
+
+**Severity:** Critical (both reviewers, independently, with identical numbers)
+**Cost:** The page where ISK is finalized and paid is the only surface in the app
+whose narrow `fullPage` capture is wider than the viewport: **771px (draft)** and
+**522px (finalized)** against 390px. A fleet member checking whether they were
+paid, on a phone, scrolls the whole document sideways. It also fails SC 1.4.4 at
+200% zoom on any display narrower than ~1600 physical px.
+
+Two independent causes, both one-line:
+
+- **(a)** `.btn-row--tight { white-space: nowrap }` — `globals.css:2974` — inherits
+ into Finalize's visible `ConfirmCost` sentence at `payouts/[id]/page.tsx:508`
+ → `lifecycle-submit.tsx:139-156`. A 137-character sentence renders as one
+ unbreakable **754px** line (measured x=120→x=874 at 1440).
+ Two fixes on the table: **remove `.btn-row--tight` from `page.tsx:508`
+ entirely** (the audit's — the row holds one button, Finalize and Unlock being
+ mutually exclusive by construction), or scope the `white-space` to direct
+ button children (the critique's). The first is cleaner and strictly local.
+- **(b)** `.pool-items` is `display: grid` with an implicit `auto` track and no
+ `minmax(0, 1fr)` — `globals.css:4859` holding `page.tsx:682`. The item table
+ escapes its `Scroller`, which also kills the edge fades **and withdraws the
+ keyboard tab stop** at `scroller.tsx:95` (SC 2.1.1).
+ Fix: `.pool-items { grid-template-columns: minmax(0, 1fr) }`.
+
+**Leverage:** `globals.css:4583` already names `.pool-items` by name as one of
+exactly two at-risk sites, and fixes only the other one (`.drawer__crew` got
+`min-width: 0` at `:4577`).
+
+---
+
+### 2 — Four of `/payouts/new`'s five error messages cannot be reached through the form
+
+**Severity:** Serious
+**Where:** `new-operation-form.tsx:96` (`required`), `:111-112` (`max={today}` +
+`required`), `:119` (`type="url"`), against `src/app/payouts/errors.ts:34-44`
+**Cost:** Native constraint validation runs before the `submit` event, and React's
+`