# The UI sweep — one consolidated brief

> **Operator order, 2026-07-28:** "I want to add up the various changes in the
> various screens (including analysis) to have it done in one go. we should
> spend 80% sharpening the axe for the 20% remaining to be effective."
>
> This is the axe. It is an **inventory and a decision list**, not a mockup
> round and not a design round. React to it ONCE; after that the sweep executes
> without coming back for design.
>
> **Supersedes `appshell-scope-6-kickoff.md`** — see "What survives from
> appshell-scope-6" at the end. Every number below carries the command that
> produced it. Anything I could not confirm is marked UNVERIFIED, never inferred.

---

## 1. The measured state (2026-07-28, all commands run this session)

```
cd apps/web && npx vitest run --pool=forks --poolOptions.forks.maxForks=2 src/styles/conformance.test.ts
```
→ **1924 legacy markers across 76 files**, all 76 baselined and shrink-only.
The oracle is **GREEN**. Green means *measured and fenced from growing*, not
clean.

```
cd apps/web/src && grep -roE '\bso-[a-z][a-z0-9-]*' --include=*.tsx --include=*.ts --include=*.css . | grep -v '\.test\.' | wc -l
```
→ **420** `so-*` usages across **51 files** — of which **68** are the
definitions in `styles/global.css` and **352** are call sites across **50**
`.tsx` files. (A prior note said "314 across 40 files"; that count is stale —
the real figure is higher.)

Top classes: `so-btn` ×123, `so-btn-secondary` ×46, `so-btn-tracked` ×45,
`so-btn-ghost` ×35, `so-btn-primary` ×31, `so-skeleton` ×19, `so-eyebrow` ×15,
`so-btn-danger-ghost` ×14, `so-table` ×11, `so-pill` ×10.

```
grep -roE 'style=\{\{' --include=*.tsx . | grep -v '\.test\.' | wc -l
```
→ **677** inline style blocks (audit 2026-07-17 said 669 — it went **UP by 8**).

```
grep -roE '<input|<select|<textarea' --include=*.tsx . | grep -v '\.test\.' | wc -l
```
→ **244** raw form controls: 155 `<input>`, 64 `<select>`, 25 `<textarea>`.
(appshell-scope-6 line 18 says 249; the current number is 244.)

```
grep -roE '#[0-9a-fA-F]{3,8}\b' --include=*.tsx --include=*.ts --include=*.css . | grep -v '\.test\.' | grep -v 'styles/tokens.css' | wc -l
```
→ **169** raw hex literals outside the token sheet.

```
for v in navy gold gold-2 muted-ink line; do grep -roE "var\(--$v\)" ... | wc -l; done
```
→ `var(--navy)` **102**, `var(--muted-ink)` **86**, `var(--line)` **66**,
`var(--gold-2)` **62**, `var(--gold)` **16**. The audit measured 175 / 159 /
105 / 92 respectively — the legacy palette is **down ~40%**, so the migration
is moving, not stalled.

```
grep -rl "components/ui\|from \"../ui" --include=*.tsx . | grep -v '\.test\.' | wc -l
```
→ **32** files import `components/ui` (the audit said ~9).

---

## 2. Triage of the 28 audit findings (`web-adopt-audit-2026-07-17.md`)

Verified against current code, not trusted. **18 survive unchanged · 6 survive
smaller · 1 fixed · 3 unverifiable.**

| # | Finding | Verdict | Evidence (today) |
|---|---|---|---|
| CR-1 | Three styling systems | **STILL TRUE, reduced** | `global.css:9-45` legacy `:root` still live; legacy var usage down ~40% (§1); 677 inline blocks; `routes/agent/styles.ts` 410 lines — but it is now token-speaking (`styles.ts:1-6`, and `grep -roE '\bso-[a-z]' routes/agent/` → **0**) |
| CR-2 | Font fragmentation | **STILL TRUE, reduced** | Two render-blocking imports remain: `global.css:6` (DM Mono + DM Sans), `tokens.css:15` (Inter + EB Garamond). Usage: `'DM Mono'` ×83, Inter ×50, `'DM Sans'` ×45, EB Garamond ×2, Cormorant ×2. The "every consumer uses Cormorant" claim is now **false** — Cormorant is down to 2 refs |
| CR-3 | `Settings.tsx` god-module | **STILL TRUE, numbers stale** | `wc -l routes/Settings.tsx` → **2974** (audit said 3,115); 56 `useState`, 38 `useQuery`/`useMutation`, 97 `style={{`, 42 raw form controls. Companion list has moved: `Analytics.tsx` is now **539** (was 907 — rebuilt, off the bar); `Automation.tsx` **748** (was 657 — grew); a NEW over-bar file the audit never saw: `routes/agent/Activity.tsx` **1558** |
| HI-1 | Gold focus ring under 3:1 | **STILL TRUE, verbatim** | `global.css:67-70` `outline: 2px solid var(--gold)`; `:74-80` gold halo on fields; `:82-88` re-applied to `.so-btn`/`.so-pill`/`a`/`button` |
| HI-2 | Gold small text | **STILL TRUE, narrower** | `.so-eyebrow` at `global.css:161-171` — `color: var(--gold)`, `'DM Mono'`, `0.72rem` (off-scale). But `PageHeader.tsx` **no longer renders it**, so "every route" is false; it is now **10 sites** (`builder-ui.tsx:145,189`, `CampaignStatsPanel.tsx:65`, `ErrorBoundary.tsx:35`, `ContactSidebar.tsx:64,139`, `TemplateStatsPanel.tsx:61`, `ContactDetail.tsx:110`, `NotFound.tsx:11`) |
| HI-3 | List state not in the URL | **STILL TRUE, much smaller** | `useSearchParams` is now in **14** non-test files (audit said 5), including `Contacts.tsx:57`, Inbox, Campaigns, Analytics, Automation, CsvBroadcast. The surviving case is `Templates.tsx:68-74` — tab/search/sortBy/sortDir/page/rowsPerPage/folderFilter all local (verified) — and it is a DORMANT screen |
| HI-4 | Bearer token in `localStorage` | **STILL TRUE** | `lib/api.ts:26,29,41,45`; `grep -n storage contexts/AuthContext.tsx` → no `storage`-event listener |
| HI-5 | Eight badge components | **STILL TRUE** | All eight files present: `ui/Pill.tsx`, `StatusPill.tsx`, `inbox/StatusBadge.tsx`, `templates/StatusBadge.tsx`, `campaigns/CampaignStatusBadge.tsx`, `charts/QualityBadge.tsx`, `templates/HealthBadge.tsx`, `contacts/ConsentBadge.tsx` |
| HI-6 | Duplicate `SegmentedControl` | **STILL TRUE** | Both `components/ui/SegmentedControl.tsx` and `routes/agent/SegmentedControl.tsx` exist. The hardcoded-colour half is fixed: one justified line remains, `agent/SegmentedControl.tsx:92` (`conformance-allow raw-color`) |
| HI-7 | `ui/` primitives unadopted | **STILL TRUE, much smaller** | `ui/Table` now imported by 7 files including non-agent (`CampaignDetail`, `automation/FlowRunsTab`, `automation/FlowsHome`, `QueryState`). **11** files still hand-roll `<table>`. Two files the audit named are GONE: `routes/Reminders.tsx` and `routes/agent/Knowledge.tsx` are **ABSENT** |
| HI-8 | Two empty-state systems | **STILL TRUE** | `components/QueryState.tsx` and `components/ui/EmptyState.tsx` both present and both in use |
| ME-1 | Marginal pill/muted contrast | **UNVERIFIED** | `styles/contrast.ts` + `chip-tokens.test.ts` exist, but the test asserts only the four **channel chips** (`:27-40`). No test covers `--cm-ok`/`--cm-warn` soft pairs or `--cm-muted` on `--cm-hover`. Not re-computed this session |
| ME-2 | Mouse-only `<tr>` rows | **STILL TRUE, one site left** | `grep -rn "<tr onClick"` → exactly **1**: `components/flows/ExecutionsView.tsx:52`. Activity was rebuilt (no row `onClick`); `routes/agent/ReviewQueue.tsx` is **ABSENT** |
| ME-3 | No per-route `document.title` | **STILL TRUE** | `grep -rn "document.title" src` → **0** matches |
| ME-4 | No skip link | **STILL TRUE** | `grep -n "skip\|Skip" components/AppShell.tsx` → **0** matches |
| ME-5 | No route-change focus reset | **STILL TRUE** | Only drawer focus exists: `AppShell.tsx:322,331` |
| ME-6 | Toast live region not pre-rendered | **STILL TRUE** | `Toast.tsx:187` `if (toasts.length === 0) return null;` |
| ME-7 | Raw hex bypasses both tiers | **STILL TRUE** | 169 raw hex outside `tokens.css` (§1) |
| ME-8 | 669 inline `style={{}}` | **STILL TRUE, WORSE** | **677** today. Concentrations: `Settings.tsx` ×97, `flow-step-editors.tsx` ×47, `TemplateEditorForm.tsx` ×30, `FlowEditor.tsx` ×27, `SegmentBuilder.tsx` ×23, `CampaignDetail.tsx` ×20, `AgentKnowledgeDoc.tsx` ×20 |
| ME-9 | One global ErrorBoundary | **STILL TRUE** | `App.tsx:144-150` — single `<ErrorBoundary>` wrapping `RouterProvider`, outside the shell |
| ME-10 | No response validation | **STILL TRUE** | `grep -rn "safeParse\|from \"zod\"" src` → **0** matches |
| ME-11 | No code-splitting / unbounded Inbox | **STILL TRUE** | `grep -n "React.lazy\|lazy(" App.tsx` → **0**. Inbox: `grep -n "virtual\|slice(" routes/Inbox.tsx` → **0** (the audit's exact line refs are stale; `Inbox.tsx` is now 834 lines) |
| LO-1 | Targets under 24×24px | **UNVERIFIED** | Line refs are dead — audit cites `Toast.tsx:500-510`, the file is **268** lines. Sizes not re-measured |
| LO-2 | IG thumbnails `alt=""` | **FIXED** | `Posts.tsx:348,511` → `alt={postTitle(post)}` |
| LO-3 | Form error association gaps | **STILL TRUE** | `grep -n aria-describedby` in `NewContactDialog.tsx` and `ReplyComposer.tsx` → **0** matches |
| LO-4 | Full-list fetch + client filter | **UNVERIFIED** | Not re-checked at line level this session |
| LO-5 | `AuthContext` value not memoized | **STILL TRUE** | `grep -n useMemo contexts/AuthContext.tsx` → **0** matches |
| LO-6 | `Pager` vs `Pagination` naming | **STILL TRUE, half-fixed** | `Pager.tsx` now has **0** `so-` and is not in the baseline (already swept); `Pagination.tsx:58` still `className="so-btn so-btn-secondary"`. The naming confusion remains |

---

## 3. What the token sheet does not cover (operator decisions, not build choices)

`design/tokens.md` (172 lines, APPROVED 2026-07-09) covers: neutrals, one
accent, four statuses + softs, four channel chips, type scale 10/11/12/13/15/
18/24, weights, spacing/radius/layout, breakpoint, and one spec each for Card ·
Pill · Input · Segmented control · Table · Nav item · Stat tile · Focus · Empty
values · Popover · Side panel · Quiet button.

Three gaps are live in the code today, each already flagged inline by the seat
that hit it:

1. **Three node swatch colours owed.** The round-4 palette named six colours
   for **nine** node kinds, so `question`, `randomizer` and `smart_delay` share
   an approved hex rather than inventing one: `canvas.css:131-138` —
   `trigger`+`randomizer` share `#0E7490`, `message`+`question` share
   `var(--cm-accent)`, `start_flow`+`smart_delay` share `#9333EA`. Consequence
   stated at `CmNodeSwatch.tsx:12-20`: the swatch stops being a unique type
   signal for those three.
2. **A second floating-shadow weight owed.** One token exists
   (`tokens.css:90` `--cm-shadow-raised`; `tokens.md:140`). The canvas spec
   wants two: `canvas.css:42` uses it as "shadow sm" on a node and
   `canvas.css:148` as "shadow lg" on a panel, both flagged in the file header
   (`canvas.css:6-9`). `tokens.md:140` itself says "split it only when a real
   design need proves they must differ" — the canvas is that need.
3. **A third, lighter elevation is being borrowed.**
   `routes/agent/SegmentedControl.tsx:92` carries
   `boxShadow: "0 1px 2px rgba(0,0,0,.05)"` with
   `conformance-allow raw-color: … no shadow token in tokens.md`. If (2) is
   ruled, rule this in the same breath.

**Doc conflict to surface (precedence rule, `CLAUDE.md`).**
`components/canvas/index.ts:2` and `CmNodeSwatch.tsx:5` both cite
`design/tokens.md` **"Canvas UI primitives"** and "design/tokens.md's own
carve-out for this primitive". That section **does not exist** —
`grep -n -i "canvas\|primitive\|round-4\|carve" design/tokens.md` returns only
`bg | #FAFAFA | app canvas` at line 16. Either the sheet owes the section or the
citations are wrong. Operator ruling: fold the section in when gaps 1–3 land.

---

## 4. The replacement inventory — what swaps for what

`components/ui/index.ts` exports (all on `--cm-*`, chrome in `ui.css`, 265 lines):
`Button` (+`ButtonVariant`) · `Card` · `Pill` (+`PillTone`) · `Chip` ·
`SegmentedControl` (+`SegOption`) · `Input` · `Textarea` · `Checkbox` · `Field` ·
`StatTile`/`StatTiles` · `EmptyState` · `Table`.

`components/canvas/index.ts` exports (chrome in `canvas.css`): `CmCanvas` ·
`CmNode` · `CmPort` · `CmWire` · `CmNodeSwatch` (+`CM_NODE_TYPES`,
`CM_NODE_TYPE_LABEL`) · `CmPanel` · `CmChipInput` · `CmSelect` · `CmInput` ·
`CmField` · `CmTextarea` · `CmCheckbox` · `CmModalTabs`.

| Legacy pattern | Count | Replacement |
|---|---|---|
| `so-btn` + variants (`-primary/-secondary/-ghost/-danger/-danger-ghost/-tracked`) | 294 | `ui/Button` with `variant` |
| `so-pill` / `so-pill-active` | 15 | `ui/Pill` (status) or `ui/Chip` (filter) |
| `so-table` / `so-table-scroll` | 16 | `ui/Table` |
| `so-card` / `so-card-hover` | 6 | `ui/Card` |
| `so-eyebrow` | 15 | tokens.md small-caps label (`--cm-fs-2xs`, `--cm-fw-label`, `--cm-muted`) — **not** gold, **not** DM Mono |
| `so-skeleton` | 19 | keep `Skeleton`, re-token it (do not multiply primitives) |
| `so-modal-*` / `so-toast-*` | 21 | keep `Modal`/`Toast` (APG-conformant per audit §"What passes"), re-token internals only |
| raw `<input>`/`<select>`/`<textarea>` | 244 | `ui/Input` · `ui/Textarea` · `ui/Checkbox` · `ui/Field`; `CmSelect` in canvas contexts |
| 8 badge components | 8 files | `ui/Pill` (one status→tone map, one place) |
| `QueryState` vs `ui/EmptyState` | 27 vs 6 uses | one empty/loading surface; `QueryState` already imports `ui/Table`, so it is the keeper — it adopts `EmptyState` internally |
| `routes/agent/SegmentedControl.tsx` | 1 file | delete; use `ui/SegmentedControl` |

---

## 5. Screen-by-screen — ordered by LEVERAGE, not by marker count

**Nine of the ten worst files sit on screens that are DORMANT or being
DELETED** (`appshell-scope-6-kickoff.md:37-43`). Sweeping by marker count is
the most expensive way to produce the least value. Order below is by how many
other screens each change improves.

### Tier 0 — the shared fence (lands and deploys FIRST, alone)

| Screen / file | Lines | Markers | What changes | Replaces | Mockup | Blocked by |
|---|---|---|---|---|---|---|
| `styles/global.css` | 605 | **237** (68 `so-`) | Kill the gold focus ring (`:67-88`) → `--cm-accent-ink` 2px, offset 2 (`tokens.md:124-125`). Kill the gold `.so-eyebrow` (`:161-171`). Drop the Google-Fonts `@import` at `:6`. Point the legacy `:root` names at `--cm-*` equivalents so **every** unswept screen improves without being touched | — | No | **D4** (fonts), **D6** (ring) |

Why first: every legacy screen consumes it. Nothing else may run concurrently.
This one file is the highest-leverage change in the whole sweep — it moves the
app-wide focus-ring failure (HI-1) and the gold-text failure (HI-2) at once.

### Tier 1 — shared components (render on every screen)

| File | Lines | Markers | What changes | Replaces | Mockup | Blocked by |
|---|---|---|---|---|---|---|
| `components/StatusPill.tsx` | — | 26 | Absorb into `ui/Pill`; one status→tone map | `ui/Pill` | No | **D1** |
| `components/QueryState.tsx` | — | 20 | Re-token; adopt `ui/EmptyState` internally (HI-8 dies here) | `ui/EmptyState` | No | D1 |
| `components/ErrorBoundary.tsx` | — | 18 | Re-token; drop `so-eyebrow:35`; add per-route boundaries inside the shell (ME-9) | `ui/*` | No | — |
| `components/Toast.tsx` | 268 | 12 | Re-token `so-toast-*`; pre-render the live region (ME-6) | — | No | — |
| `components/Skeleton.tsx` | — | 11 | Re-token `so-skeleton` (19 call sites) | — | No | — |
| `components/Modal.tsx` | — | 9 | Re-token `so-modal-*`; keep the APG behaviour untouched | — | No | — |
| `components/forms/FileInput.tsx` | — | 8 | Re-token | `ui/Input` | No | — |
| `components/Pagination.tsx` | 96 | 7 | Re-token `:58`; rename the `Pager`/`Pagination` pair (LO-6) | `ui/Button` | No | — |
| `components/ConfirmDialog.tsx` | — | 5 | Re-token | `Modal` | No | — |
| The 7 other badge files | — | ~90 | Collapse into `ui/Pill` (HI-5) | `ui/Pill` | No | D1 |
| `routes/agent/SegmentedControl.tsx` | — | 0 | **Delete** (HI-6) | `ui/SegmentedControl` | No | — |
| `components/AppShell.tsx` | 490 | 0 | Skip link to `<main>` (ME-4), route-change focus reset (ME-5), per-route `document.title` (ME-3) | — | No | — |

Tier 1 total ≈ **116 markers** plus the badge collapse — but it improves
surfaces the sweep never opens. Highest value per line in the package.

### Tier 2 — the form-control retirement (244 controls)

| File | Raw controls | What changes | Blocked by |
|---|---|---|---|
| `routes/Settings.tsx` | 42 | → `ui/Input`/`Textarea`/`Checkbox`/`Field` | Tier 1 |
| `components/flows/flow-step-editors.tsx` | 25 | **SKIP** — dying screen (Stage G) | — |
| `routes/Automation.tsx` | 17 | **SKIP** — board ruling, see §7 | — |
| `routes/AgentKnowledgeDoc.tsx` | 16 | → `ui/*` | agent-scope-14 item 6/7 (KB re-scope) |
| `components/templates/TemplateEditorForm.tsx` | 15 | **DEFER** — dormant | — |
| `routes/Contacts.tsx` | 10 | → `ui/*` | Tier 1 |
| `components/contacts/SegmentBuilder.tsx` | 10 | → `ui/*` | Tier 1 |
| `routes/agent/Activity.tsx` | 9 | → `ui/*` as part of the filtration rework | **mockup** (item 15) |
| remainder (≈100 across ~30 files) | | → `ui/*` where the screen is live | Tier 1 |

### Tier 3 — the live screens (daily use, not slated for deletion)

| Screen | File | Lines | Markers | What changes | Mockup | Blocked by |
|---|---|---|---|---|---|---|
| **Settings** | `routes/Settings.tsx` | **2974** | **0** | Split (see §6) + 97 inline blocks + 42 raw controls. **Conformance-clean already** — the ratchet is blind to it | No | **D9** |
| Inbox | `routes/Inbox.tsx` | 834 | 22 | Re-token; 4 raw controls; cap the unbounded thread + page accumulation (ME-11) | No | Tier 0/1 |
| Contacts | `routes/Contacts.tsx` | 692 | 30 | Re-token; hand-rolled `<table>` → `ui/Table`; `NewContactDialog` `aria-describedby` (LO-3) | No | Tier 1 |
| ApiDocs | `routes/ApiDocs.tsx` | 440 | 35 | Re-token; `<table>` → `ui/Table` | No | Tier 1 |
| ContactDetail | `routes/ContactDetail.tsx` | 378 | 23 | Re-token; drop `so-eyebrow:110` | No | Tier 1 |
| Login | `routes/Login.tsx` | 206 | 29 | Re-token (gold subtitle) | No | Tier 0 |
| NotFound | `routes/NotFound.tsx` | 47 | 8 | Re-token; drop `so-eyebrow:11` | No | Tier 0 |
| Home | `routes/Home.tsx` | 541 | 0 | `routes/home.css` carries 3 — clear them and delete the baseline entry | No | — |
| **Activity** | `routes/agent/Activity.tsx` | **1558** | 0 | agent-scope-14 §D item 12 (the label tag reads wrong, wanted subtle and not its own column) — items 9/10/11/13 already landed at `6ac1894` | **No** | — |
| Activity — review detail panel | same | — | — | agent-scope-14 item 14, complete redesign | **YES** | mockup round |
| Activity — filtration UI | same | — | — | agent-scope-14 item 15, "the system changed but not the UI" | **YES** | mockup round |
| Analytics | `routes/Analytics.tsx` | 539 | 0 | Conformance-clean. The problem is not styling — see **D5** | No | **D5** |

### Tier 4 — deliberately deferred (boarded, not dropped)

`CampaignDetail` 90 · `Campaigns` 82 · `Templates` 68 · `TemplateEditorForm` 57
· `TemplateStatsPanel` 52 · `TemplateDetail` 33 · `TemplateEditor` 26 ·
`CsvBroadcast` 24 · `whatsapp-message` 24 · `RecipientTable` 23 ·
`ApiCampaignBuilder` 25 · `MediaHeaderPicker` 35 · `builder-ui` 33 · plus the
rest of the campaigns/templates families — **WhatsApp surfaces, scope DORMANT**
while the number is Meta-disabled.

`flow-step-editors` 112 · `FlowEditor` 91 · `ExecutionsView` 55 · `FlowCanvas`
48 · `flow-canvas.css` 25 · `Flows` 27 — **being deleted** by channels-scope-5
Stage G. Repainting them is pure waste. ME-2's last live site
(`ExecutionsView.tsx:52`) dies with the screen.

Deferred total ≈ **960 markers, ~50% of everything**. That is the point: the
sweep buys the whole visible product for the other half.

---

## 6. `Settings.tsx` — the single worst file

`wc -l` → **2974** lines. 56 `useState` · 38 `useQuery`/`useMutation` · 97
inline `style={{}}` · 42 raw form controls · a hand-rolled `<table>`.

**It has ZERO legacy markers** — it was swept in appshell-scope-5 and is not in
`LEGACY_BASELINE`. The conformance oracle says it is perfect. It is the worst
file in the app. That gap is the strongest argument for **D10**.

**Recommended split** (D9): `routes/settings/` — one module per tab, each
owning its own queries and form state, with the parent reduced to routing +
tab chrome. Extract the repeated form shapes into `ui/Field`-based components
rather than moving inline styles around. `React.lazy` per tab module falls out
for free and starts paying down ME-11. **Oracle for the split:** the existing
`routes/Settings.test.tsx` (562 lines) must stay green untouched — if the split
needs the tests rewritten, the split changed behaviour and is wrong.

---

## 7. Decisions needed — every fork only the operator can settle

| # | Fork | Recommendation |
|---|---|---|
| **D1** | **21st.dev.** The app has **no Tailwind, no Radix, no shadcn, no CSS modules** — verified: `grep -riE 'tailwind\|@radix-ui\|shadcn\|\.module\.css\|styled-components\|emotion' package.json apps/web/package.json` → **no matches**. Adopting 21st.dev *code* imports a **fourth** styling system into an app whose top audit Critical is already "three competing styling systems". `HANDOFF.md:35-37` records a ruling: "reference by default, adopt a specific element when wanted", pending confirmation before Tier 1 | **Confirm reference-only.** Copy the interaction patterns and visual polish, implement as `cm-*` on `--cm-*`. No new dependency, the oracle keeps working unchanged, the Critical shrinks instead of growing. Needed **before Tier 1**, because Tier 1 is where the answer changes the code |
| **D2** | **Three node swatch colours** (`question`, `randomizer`, `smart_delay`) — today they share an approved hex (`canvas.css:131-138`) | Pick three. They are the only thing standing between the canvas and a unique per-type signal. If you would rather not spend a decision here: keep the sharing permanently and say so, so the inline "the sheet still owes three" comments can be deleted |
| **D3** | **A second (and third) shadow weight** — `shadow sm` on a node, `shadow lg` on a panel, plus the borrowed `0 1px 2px` at `agent/SegmentedControl.tsx:92` | Approve two more: `shadow-sm` (`0 1px 2px` of ink at 5%) and `shadow-lg` (`0 8px 28px` of ink at 14%), keeping `shadow-raised` as the middle. Derived from `--cm-ink` like the existing one, so the palette stays single-sourced |
| **D4** | **Fonts.** Two render-blocking imports, four families. The sheet says Inter + EB Garamond; usage says DM Mono ×83 / DM Sans ×45 | **Sheet wins.** Delete `global.css:6`, migrate `'DM Mono'`/`'DM Sans'` to `--cm-font`/`--cm-font-mono`. Self-hosting WOFF2 is a **separate** perf item, not this sweep |
| **D5** | **Analytics definition round — yours, and the sweep cannot start it.** Your verdict was "still confusing and you really don't know what to look at or how" (`scope-board.md:29-33`). The screen is already conformance-clean (539 lines, 0 markers), so **no amount of sweeping fixes it.** What is needed from you, and nothing more: (a) the **one question** the screen answers on open; (b) the **three to five numbers** that answer it; (c) what a **bad** number should make you do next | Answer (a)(b)(c) in three lines. The sweep does not touch Analytics until they exist. I am deliberately **not** designing it for you |
| **D6** | **The gold focus ring dies app-wide** (`global.css:67-88`, ~2.16:1, under the 3:1 floor). Replacing it changes the visible focus colour on **every** legacy screen at once | Do it in Tier 0. `tokens.md:124-125` already makes 2px `accent-ink` non-negotiable. Flagging it only because the visual change is app-wide and instant |
| **D7** | **`.so-eyebrow`** — gold, DM Mono, off-scale `0.72rem`, 15 uses | Delete the class; replace with the sheet's small-caps label. Keeps the *shape* of the section header, loses the gold |
| **D8** | **HI-4 auth token in `localStorage`** — needs an API seam (httpOnly cookie), not a UI change | **Out of this sweep.** Board it as its own package. Mixing an auth-session change into a styling sweep makes a regression un-bisectable |
| **D9** | **Settings split shape** — per-tab route modules vs per-tab components in one file | Per-tab route modules under `routes/settings/`, lazy-loaded. Same work either way; the module version also buys the first slice of code-splitting |
| **D10** | **Does the oracle grow two new ratchets?** Today it counts legacy *markers* only — which is why a 2,974-line god-module with 97 inline style blocks scores a perfect zero | **Yes.** Pin `style={{` at 677 and raw form controls at 244 as shrink-only baselines in the same test file. Same string-scan mechanism, ~20 lines, and it makes the two defects that regressed since July (ME-8 went 669→677) impossible to repeat |
| **D11** | **`design/tokens.md` "Canvas UI primitives" is cited by code but does not exist** (§3) | Amend the sheet with a Canvas section when D2/D3 land, rather than editing the citations. Surfacing it per the Precedence rule rather than silently reconciling |

---

## 8. The mechanical oracle — how the sweep proves it landed

**The oracle exists and does not need building.** `apps/web/src/styles/
conformance.test.ts` + `conformance-scan.ts` scan every shipped `.ts`/`.tsx`/
`.css` under `apps/web/src` for five rules: `legacy-class` (`so-*`),
`legacy-var` (the 33 bare-name palette entries), `legacy-font` (DM Sans / DM
Mono / Cormorant), `raw-color` (hex/rgb/hsl outside `tokens.css`),
`off-scale-type` (any size off 10/11/12/13/15/18/24).

**How it ratchets** (`conformance.test.ts:26-40`):
- a file **not** in `LEGACY_BASELINE` must have **zero** — a new violation
  fails the build immediately;
- a file **in** `LEGACY_BASELINE` is **pinned**: it may only go down. Lowering
  means editing the number; reaching zero means **deleting the entry** — the
  test fails on a stale entry, so a cleaned file cannot be left behind.

**A stage is done when its files are DELETED from `LEGACY_BASELINE`**, not
lowered and not excused (`appshell-scope-6-kickoff.md:69-72`; DECISIONS
2026-07-22, "when I say sweep, I mean a sweep"). Eyeball evidence does not
close a stage.

**Per-tier exit condition:**

| Tier | Baseline entries deleted | Markers cleared |
|---|---|---|
| 0 | `styles/global.css` | 237 |
| 1 | 9 shared-component entries + the badge files | ~206 |
| 2 | (no baseline movement — see below) | 0 |
| 3 | `Inbox`, `Contacts`, `ContactDetail`, `Login`, `NotFound`, `ApiDocs`, `home.css`, `AnalyticsAgentOutcomes` | ~151 |
| 4 | deferred, entries stay pinned | ~960 held |

**What the oracle does NOT catch — name the second oracle for each:**
- **file size / god-modules (CR-3)** → `wc -l`, plus `Settings.test.tsx` green
  through the split;
- **inline `style={{}}` (ME-8)** and **raw form controls** → nothing today.
  This is **D10**;
- **`document.title` / skip link / focus reset (ME-3/4/5)** → three assertions
  in the existing `AppShell` tests;
- **duplicate components (HI-5/HI-6/HI-8)** → a file that no longer exists is
  its own proof; the delete is the oracle;
- **contrast (ME-1)** → `styles/contrast.ts` already exists and
  `chip-tokens.test.ts` shows the pattern; extend it to `--cm-ok`/`--cm-warn`
  softs and `--cm-muted` on `--cm-hover` (currently untested — §2 ME-1).

**Full gate, per `CLAUDE.md` Definition of done:**
```
cd apps/web && npx vitest run --pool=forks --poolOptions.forks.maxForks=2
corepack pnpm --filter @channels/web build
corepack pnpm lint && corepack pnpm typecheck
```
plus 320/375/768 verified per touched screen, then published.

---

## 9. Deliberately NOT in this sweep

| Excluded | Why |
|---|---|
| **Automation** (`routes/Automation.tsx`, 748 lines, 17 raw controls) | Board ruling, `scope-board.md:24-28`: "Automation gets NO glance and should not be polished… this scope should literally change automation." Superseded by channels-scope-5 Stage G. **Polishing it is spending against a delete** |
| **Analytics visual work** | `scope-board.md:29-33` — the glance is given and negative; it is blocked on **D5**, a definition round the operator owns. The screen is already conformance-clean, so sweeping it changes nothing that was wrong with it |
| **The flow editor family** (~360 markers) | channels-scope-5 Stage G retires the screens. `appshell-scope-6-kickoff.md:62-65` already boards this |
| **The WhatsApp surfaces** (~600 markers) | Scope DORMANT while the number is Meta-disabled (`appshell-scope-6-kickoff.md:28-33`). Swept when the scope wakes |
| **HI-4 auth token storage** | Needs an API seam, not a UI change — **D8** |
| **ME-10 Zod at the web response boundary** | Real, and a project-rule violation, but it is a data-integrity package with zero visual output. Board it separately |
| **ME-11 route code-splitting** | A perf package. The Settings split (**D9**) delivers the first slice as a by-product; the other ~40 routes are their own item |
| **Self-hosting the fonts** | **D4** removes the second `@import`; WOFF2 self-hosting + preload + metric override is a separate perf item |
| **The `routes/agent/` screens' styling** | Already swept — `grep -roE '\bso-[a-z]' routes/agent/` → **0**, and no agent file appears in `LEGACY_BASELINE`. Only the agent-scope-14 §D **behaviour** defects remain |
| **New screens, features or concepts** | Not a rethink and not a design round. UX improvement means plainer words, honest empty states, real focus states, 320/375/768 clean — not new layouts |

---

## 10. What survives from `appshell-scope-6-kickoff.md`

**Survives, unchanged and still correct:**
- the leverage ordering (`global.css` → shared components → form controls →
  live screens) — §5 keeps it and adds the per-screen detail it lacked;
- all six Non-negotiables (lines 67-88): no fencing, mechanical oracle,
  tokens.md is law, improvement not redesign, accessibility, nothing temporary;
- the Stage 5 deferral of the WhatsApp + flow-editor families (lines 62-65);
- the Definition of done (lines 121-125);
- the 21st.dev fork framing (lines 90-110) — now **D1**, with `HANDOFF.md:35-37`
  recording that a ruling already exists and needs only confirmation.

**Superseded by this brief:**
- its measured numbers — 249 raw controls is now **244**, 163 hex is now
  **169**, and it never counted inline styles or `so-*` call sites;
- "Analytics — out of scope, a definition round" (line 116-117) → now **D5**
  with the three concrete questions the round needs;
- its silence on the audit — the 28 findings were never triaged; §2 does it;
- its silence on `Settings.tsx` — the worst file in the app scores zero on its
  oracle; §6 and **D10** close that hole;
- its silence on the token-sheet gaps — §3 lists three plus a doc conflict;
- Stage 3's dependency note ("depends on the primitives seat landing first") —
  **discharged**: the primitives exist and are inventoried in §4.
