---
phase: 12
slug: bulk-sweep-batch-review-queue
status: draft
shadcn_initialized: false
preset: none
created: 2026-06-24
---

# Phase 12 — UI Design Contract

> Visual and interaction contract for **Bulk Sweep & Batch Review Queue** (TAG-05, TAG-06): the admin **bulk-sweep start control** (untagged-by-default with an "all pages" toggle, a "started — N pages queued" confirmation, plus in-progress / empty / error states) and the dedicated **tag-review route** (`/app/tag-review`) — a backlog list of pages with pending suggestions, reviewed **one page at a time by REUSING the Phase-11 per-tag approval list**, with a batched **Apply approved** action, progress through the backlog, and the empty / loading / error / per-page stale states. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
>
> **FINAL phase of milestone v1.0 — subsequent phase on a shipped app.** This contract LOCKS to the existing hand-rolled token system (`web/src/styles/tokens.css` + `web/src/styles/controls.css`). It introduces **NO new design language, NO new dependency, and NO new token.** It composes three already-shipped patterns:
> 1. the **Phase-11 `TagSuggestList` approval surface** (`web/src/components/TagSuggest.tsx`) — REUSED verbatim per page in the queue (its per-tag rows, new-vs-existing badge, new-default-unchecked, select-all/clear, count line, and stale/empty/error states are inherited, **not re-specified here**);
> 2. the **admin "Rebuild graph index" section** in `Admin.tsx` (`.admin-section` / `.admin-section-heading` / `.admin-muted` / `.admin-section-row` + `.btn-secondary` + `"Starting…"` busy + muted `role="status"` confirmation + `.field-error role="alert"`) — the analog for the **sweep-start control**;
> 3. the **Phase-10 lazy-route + nav-entry pattern** (`GraphView` at `/app/graph`, the `.navrow .navrail-trash-row` + `tree-icon` + `tree-label` + `navrail-row-active` + `aria-current` row, and the `isAdmin = role === "admin"` gate in `AppShell.tsx`) — the precedent for the **`/app/tag-review` route + admin-visible nav entry**.
>
> **Scope guard:** This spec covers ONLY (a) the sweep-start control and (b) the review-queue route shell (backlog list + one-page-at-a-time review + batched apply + progress + states). It does **NOT** re-spec the per-tag approval row internals — those are inherited from `11-UI-SPEC.md` / `TagSuggest.tsx`. Backend mechanics (`KindTagSuggest` job, the `tag_suggestions` staging table, endpoint shapes, the 409 floor, batch granularity) are owned by the phase plan; they appear here only where they drive a visible state.

---

## Design System

| Property | Value |
|----------|-------|
| Tool | none (hand-rolled token CSS; no shadcn / no Tailwind — confirmed: no `components.json`, no `tailwind.config.*`, no `postcss.config.*`) |
| Preset | not applicable |
| Component library | none (token-only CSS-per-component; the `controls.css` `.btn`/`.input`/`.field` primitives, the `Admin.css` `.admin-section` block, the `Dialog.css`/`DiffReviewDialog.css`/`TagSuggest.css` modal shell, the `AppShell.css` `.navrow`/`.navrail-*` rail rows) |
| Icon library | `lucide-react` (already used app-wide; reuse — e.g. `Tags` or `Sparkles` for the review nav entry and the sweep affordance, `ListChecks` / `Inbox` for the queue list, `Check` on the batched apply mirroring `TagSuggest`, `ChevronRight` for a backlog-row open affordance, `AlertTriangle` for stale/error banners mirroring `DiffReviewDialog`) |
| Font | `--font-family-ui` (system-ui stack) for all chrome; `--font-family-mono` not used in this phase |
| New runtime dependency | **none.** No new npm or Go package. The sweep control reuses the `Admin.css` section pattern; the review route reuses the Phase-10 lazy-route shell + the Phase-11 `TagSuggestList`. |

**Rendering / state:** The sweep-start fires a `@tanstack/react-query` mutation (mirror `reindexGraphMut` in `Admin.tsx` — async 202, muted confirmation, no blocking). The review route fetches the pending-queue list via react-query (mirror `useBacklinks` / the `/graph` query) and per-page suggestions on demand; the batched apply is a mutation. Per-page review **reuses the existing `TagSuggestList`** (its local checked-set + captured base-revision contract is unchanged). **No `dangerouslySetInnerHTML`** anywhere — page titles, paths, and tag names render as React text children only (the locked stored-XSS guard, identical to `BacklinksPanel`'s `{b.title}` rule). The sweep control, the nav entry, and the review route are **admin-gated** (render only when `me.role === "admin"`, mirroring `AppShell.tsx`'s `isAdmin` and the `RequireRole(admin)` server subgroup — the server gate is the real boundary; this is convenience).

**Mount points:**
- **Sweep-start control** mounts as a new `<section className="admin-section">` in `Admin.tsx`, directly below the existing "Search" and "Graph" rebuild sections (a peer "Tagging" / "Tag suggestions" section). It is admin-only by virtue of living on the `/admin` route.
- **Review route** is a new `React.lazy` route `/app/tag-review` wrapped in `AppShell` (exactly like `/app/graph` / `GraphView`), reachable from a new **admin-visible nav row** in `.navrail-body` (beside the existing Graph and Trash rows), shown only when `isAdmin`.

---

## Spacing Scale

Declared values (must be multiples of 4) — **reused verbatim from `tokens.css`**:

| Token | Value | Usage in this phase |
|-------|-------|---------------------|
| `--space-xs` | 4px | Icon↔label gap on the nav row / sweep button / apply button, count↔progress gap, checkbox↔toggle-label gap on the "all pages" toggle |
| `--space-sm` | 8px | Sweep-section row gap (`.admin-section-row`), backlog-row vertical padding, footer button gap |
| `--space-md` | 16px | Sweep-section padding (matches `.admin-section`), review-route header padding, backlog body padding (mirrors `.agentpanel-body`) |
| `--space-lg` | 24px | Review-route empty/loading/error state padding, section spacing between backlog list and per-page panel |
| `--space-xl` | 32px | Per-page review surface max-height viewport inset (inherited from `TagSuggestList` → `.diff-dialog` `max-height: calc(100vh - var(--space-xl) * 2)`) |
| `--space-2xl` | 48px | Review-route empty-state vertical offset (matches `.agentpanel-empty` / `.admin-empty`) |
| `--space-3xl` | 64px | Page-level spacing on the review route, if needed |

Fixed interactive sizes (reused): `--hit-min-height` (40px — sweep button, "all pages" toggle, every backlog row, footer buttons), `--hit-min-icon` (44px — any icon-only variant). Layout: `--topbar-height` (56px), `--navrail-width` (the nav row lives in the existing rail). Radius: `--radius-sm` (6px) for backlog rows / cards / badges (matches `.role-badge` / `.agentpanel-suggestion`); the reused approval surface keeps its inherited radii.

Exceptions: none. All chrome uses the 4px scale; there is no canvas/numeric geometry in this phase.

---

## Typography

**Reused verbatim from `tokens.css`** (exactly 4 sizes, exactly 2 weights):

| Role | Size | Weight | Line Height | Usage in this phase |
|------|------|--------|-------------|---------------------|
| Display | `--font-size-display` 28px | `--font-weight-semibold` 600 | `--line-height-display` 1.2 | not used |
| Heading | `--font-size-heading` 20px | `--font-weight-semibold` 600 | `--line-height-heading` 1.2 | Review-route page title ("Tag review"); the sweep `.admin-section-heading` ("Tag suggestions") follows the existing `.admin-section-heading` size in `Admin.css` |
| Body | `--font-size-body` 16px | `--font-weight-regular` 400 | `--line-height-body` 1.5 | Backlog-row page title text; default button text (`.btn`); the inherited approval-row tag text |
| Label | `--font-size-label` 14px | 400 / 600 | `--line-height-label` 1.4 | Nav-entry label, sweep button label, the "all pages" toggle label, the sweep confirmation line, the backlog-row page path / pending-count, the queue progress line, AND all status lines (loading / empty / error). Mirrors `.admin-muted` / `.agentpanel-meta` / `.backlinks-status` |

The per-page stale banner heading uses `--font-size-label` at `--font-weight-semibold` 600 (inherited from `TagSuggestList` → `.diff-stale-heading`). No other font sizes or weights are introduced.

---

## Color

**60 / 30 / 10 split — reused from the established dark Obsidian palette in `tokens.css`:**

| Role | Value (token) | Usage |
|------|---------------|-------|
| Dominant (60%) | `--color-bg` `#0b0d12` | Review-route main pane + backlog body background; the inherited approval surface body |
| Secondary (30%) | `--color-surface` `#11151f` | Review-route header bar; backlog-row card fill; the sweep section follows the existing `.admin-section` treatment; the inherited approval surface header band; the pending-count badge fill (mirrors `.role-badge`) |
| Accent (10%) | `--color-accent` `#8b5cf6` | See reserved list below |
| Destructive | `--color-destructive` `#fb7185` | Not used — there are **NO destructive actions** in this phase. The sweep stages pending rows and writes nothing automatically; "reject"/skip discards a suggestion without deleting any page content; the batched apply only ADDS approved tags via the byte-stable path. (CONTEXT locks the safety model: no auto-write, ever.) |

**Accent (`--color-accent`) reserved for — explicit list (never "all interactive elements"):**
1. The **batched "Apply approved" button** — the single primary write CTA of the per-page review (`.btn .btn-primary`, accent gradient), inherited from `TagSuggestList`'s `Apply tags`. **It is NEVER auto-focused** (the trust-gate inversion is inherited verbatim — see Interaction Contract).
2. The **checked state** of an approval-row checkbox in the reused `TagSuggestList` (`accent-color: var(--color-accent)` — inherited).
3. The **review nav-entry active/selected** indication when on `/app/tag-review` (mirrors the existing nav active treatment `--color-tree-active-bg` + `navrail-row-active` + `aria-current`, exactly like the Graph row).
4. The **backlog-row hover/active** affordance — reuses the existing `.navrow`/row hover (`--color-hover`); the currently-open row in the backlog reads as selected via `--color-tree-active-bg` (same recipe as the nav active state).
5. `:focus-visible` rings on the sweep button, the "all pages" toggle, every backlog row, the nav entry, and the footer buttons (global `2px solid var(--color-accent)` from `tokens.css`).

The **sweep-start button** itself is a `.btn-secondary` (NOT accent) — it mirrors the "Rebuild graph index" / "Rebuild search index" buttons exactly (a background operational action, not a write CTA; consistency with the two existing admin rebuild controls is the contract). Accent is NOT used for the sweep button, the "all pages" toggle, the pending-count badge, backlog-row text, the progress line, or status/empty/error text.

### Semantic / secondary color usage

| Element | Color (token) | Notes |
|---------|---------------|-------|
| Pending-count badge (per backlog row) | text `--color-text-muted` `#8d96a8` on `--color-surface` fill + `--color-border` outline (= `.role-badge` recipe) | A neutral count chip (e.g. `5 pending`); count is conveyed by a number + word, never by color alone. |
| "new" badge inside the reused approval list | inherited from `TagSuggest.css` (`.tagsuggest-newbadge`) | NOT re-specified — see Phase 11. New tags also default UNCHECKED (the second non-color signal). |
| Sweep confirmation line ("started — N pages queued") | `--color-text-muted` `#8d96a8` at `--font-size-label`, `role="status"` | Quiet muted line, identical to `Admin.tsx`'s `"Graph index rebuild started."` confirmation. |
| Queue progress line ("N pages left to review") | `--color-text-muted` `#8d96a8` at `--font-size-label` | Quiet muted single line, mirrors `.backlinks-status`. |
| Status / empty / loading / error lines | `--color-text-muted` `#8d96a8` at `--font-size-label` | Quiet single lines, identical to `.backlinks-status` / `.admin-muted`. |
| In-progress / "still working" sweep state | `--color-text-muted` line (optionally with the existing `.spinner`) | The sweep is async/fire-and-forget like the rebuild jobs; the UI does not poll a "job" — it shows the backlog filling on the review route. No worker/job internals surfaced (hidden-Git rule). |
| Per-page stale-revision banner | warning icon `--color-warning` `#facc15` + heading `--color-text` + body `--color-text-muted` | Inherited from `TagSuggestList` → `.diff-stale` (warning, **never accent**, color paired with words + icon). |

No new color token is introduced this phase.

---

## Copywriting Contract

> **Hidden-Git / hidden-infra discipline (locked):** No user-facing copy may leak `job`, `queue`, `worker`, `drain`, `enqueue`, `KindTagSuggest`, `tag_suggestions`, `migration`, git, sqlite, bleve, index, or LLM-provider vocabulary. The user sees "suggestions", "pages to review", "the sweep is running in the background" — never the machinery. Errors are generic and actionable. Apostrophes/ampersands use the existing typographic entity convention (`&rsquo;` / `&amp;`) as in `TagSuggest.tsx` / `DiffReviewDialog.tsx`.

### Sweep-start control (admin section in `Admin.tsx`)

| Element | Copy |
|---------|------|
| Section heading | `Tag suggestions` |
| Section description (muted) | `Suggest tags across your pages. Suggestions wait for your review — nothing is tagged automatically.` |
| Scope toggle label (default state) | `Only untagged pages` (the locked default; checked-off = the cheaper backfill case) |
| Scope toggle label (alt) | `Include pages that already have tags` (the "all pages" opt-in — a single checkbox/switch; OFF by default per CONTEXT) |
| Sweep-start button (idle) | `Suggest tags for pages` |
| Sweep-start button (busy / submitting) | `Starting…` (mirrors the rebuild buttons' `Starting…`; button disabled while the start request is in flight) |
| Success confirmation (untagged scope) | `Started suggesting tags for untagged pages — {n} pages queued for review.` (muted `role="status"`) |
| Success confirmation (all scope) | `Started suggesting tags — {n} pages queued for review.` (muted `role="status"`) |
| Success confirmation — zero targets | `Every page already has tags — nothing to suggest.` (untagged scope found no targets; muted, no button error) |
| In-progress hint (optional, after start) | `This runs in the background. Review the suggestions on the Tag review page.` (muted; links to `/app/tag-review`) |
| Error state (start failed) | `Couldn&rsquo;t start the sweep. Try again.` (`.field-error role="alert"`, generic, hidden-infra-safe; mirrors `reindexError`) |

### Review route (`/app/tag-review`)

| Element | Copy |
|---------|------|
| Nav entry label | `Tag review` |
| Nav entry aria (active) | `aria-current="page"` when on `/app/tag-review` (inherited rail pattern) |
| Route page title | `Tag review` |
| Route subtitle (muted, optional) | `Pages with tag suggestions waiting for your approval.` |
| Backlog progress line | `{n} pages left to review` (e.g. `7 pages left to review`; muted Label; `aria-live="polite"` so it announces as the backlog drains) |
| Backlog row — primary text | the page title (React text child; falls back to the page path if no title) |
| Backlog row — secondary text (muted) | the page path |
| Backlog row — pending-count badge | `{n} pending` (e.g. `5 pending`; neutral `.role-badge`-style chip) |
| Backlog row — open affordance (aria) | `Review suggestions for {page title}` |
| Primary CTA (per-page write) | `Apply approved` (accent `.btn-primary` with the `Check` icon; inherited from `TagSuggestList`'s `Apply tags`; **NEVER auto-focused**) |
| Primary CTA busy label | `Applying…` (inherited) |
| Skip / dismiss this page (non-write) | `Skip for now` (`.btn-secondary`; leaves the page in the backlog, writes nothing) — this is the review-route analog of `Cancel`; it is DOM-first and the deliberate initial-focus target (the safe default) |
| Per-page select-all / clear | `Select all` / `Clear all` (inherited from `TagSuggestList` — not re-specified) |
| Per-page count line | `{n} selected` (inherited) |
| Empty state heading (no pending suggestions) | `No pending suggestions` |
| Empty state body | `Run a tag-suggestion sweep from the admin Settings page, then approved suggestions show up here.` |
| Empty state after a page is applied/skipped to zero | `You&rsquo;re all caught up — no more pages to review.` (muted) |
| Loading state (fetching the backlog) | `Loading pages to review…` (muted, with the existing `.spinner`) |
| Loading state (fetching one page&rsquo;s suggestions) | `Loading suggestions…` (muted single line within the per-page panel) |
| Error state (backlog load failed) | `Couldn&rsquo;t load the review queue. Refresh to try again.` (generic, hidden-infra-safe; mirrors `BacklinksPanel`) |
| Error state (apply failed, non-stale) | `Couldn&rsquo;t apply the tags. Try again.` (inherited from `TagSuggestList`) |
| Per-page stale-revision heading | `This page changed since the tags were suggested.` (inherited from `TagSuggestList` → `.diff-stale-heading`) |
| Per-page stale-revision body | `Someone edited the page while it was waiting for review. Re-run to get fresh suggestions.` (mirrors `.diff-stale-body`; wording adjusted for the queue context) |
| Per-page stale-revision actions | `Skip` (`.btn-secondary`) + `Re-run` (`.btn-primary`) — the Apply path is REMOVED in this state (inherited from `TagSuggestList`'s stale footer) |
| Destructive confirmation | **None** — there are NO destructive actions. Skip leaves a page pending; "reject"/clear unchecks a suggestion without deleting any content; apply only ADDS tags via the byte-stable path. Nothing is written until an explicit Apply, and nothing is ever deleted by this surface. |

> Copy discipline: every status / empty / error / loading / progress line is a quiet muted single line (`--color-text-muted` + `--font-size-label`), matching the Phase-9 `BacklinksPanel`, the Phase-10 graph contract, and the `Admin.tsx` rebuild confirmations. Error copy carries **zero** job / queue / worker / git / sqlite / bleve / index / model-provider vocabulary (the hidden-infra invariant).

---

## Component / Chrome Inventory (executor reference)

| Component | Pattern to clone | Token notes |
|-----------|------------------|-------------|
| Sweep-start section (`Admin.tsx`) | Clone the existing **"Graph" rebuild section** verbatim: `<section className="admin-section">` + `.admin-section-heading` + `.admin-muted` description + `.admin-section-row` (button + muted `role="status"` confirmation) + `.field-error role="alert"` for errors | Button is `.btn .btn-secondary` (NOT accent); `"Starting…"` + `disabled` while the start mutation is in flight; mutation mirrors `reindexGraphMut` (async 202, muted confirmation) |
| Scope toggle ("only untagged" vs "all") | A single `<label>`-wrapped `<input type="checkbox">` (or a small switch) above/beside the sweep button; default UNCHECKED = "only untagged pages" | Full control meets `--hit-min-height`; label at `--font-size-label`; `accent-color: var(--color-accent)` on the checkbox (the one accent use on this control); not a destructive control |
| `/app/tag-review` route + `TagReviewView.tsx` | New `React.lazy` `<Route>` in `App.tsx` wrapped in `AppShell` (clone the `GraphView` route wiring); full-pane component inside `.mainpane`; header bar uses `--color-surface` + `--space-md` like the graph route header | Admin-gated render (return null / redirect for non-admins; server is the real gate) |
| Nav entry (left rail) | Add a `navrow navrail-trash-row`-style button in `.navrail-body` beside the Graph + Trash rows (same `.navrow` + `.tree-icon` + `.tree-label` classes, lucide `Tags`/`ListChecks` icon), gated behind `isAdmin` | Active state when `location.pathname === "/app/tag-review"` → `navrail-row-active` + `--color-tree-active-bg` + `aria-current="page"` (exact Graph-row recipe) |
| Backlog list | A vertical list of selectable rows; each row clones the `.navrow`/`BacklinksPanel`-row recipe: page title (Body), muted page path (Label), a `.role-badge`-style `{n} pending` chip, optional `ChevronRight` open affordance; full row is the hit target at `--hit-min-height`; hover `--color-hover`; the open row reads selected via `--color-tree-active-bg` | Titles/paths as React text children — NEVER `dangerouslySetInnerHTML`; rows navigable by click + keyboard |
| Pending-count badge | Clone `.role-badge` exactly (`--color-surface` fill, `--color-border` outline, `--font-size-label`, `--radius-sm`, muted text) | Word + number (`5 pending`); never color-only |
| Per-page review surface | **REUSE `TagSuggestList` from `TagSuggest.tsx` unchanged** — its per-tag rows, new-vs-existing badge, new-default-unchecked, select-all/clear, count line, trust-gate focus inversion (Skip/Cancel DOM-first + focused; Apply accent + NEVER auto-focused; Esc + backdrop = dismiss), focus trap, and stale/empty/error states are all inherited | The ONLY adaptations: the dismiss button reads `Skip for now` (leaves the page in the backlog), the primary CTA reads `Apply approved`, and on success the row leaves the backlog + the progress line decrements. **Do NOT re-implement the row internals.** |
| Progress line | A muted `--font-size-label` line above or below the per-page surface: `{n} pages left to review`, `aria-live="polite"` | `--color-text-muted`; decrements as pages are applied or skipped-to-resolution |
| Empty / loading / error states | Clone `.admin-empty` (heading + muted body) for the route-level empty state; `.agentpanel-loading` (spinner + muted line) for loading; the `BacklinksPanel` error line for errors | `--color-text-muted`, `--font-size-label`; centered with `--space-2xl` vertical offset for the empty state |

---

## Interaction Contract (maps to TAG-05, TAG-06)

| # | Requirement | Contract |
|---|-------------|----------|
| TAG-05 | Admin bulk sweep → enqueues per-page suggestions → produces a pending queue, writing nothing automatically | The **sweep-start control** lives in an admin-only `Admin.tsx` section. The **scope toggle defaults to "only untagged pages"** (the locked default); checking "include pages that already have tags" sweeps all pages. Clicking **`Suggest tags for pages`** fires an async start mutation (mirrors `reindexGraphMut` — returns immediately, 202), shows `Starting…` while in flight, then a muted `role="status"` confirmation — **`Started… — {n} pages queued for review.`** — or **`Every page already has tags — nothing to suggest.`** when the untagged scope finds no targets. The control surfaces **no** job/queue/worker internals (hidden-infra rule). The sweep **writes nothing** — it only produces pending suggestions that appear on the review route as the background work completes. |
| TAG-06 (review) | A user reviews the queue and approves/rejects per page | The **`/app/tag-review`** route (admin-gated, reachable from the admin-visible nav entry) lists every page with pending suggestions as a backlog of rows (title + muted path + `{n} pending` chip). Selecting a row opens that page&rsquo;s suggestions **one page at a time** in the **reused `TagSuggestList`** surface — per-tag checkbox rows, the `new` badge, **new tags default UNCHECKED**, select-all/clear, and a live `{n} selected` count (all inherited from Phase 11). **Rejecting** is per-tag (uncheck) or whole-page (`Skip for now`, which leaves the page in the backlog and writes nothing). A muted `aria-live` **progress line** (`{n} pages left to review`) tracks the backlog draining. |
| TAG-06 (apply) | Approvals route through the same byte-stable apply path, committed in batches | The per-page primary CTA is **`Apply approved`** — the single accent action, **NEVER auto-focused** (the trust-gate inversion is inherited verbatim: `Skip for now` is DOM-first and the deliberate initial-focus target so a reflexive Enter cannot write; Esc + backdrop dismiss without applying). Apply sends the checked tags + the page&rsquo;s captured base revision through the **Phase-11 byte-stable apply path** (the server re-validates/normalizes and commits in **batches**, not one commit per page — backend-owned). On success the page leaves the backlog and the progress line decrements. The UI never assumes its checked list is authoritative (server re-validates). |
| TAG-06 (resumable / stale) | The sweep is resumable and never bypasses approval; a per-page stale base-revision is surfaced | The review route reads from the **persisted pending queue**, so reloading the page / returning later shows the same backlog (resumability is visible: the queue survives navigation and worker restarts — no client state required). The load-bearing safety guarantee is honored in the UI by having **no auto-apply affordance anywhere** — every write is an explicit per-page `Apply approved`. When a page changed since its suggestion was staged (a **409 stale base-revision**), the reused surface switches to the inherited **stale state** (warning banner, Apply path removed, `Skip` / `Re-run`) — that one page is handled individually without sinking the rest of the backlog, mirroring the per-page 409-floor-without-failing-the-batch contract. |

**Accessibility / motion:** The reused `TagSuggestList` brings its full focus trap + Esc + restore-focus-on-close + `aria-live` count (inherited — not re-specified). Backlog rows are keyboard-navigable buttons/links with visible `:focus-visible` accent rings; the open row is marked selected (`aria-current` / `aria-selected` as appropriate). The progress line uses `aria-live="polite"`. Every interactive element (sweep button, scope toggle, nav entry, backlog row, footer buttons) meets `--hit-min-height` (40px) / `--hit-min-icon` (44px). The pending-count badge and "new" badge convey meaning with words + numbers, never color alone. Under `prefers-reduced-motion: reduce`, any spinner stops animating (inherited rule). No `dangerouslySetInnerHTML` on any title, path, or tag.

---

## Registry Safety

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none (project does not use shadcn) | not applicable |
| third-party registries | none | not applicable |

No shadcn registry is in use and **no new dependency is introduced** this phase — the sweep control reuses the `Admin.css` section pattern, the review route reuses the Phase-10 lazy-route shell + nav-entry pattern, and per-page review reuses the Phase-11 `TagSuggestList`. Registry vetting gate: not applicable.

---

## Reused Tokens (explicit inventory for the checker)

Spacing: `--space-xs`, `--space-sm`, `--space-md`, `--space-lg`, `--space-xl`, `--space-2xl`, `--space-3xl` · sizes `--hit-min-height`, `--hit-min-icon` · layout `--topbar-height`, `--navrail-width` · radius `--radius-sm`.
Typography: `--font-size-heading`, `--font-size-body`, `--font-size-label`; `--font-weight-regular`, `--font-weight-semibold`; `--line-height-heading`, `--line-height-body`, `--line-height-label`; `--font-family-ui`.
Color: `--color-bg`, `--color-surface`, `--color-text`, `--color-text-muted`, `--color-border`, `--color-hover`, `--color-tree-active-bg`, `--color-accent`, `--color-accent-fg`, `--color-warning`.
Inherited (Phase 11, not re-specified here): `--color-accent-soft`, `--color-accent-border` (TagSuggestList trigger/checked states), `.tagsuggest-*` classes, `.diff-stale*` classes.
**New tokens added this phase: none.**

---

## Checker Sign-Off

- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS

**Approval:** pending
