diff --git a/.planning/phases/19-template-editor-ui/19-UI-SPEC.md b/.planning/phases/19-template-editor-ui/19-UI-SPEC.md
new file mode 100644
index 0000000..b99e990
--- /dev/null
+++ b/.planning/phases/19-template-editor-ui/19-UI-SPEC.md
@@ -0,0 +1,249 @@
+---
+phase: 19
+slug: template-editor-ui
+status: draft
+shadcn_initialized: false
+preset: none
+created: 2026-04-06
+---
+
+# Phase 19 — UI Design Contract
+
+> Visual and interaction contract for the template editor UI phase. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
+
+---
+
+## Design System
+
+| Property | Value |
+|----------|-------|
+| Tool | none — manual inline styles + Tailwind utilities |
+| Preset | not applicable |
+| Component library | none (Radix not installed; components are custom) |
+| Icon library | none (text/Unicode symbols used inline, e.g. `+`, `✓`, `×`) |
+| Font (sans) | Geist Sans via `var(--font-sans)` / `var(--font-geist-sans)` |
+| Font (serif/display) | Cormorant Garamond via `var(--font-serif)` — wordmark only |
+
+Source: `globals.css`, `PortalNav.tsx`, `layout.tsx`
+
+**Note:** `components.json` was not found. The project does not use shadcn. All new components follow the existing inline-style + selective Tailwind pattern established in PreparePanel, ClientsPageClient, and PortalNav.
+
+---
+
+## Spacing Scale
+
+Declared values (multiples of 4 only):
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| xs | 4px | Icon gaps, dot indicator margin |
+| sm | 8px | Inline element spacing, button padding vertical |
+| md | 16px | Panel internal padding, form field gaps |
+| lg | 24px | Section spacing within panels, list gaps |
+| xl | 32px | Page-level top margin |
+| 2xl | 48px | Empty-state vertical padding |
+| 3xl | 64px | Nav height (established: PortalNav is 64px) |
+
+Exceptions:
+- Role color dot: 8px diameter (not a layout token — cosmetic)
+- Signer/role pill height: 32px (matches PreparePanel signer pill pattern)
+- Right panel (TemplatePanel) width: 280px fixed — matches PreparePanel column width in DocumentPageClient split layout
+- AI Auto-place button and Save button minimum height: 36px (matches existing PreparePanel button height)
+
+Source: `PreparePanel.tsx`, `PortalNav.tsx`, `layout.tsx` (2rem = 32px page padding)
+
+---
+
+## Typography
+
+| Role | Size | Weight | Line Height |
+|------|------|--------|-------------|
+| Body | 14px (0.875rem) | 400 | 1.5 |
+| Label / caption | 12px (0.75rem) | 400 | 1.4 |
+| Section heading | 14px (0.875rem) | 600 | 1.3 |
+| Page heading | 24px (1.5rem) | 700 | 1.2 |
+
+Source: `ClientsPageClient.tsx` (h1: 1.5rem/700), `PreparePanel.tsx` (h2: font-semibold/text-gray-900, body: 0.875rem/400), `PortalNav.tsx` (links: 0.8125rem/500 uppercase)
+
+Weights used: regular (400) and semibold/bold (600/700). No third weight.
+
+Letter-spacing exceptions:
+- Nav links: `letter-spacing: 0.08em; text-transform: uppercase` — preserved exactly, no change
+- Wordmark: `letter-spacing: 0.02em` — no change
+
+---
+
+## Color
+
+| Role | Value | Usage |
+|------|-------|-------|
+| Dominant (60%) | `#FAF9F7` (cream) | Page background, panel backgrounds, modal backgrounds |
+| Secondary (30%) | `#F9FAFB` / `#F0EDE8` (cream-mid) | Panel interior fill, list row backgrounds, right panel |
+| Accent (10%) | `#C9A84C` (gold) | Primary CTA buttons only, active nav underline |
+| Navy | `#1B2B4B` | Page headings, nav background, secondary button fill |
+| Destructive | `#DC2626` (red-600) | Delete role action button only |
+
+Accent (`#C9A84C`) reserved for:
+- "Save Template" button (primary CTA)
+- "Add Role" button (secondary CTA within TemplatePanel)
+- Active nav link underline (existing pattern, unchanged)
+
+Navy (`#1B2B4B`) used for:
+- "AI Auto-place" button fill (matches PreparePanel AI button pattern)
+- Page heading text
+- Wordmark
+
+Semantic colors:
+- Success inline: `#059669` (green — "Saved" confirmation inline text, same as PreparePanel "Sent ✓")
+- Error inline: `#DC2626` (red — error messages)
+- Muted text: `#6B7280` (gray-500 — counts, timestamps, placeholder copy)
+- Border default: `#E5E7EB` (gray-200)
+
+Source: `globals.css`, `PreparePanel.tsx`, `ClientsPageClient.tsx`, `ConfirmDialog.tsx`
+
+---
+
+## Component Inventory
+
+Components this phase must produce (from CONTEXT.md D-12/D-13):
+
+### 1. TemplatePanel (new)
+Right panel, 280px wide, sticky within the editor view.
+
+Sections (top to bottom):
+1. **Template name** — editable inline `` (14px, border-bottom-only style when focused, gray-200 border at rest). Displays current name from DB.
+2. **Roles section** — heading "Signers / Roles" (12px uppercase label), list of role pills, "Add role" button.
+3. **Role pill** — colored dot (8px, `background: role.color`) + role label string + rename icon (pencil, text "Edit") + remove button (`×`). Free-form rename via inline edit on click.
+4. **Add role** — text input + "Add" button. Preset suggestions: "Buyer", "Co-Buyer", "Seller", "Co-Seller" as clickable chips below the input.
+5. **AI Auto-place button** — full-width, navy fill (`#1B2B4B`), white text, 36px height. Label: "AI Auto-place Fields". Shows spinner + "Placing…" during loading.
+6. **Save button** — full-width, gold fill (`#C9A84C`), white text, 36px height. Label: "Save Template". Shows "Saving…" during loading. Shows inline "Saved" (green) or error message after.
+
+### 2. TemplatePageClient (new)
+State owner. Layout: CSS flexbox row — left: `PdfViewerWrapper` + `FieldPlacer` (flex: 1, min-width: 0), right: `TemplatePanel` (280px, flex-shrink: 0). Gap: 24px. Pattern mirrors `DocumentPageClient`.
+
+### 3. Templates list page (new)
+- Page heading: "Templates" (24px/700, navy)
+- Subtitle: "{N} template{s}" (14px/400, gray-500)
+- "New Template" button: top-right, gold fill (primary CTA). Label: "+ New Template"
+- Table/list rows: each row shows template name (14px/600, navy), form name (14px/400, gray-500), field count ("N fields", 12px/400, gray-500), last updated (12px/400, gray-500). Click row → navigate to editor.
+- Row hover: `background: #F0EDE8` (cream-mid), cursor pointer.
+
+### 4. PortalNav update
+Add "Templates" link between "Clients" and "Profile" in `navLinks` array. No visual change — same style as existing links.
+
+### 5. FieldPlacer update (non-breaking)
+Add optional `onPersist?: (fields: SignatureFieldData[]) => Promise | void` prop. When provided, replaces the 4 internal `persistFields(docId, fields)` call sites. Existing callers unaffected.
+
+---
+
+## Interaction States
+
+### AI Auto-place button
+| State | Visual |
+|-------|--------|
+| Idle | Navy fill, "AI Auto-place Fields", full opacity |
+| Loading | Navy fill, "Placing…" + spinner (CSS border-radius spin), disabled |
+| Success | Button returns to idle; FieldPlacer reloads via key increment |
+| Error | Inline error message below button in red-600 text (14px), button returns to idle |
+
+### Save button
+| State | Visual |
+|-------|--------|
+| Idle | Gold fill, "Save Template", full opacity |
+| Loading | Gold fill at 0.7 opacity, "Saving…", disabled |
+| Success | Inline "Saved" text in green (#059669) below button, fades after 3s |
+| Error | Inline error in red below button |
+
+### Role pill inline rename
+- Click role label → label becomes a text input (same font, border-bottom style)
+- Enter or blur → commits rename, calls PATCH /api/templates/[id] with updated `signatureFields` role strings
+- Escape → cancels rename, reverts to original label
+
+### Remove role
+- Click `×` on role pill
+- If no fields are assigned to that role: remove immediately
+- If fields are assigned to that role: show ConfirmDialog — "Remove role?" / "Removing 'Buyer' will unassign {N} field(s). This cannot be undone." / Confirm: "Remove Role" (red-600), Cancel: "Cancel"
+
+### New Template modal (from list page)
+- Reuses AddDocumentModal pattern: overlay, white rounded card, form fields.
+- Fields: "Template name" (text input), "Select form" (PDF form picker from library).
+- CTA: "Create Template" (gold fill). On success: navigate to `/portal/templates/[id]`.
+
+---
+
+## Layout
+
+### Template editor page (`/portal/templates/[id]`)
+```
+[PortalNav — 64px sticky top]
+[main — max-width 1200px, margin: 0 auto, padding: 32px]
+ [page header — flex row]
+ [h1 "Edit Template: {name}"] [field count badge "{N} fields placed" — optional, muted]
+ [editor body — flex row, gap: 24px]
+ [left: PdfViewerWrapper + FieldPlacer — flex:1]
+ [right: TemplatePanel — width: 280px, flex-shrink: 0]
+```
+
+### Templates list page (`/portal/templates`)
+```
+[PortalNav — 64px sticky top]
+[main — max-width 1200px, margin: 0 auto, padding: 32px]
+ [page header — flex row space-between]
+ [h1 "Templates"] [subtitle "{N} templates"] [CTA "+ New Template"]
+ [list — flex column, gap: 8px]
+ [template row × N]
+```
+
+---
+
+## Copywriting Contract
+
+| Element | Copy |
+|---------|------|
+| Primary CTA (list page) | "+ New Template" |
+| Primary CTA (editor) | "Save Template" |
+| Secondary CTA (editor) | "AI Auto-place Fields" |
+| Nav link | "Templates" |
+| Page heading (list) | "Templates" |
+| Page heading (editor) | "Edit Template: {name}" |
+| Empty state heading (list) | "No templates yet" |
+| Empty state body (list) | "Create a template to reuse field placements across documents." |
+| Empty state CTA (list) | "+ Create your first template" |
+| Error — AI auto-place | "AI placement failed. Check that the form PDF is accessible and try again." |
+| Error — save | "Save failed. Please try again." |
+| Loading — AI | "Placing…" |
+| Loading — save | "Saving…" |
+| Success — save | "Saved" |
+| Panel section label — roles | "Signers / Roles" |
+| Role add placeholder | "Role label (e.g. Buyer)" |
+| Role remove confirm title | "Remove role?" |
+| Role remove confirm body | "Removing '{role}' will unassign {N} field(s). This cannot be undone." |
+| Role remove confirm CTA | "Remove Role" |
+| Role remove cancel | "Cancel" |
+| Field count badge | "{N} field{s} placed" |
+
+Source: CONTEXT.md decisions D-04, D-05, D-12, D-13; modeled on PreparePanel copy patterns.
+
+---
+
+## Registry Safety
+
+| Registry | Blocks Used | Safety Gate |
+|----------|-------------|-------------|
+| shadcn official | none — shadcn not installed | not applicable |
+| Third-party | none declared | not applicable |
+
+No third-party registry blocks in scope for this phase.
+
+---
+
+## 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