diff --git a/docs/UI-REDESIGN-PLAN.md b/docs/UI-REDESIGN-PLAN.md
new file mode 100644
index 0000000..a34ad00
--- /dev/null
+++ b/docs/UI-REDESIGN-PLAN.md
@@ -0,0 +1,573 @@
+# UI Redesign — Implementation Plan
+
+*Branch: `ui-redesign` | Last updated: 2026-04-21*
+
+---
+
+## Overview
+
+Replace the basic Phase 6 single-page app (`web/static/`) with the enterprise-grade
+migration console designed in `docs/ui-mockup/mockup.html`.
+
+The backend is complete (Phases 8–13, 108/108 tests passing). All new UI phases are
+**frontend-only** unless noted. Existing FastAPI routes do not change except where
+noted under Phase 16 (readiness data) and Phase 19 (Verification API).
+
+### Design reference
+
+Open `docs/ui-mockup/mockup.html` in a browser to see all 8 screens before starting.
+
+### Docusign 2024 brand tokens
+
+| Token | Value | Usage |
+|---|---|---|
+| Cobalt | `#4C00FF` | Primary CTA, active nav highlight |
+| Inkwell | `#130032` | Left nav background |
+| Ecru | `#F8F3F0` | Page background |
+| Poppy | `#FF5252` | Error / Blocked badge |
+| Slate | `#6B6B9A` | Secondary text, muted labels |
+| White | `#FFFFFF` | Card surfaces |
+
+Typography: `Inter` (Google Fonts), fallback `-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`.
+
+---
+
+## Current state
+
+`web/static/` — three files, ~600 lines total:
+- `index.html` — 79 lines, single-page layout (header, two panels, history table)
+- `app.js` — 343 lines, vanilla JS (auth, template list, migrate, history)
+- `style.css` — 186 lines, basic styles, non-Docusign colours
+
+---
+
+## File structure after redesign
+
+Keep no-build-step approach (vanilla JS ES modules, no bundler). Split monolith into
+logical files served statically by FastAPI.
+
+```
+web/static/
+ index.html # app shell (nav, router outlet, modals)
+ css/
+ tokens.css # CSS custom properties (brand colours, spacing)
+ base.css # reset, typography, utility classes
+ nav.css # left sidebar nav + top bar
+ cards.css # template cards, readiness badges
+ modals.css # dialog / modal styles
+ tables.css # history and audit tables
+ forms.css # settings form inputs
+ js/
+ state.js # global app state (project, auth, templates)
+ router.js # hash-based client-side router
+ api.js # thin fetch wrappers for all backend endpoints
+ auth.js # auth status, connect/disconnect, Adobe dialog
+ project.js # project switcher modal, project CRUD (localStorage)
+ templates.js # template list view, readiness badges, filters
+ migration.js # options modal, progress polling, results view
+ verification.js # send test envelope, poll status
+ history.js # history & audit view
+ settings.js # settings screen
+ utils.js # escHtml, formatDate, debounce, etc.
+```
+
+`app.js` and `style.css` are **deleted** (replaced by the above).
+`index.html` is **rewritten** as the app shell.
+
+---
+
+## Phase 14 — App Shell & Navigation
+
+**Goal:** Branded shell that all other views live inside. No functional logic yet —
+just the frame, router, and state container.
+
+### index.html structure
+
+```html
+
+
+
+
+ …
+
+
+ …
+
+```
+
+### css/tokens.css
+
+```css
+:root {
+ --cobalt: #4C00FF;
+ --inkwell: #130032;
+ --ecru: #F8F3F0;
+ --poppy: #FF5252;
+ --slate: #6B6B9A;
+ --white: #FFFFFF;
+ --success: #28A745;
+ --warning: #F0A500;
+ --border: #E0DCF8;
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --shadow-sm: 0 1px 4px rgba(0,0,0,0.08);
+ --shadow-md: 0 4px 16px rgba(0,0,0,0.12);
+}
+```
+
+### js/router.js
+
+```js
+const ROUTES = {
+ '#/dashboard': () => import('./templates.js').then(m => m.renderDashboard()),
+ '#/templates': () => import('./templates.js').then(m => m.renderTemplates()),
+ '#/results': () => import('./migration.js').then(m => m.renderResults()),
+ '#/issues': () => import('./issues.js').then(m => m.renderIssues()),
+ '#/verify': () => import('./verification.js').then(m => m.renderVerification()),
+ '#/history': () => import('./history.js').then(m => m.renderHistory()),
+ '#/settings': () => import('./settings.js').then(m => m.renderSettings()),
+};
+// Default route: #/templates
+```
+
+### js/state.js
+
+```js
+export const state = {
+ project: null, // { id, name } — loaded from localStorage
+ auth: { adobe: false, docusign: false },
+ templates: [], // array from /api/templates/status
+ selectedIds: new Set(),
+ lastMigrationResults: null, // results from most recent batch job
+};
+// Simple pub/sub: subscribe(key, fn) / publish(key)
+```
+
+### js/api.js — endpoint wrappers
+
+All existing endpoints wrapped:
+```js
+export const api = {
+ auth: {
+ status: () => GET('/api/auth/status'),
+ connectAdobe: () => POST('/api/auth/adobe/connect'),
+ connectDocusign: () => POST('/api/auth/docusign/connect'),
+ exchangeAdobe: (url) => POST('/api/auth/adobe/exchange', { redirect_url: url }),
+ disconnect: (p) => POST(`/api/auth/${p}/disconnect`),
+ },
+ templates: {
+ status: () => GET('/api/templates/status'),
+ adobe: () => GET('/api/templates/adobe'),
+ docusign: () => GET('/api/templates/docusign'),
+ },
+ migrate: {
+ run: (body) => POST('/api/migrate', body),
+ batch: (body) => POST('/api/migrate/batch', body),
+ batchStatus: (id) => GET(`/api/migrate/batch/${id}`),
+ history: () => GET('/api/migrate/history'),
+ },
+};
+```
+
+### js/utils.js
+
+```js
+export const escHtml = str => String(str).replace(/[&<>"]/g, c => …);
+export const formatDate = iso => new Date(iso).toLocaleDateString(…);
+export const formatRelative = iso => …;
+export const debounce = (fn, ms) => { … };
+export const uuid = () => crypto.randomUUID();
+```
+
+### Commit
+
+`feat(ui-phase-14): app shell — nav, router, state, brand tokens`
+
+---
+
+## Phase 15 — Project / Customer Context
+
+**Goal:** Project switcher so the same installation can manage migrations for
+multiple customers without mixing history or credentials.
+
+### Data model (localStorage only — no backend)
+
+```js
+// Stored in localStorage key: 'migrator_projects'
+{
+ active: "uuid-1",
+ projects: [
+ { id: "uuid-1", name: "Acme Corp", createdAt: "2026-04-21T…" },
+ { id: "uuid-2", name: "Globex Inc", createdAt: "2026-04-22T…" },
+ ]
+}
+```
+
+Credentials remain in the server-side signed cookie session. Switching projects
+triggers a fresh `/api/auth/status` check (session may still be valid if the user
+didn't disconnect).
+
+### js/project.js
+
+```js
+export function listProjects() { … } // returns projects array
+export function createProject(name) { … } // generates uuid, saves, returns project
+export function deleteProject(id) { … }
+export function getActive() { … } // returns active project or null
+export function setActive(id) { … } // updates localStorage + triggers nav refresh
+```
+
+### Project switcher modal
+
+- Opened by clicking project name in nav footer
+- Lists projects: name + creation date + "Activate" button
+- "New Project" inline form (name field + Create button)
+- Deleting a project requires confirmation ("Delete Acme Corp? This cannot be undone.")
+- First run: modal opens automatically with welcome copy
+
+### Nav footer display
+
+Shows `▸ Acme Corp` (truncated to 18 chars). Clicking opens switcher modal.
+No project → shows `▸ New Project` in amber.
+
+### Commit
+
+`feat(ui-phase-15): project switcher — localStorage CRUD, switcher modal`
+
+---
+
+## Phase 16 — Templates View with Readiness Badges
+
+**Goal:** Replace the two-panel list with a filterable, sortable single table.
+Each row shows a readiness badge computed from validation results.
+
+### Readiness badge system
+
+| Badge | Colour | Condition |
+|---|---|---|
+| Ready | `--success` green | `blockers=[]`, `warnings=[]` |
+| Caveats | `--warning` amber | `blockers=[]`, `warnings.length > 0` |
+| Blocked | `--poppy` red | `blockers.length > 0` |
+| Migrated | `--cobalt` | `status=migrated` and no blockers |
+| Needs Update | `--warning` amber | `status=needs_update` |
+| Verified | green + ✓ | post-migration verification passed (Phase 19) |
+
+### Backend update required: `web/routers/templates.py`
+
+Add `blockers: list[str]` and `warnings: list[str]` to each template object in
+`GET /api/templates/status`. Run `validate_template()` on the normalized form if the
+template has been downloaded; otherwise return empty lists.
+
+```python
+# In templates.py status endpoint, for each adobe template:
+normalized_dir = Path(settings.downloads_dir) / f"{template['name']}__{template['id']}"
+if normalized_dir.exists():
+ normalized = adobe_folder_to_normalized(str(normalized_dir))
+ result = validate_template(normalized)
+ blockers = result.blockers
+ warnings = result.warnings
+else:
+ blockers, warnings = [], []
+```
+
+Add 3 backend tests to `tests/test_api_templates.py`:
+- `test_status_includes_blockers_and_warnings_fields`
+- `test_status_blockers_populated_when_template_downloaded`
+- `test_status_empty_when_not_downloaded`
+
+### js/templates.js
+
+```js
+export function renderTemplates() {
+ // Fetches state.templates (or refreshes via api.templates.status())
+ // Renders filterable table into #router-outlet
+ // Columns: ☐ | Name | Readiness | Fields | Last Modified | DS Status | Actions
+ // Filter bar: search input + status dropdown + readiness dropdown
+ // Bulk toolbar (hidden until ≥1 selected): "Migrate X selected" button
+}
+
+export function renderTemplateDetail(adobeId) {
+ // 4-tab layout: Overview | Fields | Issues | Migration History
+}
+```
+
+### Template detail view (`#/templates/:id`)
+
+- **Overview tab:** name, description, roles, document count, last modified date
+- **Fields tab:** table of fields — type, label, page, role, required, conditional
+- **Issues tab:** blockers (red cards) + warnings (amber cards) from validation
+- **Migration History tab:** records from `/api/migrate/history` filtered to this template
+
+### Commit
+
+`feat(ui-phase-16): templates view — readiness badges, filters, detail tabs, backend blockers/warnings`
+
+---
+
+## Phase 17 — Migration Workflow UI
+
+**Goal:** Options modal → progress view → results view as a cohesive flow.
+
+### Flow
+
+```
+Templates view → select ≥1 template → "Migrate Selected" button →
+ Options modal → "Run Migration" →
+ Progress view (replaces modal) →
+ Results view (#/results)
+```
+
+### js/migration.js
+
+```js
+export function showOptionsModal(selectedIds) {
+ // Renders modal with:
+ // - Dry run toggle (default: off)
+ // - Overwrite existing toggle (default: off, from settings)
+ // - Include documents toggle (default: on, from settings)
+ // - Target folder text input (optional)
+ // - Selected count display
+ // - "Run Migration" button
+}
+
+export async function runMigration(ids, options) {
+ // Calls POST /api/migrate/batch
+ // Returns job_id
+}
+
+export async function pollJob(jobId, onProgress, onComplete) {
+ // Polls GET /api/migrate/batch/{jobId} every 2s
+ // Calls onProgress({ completed, total, results })
+ // Calls onComplete(finalResults) when status === 'done'
+}
+
+export function renderResults(jobResults) {
+ // Navigates to #/results and renders:
+ // - Summary row: X Created | Y Updated | Z Skipped | W Blocked | V Errors
+ // - Per-template result table
+ // - "Verify Templates" button (pre-loads migrated IDs)
+ // - "Back to Templates" button
+ // - "Export CSV" button (client-side Blob download)
+}
+```
+
+### Progress view (inline, inside modal)
+
+After "Run Migration" is clicked:
+- Modal content replaces with: progress bar + per-template status list
+- Each template row: name → ⏳ spinning → ✅ success or ❌ error
+- "View Results" button appears when job status === 'done'
+
+### Commit
+
+`feat(ui-phase-17): migration workflow — options modal, progress polling, results view`
+
+---
+
+## Phase 18 — Issues & Warnings View
+
+**Goal:** A dedicated screen to review all validation problems before migrating.
+
+### js/issues.js
+
+```js
+export function renderIssues() {
+ // Reads state.templates (already has blockers/warnings from Phase 16)
+ // Renders two sections:
+ // BLOCKERS — templates that will fail migration
+ // WARNINGS — templates that will migrate with caveats
+ // Each item: template name | issue message | suggested action link
+ // "Migrate Anyway" button on warning items → showOptionsModal([id])
+ // "View Template" link → #/templates/:id
+}
+```
+
+### Nav badge
+
+Left nav Issues link shows a red badge with count of blocked templates.
+Updates whenever `state.templates` changes.
+
+### Commit
+
+`feat(ui-phase-18): issues view — blocked and warning templates, nav badge`
+
+---
+
+## Phase 19 — Verification View
+
+**Goal:** Send test envelopes to confirm migrated templates work end-to-end.
+
+### New backend: `web/routers/verify.py`
+
+```python
+POST /api/verify/send
+ body: { template_id: str, recipient_name: str, recipient_email: str }
+ action: GET /v2.1/accounts/{id}/envelopes (create via template)
+ returns: { envelope_id: str }
+
+GET /api/verify/status/{envelope_id}
+ action: GET /v2.1/accounts/{id}/envelopes/{envelopeId}
+ returns: { status: str, completed_at: str | null }
+
+POST /api/verify/void/{envelope_id}
+ body: { reason: str }
+ action: PUT envelope status to "voided"
+ returns: { voided: true }
+```
+
+Register router in `web/app.py`: `app.include_router(verify_router, prefix="/api/verify")`.
+
+### tests/test_api_verify.py
+
+Four tests (all mock DocuSign calls with respx):
+- `test_send_requires_auth`
+- `test_send_returns_envelope_id`
+- `test_status_returns_envelope_state`
+- `test_void_calls_docusign`
+
+### js/verification.js
+
+```js
+export function renderVerification(preloadedTemplateIds = []) {
+ // Shows list of migrated templates (from history or passed-in IDs)
+ // Per-template row:
+ // - Template name + DS template ID
+ // - "Send Test Envelope" button → opens send dialog
+ // - Status chip (Not Tested | Sent | Delivered | Completed = Verified | Voided)
+ // Send dialog: recipient name + email (pre-filled from settings), "Send" button
+ // After send: row updates with status, "Void" button, polling every 5s
+}
+```
+
+### Commit
+
+`feat(ui-phase-19): verification view + verify API endpoints (send/status/void)`
+
+---
+
+## Phase 20 — History & Audit View
+
+**Goal:** Filterable, exportable migration history.
+
+### js/history.js
+
+```js
+export function renderHistory() {
+ // Calls GET /api/migrate/history
+ // Renders:
+ // - Filter bar: date range, template name search, status filter
+ // - Table: timestamp | template | action | status | DS ID | warnings | checksum
+ // - Expandable row: full blockers/warnings list, field count diff
+ // - "Export CSV" button (client-side)
+}
+```
+
+SHA-256 checksum: first 8 chars displayed, full value in title attribute (tooltip).
+
+### Commit
+
+`feat(ui-phase-20): history & audit view — filters, export, checksum display`
+
+---
+
+## Phase 21 — Settings View
+
+**Goal:** Central config screen for verification defaults and migration defaults.
+
+### Settings (localStorage key: `migrator_settings`)
+
+| Key | Default | UI control |
+|---|---|---|
+| `testRecipientName` | `""` | Text input |
+| `testRecipientEmail` | `""` | Email input |
+| `autoVoidHours` | `24` | Number input |
+| `defaultOverwrite` | `false` | Toggle |
+| `defaultIncludeDocs` | `true` | Toggle |
+
+### js/settings.js
+
+```js
+export function renderSettings() {
+ // 3 sections:
+ // 1. Verification defaults (name, email, auto-void timer)
+ // 2. Migration defaults (overwrite, include documents)
+ // 3. Connection info (read-only: connected accounts, base URLs)
+ // Save button writes to localStorage
+ // Values pre-loaded into options modal (Phase 17) and send dialog (Phase 19)
+}
+```
+
+### Commit
+
+`feat(ui-phase-21): settings view — verification defaults, migration defaults`
+
+---
+
+## Phase 22 — Smoke Test Checklist & Cleanup
+
+**Goal:** Validate the full redesigned UI works end-to-end, update docs.
+
+### tests/UI-SMOKE-TEST.md
+
+Manual checklist:
+- [ ] First run: project switcher opens automatically
+- [ ] Create project "Test Customer", verify it appears in nav footer
+- [ ] Connect Adobe Sign via `.env` path → badge turns green
+- [ ] Connect DocuSign via JWT path → badge turns green
+- [ ] Templates view loads ≥1 template with correct readiness badge
+- [ ] Select 2 templates → options modal opens → dry run → results show `dry_run` status
+- [ ] Select 2 templates → real migration → progress bar counts up → results view
+- [ ] Navigate to Verification → Send Test → status updates to Completed
+- [ ] History view shows all migrations with correct counts and checksums
+- [ ] Issues view shows blocked templates (use a fixture template with no recipients)
+- [ ] Settings: save test recipient → reopen Settings → values persist
+
+### Final tasks
+
+- Run `pytest tests/ -v` — confirm all tests still pass (≥108 + new verify tests)
+- Update `README.md` — new UI navigation guide section
+- Update `docs/agent-harness/EXECUTION-BOARD.md` — Phases 14–22 complete
+- Push `ui-redesign` branch to Gitea
+- Open PR to `master`
+
+### Commit
+
+`feat(ui-phase-22): smoke test checklist, README update, final cleanup`
+
+---
+
+## Dependency order
+
+```
+Phase 14 (Shell)
+ └── Phase 15 (Project)
+ └── Phase 16 (Templates + backend readiness data)
+ ├── Phase 17 (Migration workflow)
+ │ └── Phase 18 (Issues view)
+ └── Phase 19 (Verification + verify API)
+
+Phase 20 (History) ← depends on Phase 14 only, can run after Phase 14
+Phase 21 (Settings) ← depends on Phase 14 only, can run after Phase 14
+
+Phase 22 (Cleanup) ← depends on all phases complete
+```
+
+Phases 20 and 21 can be implemented in parallel with Phases 17–19.
+
+---
+
+## What does NOT change
+
+- All existing FastAPI routes (`auth.py`, `templates.py`, `migrate.py`)
+- All backend Python source (`src/`)
+- All 108 existing tests
+- `.env` / credential handling
+- The CLI pipeline (`src/migrate_template.py`)
+
+Only backend additions:
+1. **Phase 16:** `blockers` + `warnings` fields added to `GET /api/templates/status`
+2. **Phase 19:** New `web/routers/verify.py` with 3 envelope endpoints
diff --git a/docs/agent-harness/EXECUTION-BOARD.md b/docs/agent-harness/EXECUTION-BOARD.md
index 851fbbc..0281f00 100644
--- a/docs/agent-harness/EXECUTION-BOARD.md
+++ b/docs/agent-harness/EXECUTION-BOARD.md
@@ -149,10 +149,74 @@
---
+## UI Redesign — Phases 14–22 (in progress)
+
+*Full plan: `docs/UI-REDESIGN-PLAN.md`*
+
+### Phase 14 — App Shell & Navigation
+- [ ] Rewrite `index.html` as app shell (left nav, router outlet, top bar)
+- [ ] `css/tokens.css` — Docusign 2024 brand custom properties
+- [ ] `css/base.css` — reset, Inter font, utility classes
+- [ ] `css/nav.css` — Inkwell sidebar, logo, nav links, project footer
+- [ ] `js/utils.js` — escHtml, formatDate, debounce, uuid
+- [ ] `js/router.js` — hash-based router (#/templates default)
+- [ ] `js/state.js` — global state with pub/sub
+- [ ] `js/api.js` — fetch wrappers for all existing endpoints
+
+### Phase 15 — Project / Customer Context
+- [ ] `js/project.js` — project CRUD (localStorage)
+- [ ] Project switcher modal (list, create, delete, activate)
+- [ ] First-run experience (auto-open modal if no projects)
+- [ ] Active project name in nav footer
+
+### Phase 16 — Templates View with Readiness Badges
+- [ ] Backend: add `blockers[]` + `warnings[]` to `GET /api/templates/status`
+- [ ] Add 3 backend tests to `tests/test_api_templates.py`
+- [ ] `js/templates.js` — filterable/sortable table with readiness badges
+- [ ] Template detail view (4 tabs: Overview, Fields, Issues, Migration History)
+- [ ] `css/cards.css` — badge styles, table hover, bulk toolbar
+
+### Phase 17 — Migration Workflow UI
+- [ ] Options modal (dry_run, overwrite, include_documents, target folder)
+- [ ] Progress view with batch job polling (every 2s)
+- [ ] `js/migration.js` — showOptionsModal, runMigration, pollJob, renderResults
+- [ ] Results view (#/results) with summary + export CSV
+- [ ] `css/modals.css`
+
+### Phase 18 — Issues & Warnings View
+- [ ] `js/issues.js` — issues view (Blockers + Warnings sections)
+- [ ] Nav badge showing blocked template count
+
+### Phase 19 — Verification View + API
+- [ ] `web/routers/verify.py` — POST /send, GET /status/{id}, POST /void/{id}
+- [ ] Register verify router in `web/app.py`
+- [ ] `tests/test_api_verify.py` — 4 tests
+- [ ] `js/verification.js` — send test envelope, poll status, void
+
+### Phase 20 — History & Audit View
+- [ ] `js/history.js` — filterable history table, expand row, export CSV
+- [ ] Checksum display (first 8 chars, full on hover)
+
+### Phase 21 — Settings View
+- [ ] `js/settings.js` — 3 sections (verification defaults, migration defaults, connection info)
+- [ ] `css/forms.css`
+
+### Phase 22 — Smoke Test Checklist & Cleanup
+- [ ] `tests/UI-SMOKE-TEST.md` — manual test checklist (11 steps)
+- [ ] Full backend test suite passes (≥108 + verify tests)
+- [ ] Update `README.md` — new UI navigation guide
+- [ ] Update EXECUTION-BOARD.md — all phases complete
+- [ ] Push `ui-redesign` branch to Gitea
+- [ ] Open PR to `master`
+
+---
+
## Gitea
- [x] Committed and pushed all changes (2026-04-17)
-- [ ] Commit and push Phase 8–13 work (ui-redesign branch)
+- [x] Committed Phase 8–13 work (ui-redesign branch, 2026-04-21)
+- [x] Committed UI mockup + Docusign 2024 brand (ui-redesign branch, 2026-04-21)
+- [ ] Push Phases 14–22 UI implementation (ui-redesign branch)
---
@@ -164,3 +228,5 @@
- (2026-04-17) v2 planning complete — idempotent upload + web UI implementation begins
- (2026-04-21) Blueprint comparison complete — added normalized schema, validation service, migration options, rate-limit/retry, security hardening, and batch migration phases (Phases 8–13)
- (2026-04-21) Phases 8–13 fully implemented — 108/108 tests passing on ui-redesign branch
+- (2026-04-21) Enterprise UI mockup designed — 8 screens, Docusign 2024 branding, official SVG logo embedded
+- (2026-04-21) UI Redesign plan written (Phases 14–22) — frontend-only except Phase 16 (readiness data) and Phase 19 (verify API)