# App shell scope — work-package 4: design-token conformance sweep

> Authored by the orchestrator (Fable), 2026-07-20. **Authority:** operator
> steer 2026-07-20 ("I still see elements and aspects of the app not
> following design tokens like inbox, analytics, settings and also some
> sections in agents, like it has some aspect of the old design tokens") +
> adopt audit Critical #1 (three competing styling systems,
> `plans/rethink/web-adopt-audit-2026-07-17.md`). Presentation-only package:
> zero behavior change.

## Scope

Bring every legacy-styled surface onto the `--cm-*` token system and house
components. Old look = the WhatsApp-era styling: hardcoded hex values,
legacy CSS classes, navy-era remnants, non-token fonts/spacings.

**In:** Inbox, Settings (all tabs — styling only, do NOT refactor the
god-module; that is a separate audit item), remaining legacy aspects inside
Agent screens, and any other route the Stage-0 inventory catches.
**Out (other seats own them):** Analytics (analytics-scope-1 rebuilds it),
Contacts/Segments (channels-scope-3), the new `/agent/situations` screen
(agent-scope-4), the AppShell nav itself (already done).

## Stages

### Stage 0 — inventory (the oracle's foundation)
Grep-based inventory per route: hardcoded hex/rgb values, styles not
derived from `--cm-*`, legacy class names/systems (name them), non-house
fonts. Table in the stage log: route → violations → severity. This list IS
the work list; nothing swept that isn't on it, nothing on it left unswept
without a recorded reason.

### Stage 1..N — per-surface conformance
One commit per surface (Inbox, Settings, agent remnants, …). House
patterns only; visual parity where the screen already matches the design
language, correction where it doesn't. Existing tests must stay green
unmodified (presentation-only); snapshot/style tests updated deliberately.

### Final — evidence
Screenshots 1440 + 375 per swept surface → `plans/rethink/evidence/
appshell-scope-4-*`. Grep oracle re-run clean.

## Gate bar

1. Web suite green (capped forks), typecheck/lint/build exit 0.
2. Stage-0 inventory recorded; post-sweep grep shows zero remaining
   violations on swept surfaces (or each remnant has a recorded reason).
3. Zero behavior change — no route, handler, or API-call diffs.
4. Screenshots delivered before publish; style law (no pills, icons not
   emojis, wordmark-only serif) holds.
5. Deferrals to the board; stage log current.

## Standing rules

`corepack pnpm`; capped-fork suites; pathspec commits; rebase-with-backoff
before push (never delete index.lock); pushed state builds; no deploy/
publish (orchestrator's lane).

## Stage log

### Stage 0 — inventory (2026-07-20, Opus executor) — THE ORACLE

Method: `grep -rEc` across `apps/web/src/{routes,components,styles}` for the four
violation classes. **Legacy vars** = `global.css` names (`--navy*`, `--gold*`,
`--green*`, `--red*`, `--warn*`, `--info*`, `--bg*`, `--card`, `--text*`,
`--muted*`, `--line*`). **Legacy classes** = `so-*` chrome. **Non-house fonts**
= literal `'DM Sans'` / `'DM Mono'` / `Cormorant` (house = `--cm-font` Inter,
`--cm-font-mono`, `--cm-font-brand` EB Garamond wordmark-only). **Raw hex/rgba**
= hardcoded colour outside `tokens.css`.

Reality confirmed (adopt-audit CR-1): the `--cm-*` system is adopted only by the
Agent section + Home + AppShell; the **whole rest of the app runs on the legacy
`global.css` navy/gold `so-*` system**. This is an app-wide stalled migration, not
a handful of strays. This package sweeps the **operator-named surfaces** and defers
the dormant WhatsApp-era surfaces to the board.

#### Work list (in scope) — route/file → violations → severity

| Surface | File | legacy-var | so-* | DM-font | raw-hex | sev |
|---|---|--:|--:|--:|--:|---|
| **A · Agent remnants** | `routes/agent/styles.ts` | 64 | 0 | 11 | 2 (`#fff`) | HIGH |
| (flagship; must be pure cm) | `routes/agent/SegmentedControl.tsx` | 5 | 0 | 0 | 2 (`#fff`,rgba) | MED |
| | `routes/agent/InstagramChannelCard.tsx` | 2 | 1 | 0 | 0 | LOW |
| | `routes/agent/FacebookChannelCard.tsx` | 2 | 1 | 0 | 0 | LOW |
| | `routes/agent/posts.css` | 0 | 0 | 0 | ~20 (placeholder gradients + image scrims — mostly legit) | LOW |
| **B · Inbox** | `routes/Inbox.tsx` | 39 | 2 | 5 | 0 | HIGH |
| (chrome only) | `components/inbox/ContactSidebar.tsx` | 23 | 4 | 6 | 0 | HIGH |
| | `components/inbox/MessageDetailDrawer.tsx` | 22 | 0 | 5 | 0 | HIGH |
| | `components/inbox/MessageContent.tsx` | 13 | 0 | 3 | 0 | MED |
| | `components/inbox/TemplateSendForm.tsx` | 8 | 2 | 3 | 0 | MED |
| | `components/inbox/ReplyComposer.tsx` | 8 | 3 | 1 | 0 | MED |
| | `components/inbox/StatusBadge.tsx` | 7 | 0 | 1 | 0 | MED |
| | `components/inbox/InboxMedia.tsx` | 5 | 0 | 0 | 1 (`--danger,#c0392b`) | LOW |
| **C · Settings** | `routes/Settings.tsx` | 53 | 57 | 8 | 3 (`#fff`,`#0A1F44`×2) | HIGH |
| (styling only, no refactor) | `components/settings/WebhookStatusPanel.tsx` | 17 | 0 | 0 | 0 | MED |
| | `components/settings/ConnectionForm.tsx` | 7 | 0 | 1 | 0 | MED |
| **D · Contacts + Segments** | `routes/Contacts.tsx` | 26 | ~41 | 2 | 6 (`#fff`, navy/gold fills) | HIGH |
| (un-fenced mid-package — see | `routes/ContactDetail.tsx` | 19 | ~2 | 5 | 0 | HIGH |
| orchestrator note below; | `components/contacts/SegmentBuilder.tsx` | 20 | 0 | 2 | 3 (`#fff`, navy fill) | HIGH |
| channels-scope-3 built the | `components/contacts/ImportDialog.tsx` | 20 | 2 | 5 | 0 | MED |
| filter/fold ON legacy tokens) | `components/contacts/ConsentPanel.tsx` | 10 | 0 | 1 | 0 | MED |
| | `components/contacts/SegmentsPanel.tsx` | 10 | 2 | 2 | 0 | MED |
| | `components/contacts/NewContactDialog.tsx` | 5 | 2 | 1 | 0 | LOW |
| | `components/contacts/ConsentBadge.tsx` / `ChannelMark.tsx` | 3 / 2 | 0 | 1 / 0 | 0 | LOW |

> **Fence change (orchestrator, mid-package):** channels-scope-3 landed and the
> operator flagged it ("the channels changes and filters didn't change the pages
> to obey new design tokens, just built on top of it"). Contacts/Segments moved
> from Fenced → **Surface D** with its own commit. Analytics stays fenced.

#### Fenced (owned by other seats — NOT touched)
- **Analytics**: `Analytics.tsx`, `AnalyticsAgentOutcomes.tsx`, `analytics-shared.tsx`,
  `analytics.css`, `components/charts/*` — analytics-scope-1 rebuild.
- **AppShell nav** — done. **`/agent/situations`** — agent-scope-4 (does not exist yet).

#### STOP-AND-REPORT (conformance would force a behaviour/semantic change — left as-is deliberately)
- `components/inbox/MessageBubble.tsx` — `#d9fdd3` (WhatsApp sent-green), `#53bdeb`
  (read-tick blue), `#111b21`, `#fbe9e7` (failed), tail SVGs. These are a **deliberate
  WhatsApp message-bubble skeuomorph**, not brand chrome. Retinting them to `--cm-*`
  would change what the bubble *means* (it reproduces WhatsApp). Fenced by design.
- `components/inbox/Avatar.tsx` — an 8-colour **deterministic identity palette** (hash →
  avatar colour). Not UI chrome; retinting changes per-contact identity colours. Fenced.

#### Deferred to the board (dormant WhatsApp-era surfaces — not operator-named, not urgent)
Campaigns/Templates/Flows/Automation/Reminders/ApiDocs/CsvBroadcast/QrCampaigns/Login
and their `components/{campaigns,templates,flows}/*` — all still on the legacy system
(adopt-audit: "none urgent until the number returns"). Shared cross-cutting chrome
(`PageHeader`, `QueryState`, `StatusPill`, `Pager`, `Pagination`, `Toast`, `Tags`,
`ConfirmDialog`) touches fenced surfaces → belongs to the DS-convergence board package
(audit CR-1/HI-5/HI-8), not a per-surface sweep here.

#### Legacy→cm mapping used (semantic, applied uniformly)
`--navy`(text)→`--cm-ink` · `--navy`(accent fill/active)→`--cm-accent` · `--gold*`(UI
accent — gold is logo-only in cm)→`--cm-accent*` · `--text`→`--cm-ink` ·
`--text-2`→`--cm-ink-2` · `--muted`→`--cm-muted` · `--muted-ink`→`--cm-ink-2` ·
`--line`→`--cm-line` · `--line-strong`→`--cm-line-strong` · `--bg`→`--cm-bg` ·
`--bg-2`/`--navy-soft`→`--cm-hover` · `--card`/`#fff`(surface)→`--cm-surface` ·
`--green*`→`--cm-ok*` · `--red*`→`--cm-bad*` · `--warn*`→`--cm-warn*` ·
`--info*`→`--cm-info*` · `'DM Sans'`→`var(--cm-font)` · `'DM Mono'`→`var(--cm-font-mono)`.
Agent = correction (must be pure cm); Inbox/Settings = parity where it already matched,
correction of navy/gold to zinc/indigo per `design/tokens.md`.

**Baseline:** `corepack pnpm --filter @channels/web build` exit 0 before any edit.

### Stages 1–4 — per-surface conformance (2026-07-20, Opus executor) — EXECUTED

One pathspec commit per surface. All presentation-only, zero route/handler/API diffs,
existing behaviour tests **green unmodified** (no test edited).

| Surface | sha | oracle tail |
|---|---|---|
| A · Agent remnants | `d6ad0c3` | agent suite 162/162; build exit 0 |
| B · Inbox chrome | `e26cb86` | inbox suite 52/52; build exit 0 |
| C · Settings | `c2bc536` | Settings suite 25/25; build exit 0 |
| D · Contacts + Segments | `de5656d` | Contacts/Segments 32/32; build exit 0 |

**Gate oracles (whole package):**
- **Full web suite (capped forks): 628/628 pass, 83 files, exit 0.**
- **Typecheck exit 0. Lint 0 errors** (6 pre-existing warnings in `quality-api.test.ts`, not this package).
- **Post-sweep grep on all 26 swept files: ZERO** legacy vars (`--navy/gold/muted/line/
  text/bg/card/red/green/warn/info`) and ZERO `'DM Sans'`/`'DM Mono'`/`Cormorant`.
- Screenshots (real compiled `tokens.css`+`global.css`+`ui.css`, exact classes/inline
  tokens): `plans/rethink/evidence/appshell-scope-4-tokens-{1440,375}.png`. Render
  confirms indigo/zinc across all four surfaces; the WhatsApp bubble stays green
  (fenced skeuomorph). *(Method note: a `vite preview` of `dist/` briefly existed with
  an authenticated session but was torn down by another seat; sessions can't
  authenticate as the operator, so the token-render harness is the self-serve evidence —
  the live authenticated click-through remains the operator's, as with prior gate bars.)*

### Deferred (recorded reasons — NOT swept)
1. **`so-btn*` button chrome** (76 usages across swept surfaces) — **no `cm-btn`
   primitive exists** in `ui/`. Retinting `.so-btn*` in `global.css` is an app-wide
   change (hits fenced + dormant surfaces, unscreenshotable per-surface here). → DS-
   convergence board package (audit CR-1), which should first author a cm button.
2. **`so-pill*` filter-chips + `so-eyebrow` + `so-h*`** — `cm-pill` is a *status*
   component, not a filter toggle; `so-eyebrow`/`so-h*` are the gold-text/heading
   chrome tied to a11y fixes HI-1/HI-2 (their own board package). No 1:1 house swap.
3. **Dormant WhatsApp-era surfaces** (Campaigns/Templates/Flows/Automation/Reminders/
   ApiDocs/CsvBroadcast/QrCampaigns/Login) + shared cross-cutting chrome (`PageHeader`,
   `QueryState`, `StatusPill`, `Pager`, `Pagination`, `Toast`) — not operator-named,
   app-wide blast radius → DS-convergence board package.
4. **`MessageBubble.tsx` + `Avatar.tsx`** — behaviour/meaning change (see STOP-AND-REPORT).
5. **`SegmentBuilder` card/control chrome** inherits `campaigns/builder-ui.tsx` (dormant) —
   the swept file's own inline styles are on cm; the imported builder primitives are not.
