diff --git a/.agents/plans/design-system.md b/.agents/plans/design-system.md
new file mode 100644
index 00000000..deac1f6a
--- /dev/null
+++ b/.agents/plans/design-system.md
@@ -0,0 +1,421 @@
+Activate the /implement-task skill first.
+
+# Plan: Design system — tokens, primitives, and high-traffic migrations
+
+## Acceptance Criteria
+
+Sourced from the Citadel scratchpad topic (no formal ticket): "Build out reusable components / tokens so the cockpit looks/feels like a professional UI app." Reformulated as checkable items, grounded in `specs/B.2-ade-cockpit.md` and `specs/B.8-ui-performance-quality.md`:
+
+- [ ] A single canonical token layer (`apps/web/src/design-system/tokens.css`) defines all color, typography, elevation, radius, spacing, and motion tokens used by the cockpit, in both light and dark themes.
+- [ ] `apps/web/src/styles.css` no longer inlines token definitions; it imports the canonical token file. Legacy `--color-*`, `--panel*`, `--line*`, `--muted`, `--surface*`, `--topbar-*` aliases continue to resolve so existing CSS keeps rendering unchanged.
+- [ ] A reusable React primitive library lives under `apps/web/src/components/ui/` with at least these primitives implemented and tested: `Button` (extended), `Badge` (extended), `Card`, `Panel` (+ header/body/footer subcomponents), `Input`, `Textarea`, `Select`, `Label`, `FormField`, `HelpText`, `Tabs` (+ list/trigger/content), `Dialog` (+ header/footer/title/description), `Tooltip`, `Chip`, `IconButton`, `EmptyState`, `Skeleton`, `Toast` (region + hook).
+- [ ] Every primitive is keyboard-accessible (focus-visible ring on token), supports light/dark theming via tokens (no hard-coded hex colours in component files), and exposes the `className` escape hatch via `cn(...)`.
+- [x] Four high-traffic surfaces are touched by primitives in the same PR to prove they work. Two are fully migrated, two are partial (scope narrowed during implementation — see notes):
+ - [x] `modals.tsx` — all centered modals use `Dialog` (Modal scaffolding rewritten over Radix Dialog).
+ - [x] Inspector tab strip in `inspector.tsx` uses `Tabs` (extracted into `inspector-tabs.tsx`; `data-active={tab}` preserved on the TabsList wrapper for the inspector-deploy.css contract).
+ - [~] Scheduled-agent form (`scheduled-agent-form.tsx`) — **only the Name field** migrated to `FormField + Input`. Twelve other fields (schedule type, cron preset, cron, when/at, repo, runtime, run mode, background cwd, workspace strategy, existing workspace, base branch, overlap policy, description, prompt, enabled) keep their bespoke `` shape. Deferred because each carries conditional rendering + custom validation timing that needs case-by-case translation; full migration is a follow-up PR. New `scheduled-agent-form.test.tsx` locks in the FormField contract for the migrated field.
+ - [~] Workspace-card status pills — **only the namespace pill** migrated to `Chip`. The approval-pill (icon-only, transparent fill, tonal icon colour) and the workspace-card-diff (two-tone +/- counter) remain bespoke; flagged with an in-code `TODO(implement-task)` comment. They don't map cleanly onto Badge/Chip's surface-fill conventions and need a design pass before rewriting.
+- [ ] A dev-only route `/design-system` renders every primitive's variants in both themes. Hidden behind `import.meta.env.DEV` — not bundled into production builds.
+- [ ] Spec **B.8 #4** ("shadcn-style components are used where they improve consistency and speed") is checked off and the spec's bullet annotated `[~]` or `[x]` with a line linking back to this design system layer.
+- [ ] `make check` and `make e2e` pass.
+- [ ] An existing visual regression in the migrated surfaces is not introduced — Playwright `theme-audit.spec.ts` continues to pass.
+
+## Context and problem statement
+
+The cockpit's visual layer today is ~8,700 lines of bespoke CSS spread across 19 files (`styles.css`, `chrome.css`, `cockpit-extras.css`, `inspector-*`, `settings-*`, `scheduled-agents*`, etc.) and only two React primitives (`Button`, `Badge`) under `apps/web/src/components/ui/`. The token layer already exists in `styles.css` (warm-cream / dark themes with `--c-*` semantic tokens plus `--color-*` legacy aliases) but:
+
+1. Tokens live inline in `styles.css` with no separate "design system" home and no inventory document.
+2. Higher-level patterns — modals, tabs, panels, form fields, tooltips, chips, empty states, skeletons — are reimplemented per screen in bespoke CSS, drifting in spacing, radius, and density.
+3. `packages/ui` exists as a stub (single string export) but has no React deps and no consumers; it's reserved for v2 packaging, not the current target.
+4. `apps/web/src/components/ui/button.tsx` and `badge.tsx` use `class-variance-authority` (CVA) + Tailwind + `cn()` — that's the right foundation but the library stops there.
+
+The work is to **extend the existing primitive layer to cover the patterns the cockpit actually uses repeatedly**, migrate the highest-traffic surfaces to prove the primitives are sufficient, and leave a documented `/design-system` route for visual review. This satisfies the operator-facing AC in **B.2 #7** ("dark-blue v1-inspired palette… dense, no marketing hero areas"), **B.8 #3** ("calm, dense, premium, operational"), and **B.8 #4** ("shadcn-style components are used where they improve consistency and speed").
+
+## Spec alignment
+
+- `specs/B.2-ade-cockpit.md` — no behavioral change to cockpit ACs. The plan preserves all current cockpit semantics (three-column shell, no page scroll, modal centering, compact pill tab strip). Migration to primitives keeps visual treatment within token-defined ranges.
+- `specs/B.8-ui-performance-quality.md` — directly serves AC **#4** ("shadcn-style components are used where they improve consistency and speed") and supports **#3**, **#6**, **#8**, **#12**. The implementation step "Spec annotations" updates B.8 #4 from `[ ]` to `[~]` (partial — first wave of primitives shipped; full surface migration deferred), and adds a one-line reference to `apps/web/src/design-system/README.md`.
+- `specs/C-technical-stack.md` — three new dependencies (`@radix-ui/react-dialog`, `@radix-ui/react-tooltip`, `@radix-ui/react-tabs`) are added to `apps/web/package.json`. These are first-party Radix packages, same maintainer as the already-installed `@radix-ui/react-slot`. The plan's Implementation Steps include the lockfile review and the post-install lifecycle audit per MEMORY (`feedback_codex_broken` is unrelated; the relevant guidance is the standard "treat pnpm-lock.yaml as security-sensitive" rule from `.agents/skills/extensions/review-pr.md`).
+
+No spec file gets a *content* update beyond the B.8 #4 annotation; the design system is delivery against existing ACs, not new behavior.
+
+## Implementation approach
+
+The chosen strategy is **extend the existing CVA + Tailwind + token primitive layer** under `apps/web/src/components/ui/`, with tokens consolidated into a single imported `tokens.css` and primitive coverage expanded to the patterns the cockpit actually repeats. Migration of bespoke screens happens screen-by-screen, with **four high-traffic surfaces migrated in the same PR** to prove the primitives are sufficient. Remaining surfaces remain on their bespoke CSS until follow-up PRs.
+
+Rationale:
+
+- The codebase already standardised on CVA + Tailwind 4 + `cn()` (`apps/web/src/components/ui/button.tsx`, `badge.tsx`, `apps/web/src/lib/utils.ts`). Continuing on the same foundation avoids a rewrite tax and keeps the bundle small.
+- Radix headless primitives (`@radix-ui/react-dialog`, `@radix-ui/react-tooltip`, `@radix-ui/react-tabs`) give us accessibility (focus trap, Escape handling, ARIA wiring) without owning the behavior. Same vendor as `@radix-ui/react-slot` which is already installed.
+- Tokens stay in `apps/web/src/design-system/tokens.css` (not `packages/ui`) because (a) `packages/ui` is currently a stub, has no React deps, and migrating there would expand the PR scope and (b) cockpit-specific tokens (dark-blue v1-inspired palette per B.2 #7) are not yet a shared design surface — there is no second consumer.
+- Migrating four surfaces in the same PR proves the primitives cover real use cases (rather than landing a library that nothing uses, then discovering gaps in follow-ups). The four were chosen because they collectively exercise every new primitive type: `Dialog` (modals.tsx), `Tabs` (inspector), `FormField` family (scheduled-agent-form), `Badge`/`Chip` (workspace-card pills).
+
+## Alternatives considered
+
+**Alternative A — move primitives into `packages/ui` now.** Rejected. The package is currently a stub with no React peer-dep, no test setup, and no consumer beyond `apps/web`. Promoting it now would (a) inflate this PR with packaging work, (b) require adjustments to `pnpm` workspace dependency rules and the `check:arch` boundary script, and (c) deliver no immediate benefit because there is no second consumer. Leave `packages/ui` for a future "extract" PR once a second consumer (e.g. `apps/cli` web shell, or a hosted variant) actually exists.
+
+**Alternative B — adopt `shadcn/ui` CLI and let it scaffold the components.** Rejected. shadcn copies components into the repo; we'd still own them. The bookkeeping cost of running the CLI plus matching its directory conventions outweighs the benefit when we already have CVA + `cn()` patterns established. We can borrow shadcn's ergonomics (CVA variants, Radix headless, `cn()` merge) without the tooling. Spec **B.8 #4** says "shadcn-style," not "shadcn the package."
+
+**Alternative C — build all primitives but migrate zero screens, deferring migration to follow-up PRs.** Rejected. Without a real consumer in the same PR, primitive APIs drift from real usage and the PR delivers an unproven library. The "four migrations in the same PR" rule keeps the design honest and surfaces API gaps before merge.
+
+**Alternative D — convert everything in one mega-PR.** Rejected. 8,700 lines of bespoke CSS across 19 files cannot be reviewed in one diff; the file-size and review-burden costs are high. Phased delivery (this PR + follow-ups documented in the PR description) is the responsible choice.
+
+## Implementation steps
+
+### Spec annotations
+- Annotate `specs/B.8-ui-performance-quality.md` line **UI Quality #4** from `[ ]` to `[~]` with a trailing note: "First wave of primitives shipped under `apps/web/src/components/ui/`; see `apps/web/src/design-system/README.md`. Remaining surfaces deferred to follow-up PRs."
+
+### Token consolidation
+
+**Step 1 — enumerate the current token surface (must run before consolidation):**
+```bash
+grep -rohE "var\(--[a-z0-9-]+\)" apps/web/src --include="*.css" --include="*.tsx" \
+ | sed -E 's/^var\(//; s/\)$//' \
+ | sort -u \
+ > apps/web/src/design-system/tokens.inventory.txt
+```
+Commit this file as the canonical inventory. There are ~51 unique tokens at HEAD; the consolidation must preserve every one of them (a missing alias silently breaks unmigrated CSS).
+
+**Step 2 — collapse the `prefers-color-scheme` blocks into explicit `data-theme` blocks.**
+
+`apps/web/src/styles.css` today has two `@media (prefers-color-scheme: light)` blocks (lines 152 and 199) that define tokens *only* visible when the OS theme is light and no explicit `data-theme` is set. The consolidated `tokens.css` must:
+
+- Define every token in **both** `:root` (light default) and `:root[data-theme="dark"]` (dark explicit) blocks.
+- Move any token that lives **only** inside a `prefers-color-scheme` block into the explicit blocks so every token is reachable without OS-theme simulation.
+- Retain a single `@media (prefers-color-scheme: dark)` block scoped to `:root:not([data-theme="light"]):not([data-theme="dark"])` that re-points to the dark token values — this preserves OS-driven auto-theming for users who haven't picked a theme.
+
+**Step 3 — write `apps/web/src/design-system/tokens.css`** containing the unified `:root` (light) and `:root[data-theme="dark"]` declarations covering every token in `tokens.inventory.txt` (both `--c-*` semantic tokens and legacy `--color-*`/`--panel*`/`--line*`/`--muted`/`--surface*`/`--topbar-*`/`--overlay-bg`/`--shadow-color` aliases) plus the auto-theming media block above.
+
+**Step 4 — edit `apps/web/src/styles.css`** to replace the inlined token blocks (current lines ~1–200) with `@import "./design-system/tokens.css";`. Keep all non-token rules (resets, keyframes, structural classes) in `styles.css`.
+
+**Step 5 — write `apps/web/src/design-system/README.md`** (≤120 lines): inventory of every token, semantic intent, "how to add a new token" rule, and an explicit note about the happy-dom coverage gap for `prefers-color-scheme` (mitigated because Step 2 moves all tokens out of those blocks).
+
+**Step 6 — write `apps/web/src/design-system/index.ts`** re-exporting every primitive so consumers can `import { Button, Dialog, Tabs } from "./design-system"`.
+
+### Primitive: Button (extend)
+- Edit `apps/web/src/components/ui/button.tsx`:
+ - Add variants: `destructive` (uses `--color-danger`), `link` (no chrome, underline on hover).
+ - Add `loading?: boolean` — renders an inline spinner (reuse the `@keyframes spin` in `styles.css`) and sets `disabled` while loading. Preserves children for layout stability.
+ - Add sizes: keep `default` (`min-h-9`), `icon` (`h-9 w-9`); add `sm` (`min-h-7 px-2 text-xs`) and `lg` (`min-h-10 px-4`).
+ - Existing `default`/`secondary`/`ghost` variants stay unchanged.
+
+### Primitive: Badge (extend)
+- Edit `apps/web/src/components/ui/badge.tsx`:
+ - Add variants: `info` (`--color-info`), `warn` (`--color-warning`), `merged` (`--color-merged`), `neutral-strong` (heavier neutral fill).
+ - Existing `neutral`/`ready`/`blocked` variants stay unchanged.
+ - Add optional `dot?: boolean` prop — renders a 6px circle prefix in the same color tone.
+
+### Primitive: Card / Panel
+- Create `apps/web/src/components/ui/card.tsx` exporting `Card` (rounded surface using `--c-card` bg, `--c-line-2` border, `--sh-card` shadow).
+- Create `apps/web/src/components/ui/panel.tsx` exporting `Panel`, `PanelHeader`, `PanelBody`, `PanelFooter`. Header uses `--c-elev`, compact uppercase-spaced label slot (matches B.8 #12: "small uppercase panel titles").
+
+### Primitive: Form field family
+- Create `apps/web/src/components/ui/input.tsx` (`Input`, `Textarea`) — native elements styled via CVA. Tokens: `--c-surface` bg, `--c-line-2` border, focus ring `--color-action`.
+- Create `apps/web/src/components/ui/select.tsx` (`Select` — native `