diff --git a/.planning/phases/20-apply-template-and-portal-nav/20-RESEARCH.md b/.planning/phases/20-apply-template-and-portal-nav/20-RESEARCH.md
new file mode 100644
index 0000000..e91b6ca
--- /dev/null
+++ b/.planning/phases/20-apply-template-and-portal-nav/20-RESEARCH.md
@@ -0,0 +1,573 @@
+# Phase 20: Apply Template and Portal Nav - Research
+
+**Researched:** 2026-04-06
+**Domain:** Next.js App Router, Drizzle ORM, React client-state wiring, JSONB field snapshot copy
+**Confidence:** HIGH
+
+---
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+
+**AddDocumentModal Redesign**
+- D-01: Modal gets two tabs: "Forms Library" (existing, unchanged) and "My Templates" (new). Tab switching is client-side state — no page reload.
+- D-02: "My Templates" fetches from `GET /api/templates`. Each row shows: template name, form name, field count. Agent clicks a row to select it.
+- D-03: No second "role mapping" step in the modal. Agent picks template → clicks "Add Document" → document created immediately. Role assignment happens in PreparePanel (existing flow).
+- D-04: "Forms Library" tab, "Or upload a custom PDF" section, and all existing modal behavior remain completely unchanged. Pure additive change to `AddDocumentModal.tsx`.
+
+**Apply Template Operation (API)**
+- D-05: `POST /api/documents` accepts a new optional body field `documentTemplateId: string`. When present, server reads template's `signatureFields`, copies with new UUIDs, stores as new document's fields.
+- D-06: Role-to-email mapping: server reads role labels from copied fields' `signerEmail` values. Builds `signers` array: role[0] → `client.email`, role[1] → `client.contacts[0]?.email` (if present), role[2+] → left as-is (role label string). Happens server-side during document creation.
+- D-07: Every `SignatureFieldData` object copied from template gets `id: crypto.randomUUID()`. No template field ID may appear in the new document's fields.
+- D-08: Created document's `formTemplateId` set to the template's `formTemplateId`. Document is NOT linked to `documentTemplate` record after creation — snapshot, not live reference.
+
+**Text Hints as Quick-Fill**
+- D-09: Field `hint` values copied verbatim from `documentTemplates.signatureFields[].hint` into new document's fields.
+- D-10: In PreparePanel, when agent selects a text field with a `hint`, it appears as a clickable suggestion chip in the existing Quick Fill suggestions area. Clicking fills the field with hint text.
+- D-11: Hints are NOT shown as placeholder text on the field — only in PreparePanel suggestions area.
+- D-12: PreparePanel already has `textFillData` system and Quick Fill area. Hint chip is added to that same section — no new UI region.
+
+**Navigation (Already Delivered — No Work Needed)**
+- Templates nav link: DONE in Phase 19
+- `/portal/templates` list page: DONE in Phase 19
+- Phase 20 does NOT touch nav or templates list page.
+
+### Claude's Discretion
+- Exact tab styling in AddDocumentModal (pill tabs vs underline tabs)
+- Empty state for "My Templates" tab when no templates exist
+- How field count is displayed in the template picker row
+- Order of roles when more than 2 are present in the template
+
+### Deferred Ideas (OUT OF SCOPE)
+- Template versioning (already addressed by D-08 snapshot approach)
+- Bulk-apply template to multiple clients at once
+- Template categories or tags for organizing the My Templates list
+
+
+---
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|------------------|
+| TMPL-10 | Agent can choose "Start from template" when adding a document | D-01/D-02: "My Templates" tab in AddDocumentModal; API in AddDocumentModal.handleSubmit |
+| TMPL-11 | Applying template creates document with all template fields pre-loaded (fresh IDs, positions, types, role assignments) | D-05/D-07: POST /api/documents copies `signatureFields` with `crypto.randomUUID()` per field |
+| TMPL-12 | Template signer roles auto-mapped to client contacts in order; agent can override before sending | D-06: server maps roles to `client.email` + `contacts[]`; PreparePanel signer list is the override surface |
+| TMPL-13 | Text hints from template appear as quick-fill suggestions in PreparePanel | D-09/D-10/D-12: hints copied into document fields; PreparePanel surfaces as chip next to Client Name/Address |
+| TMPL-14 | Editing a template after documents created from it does NOT change those documents | D-08: snapshot copy at creation — no FK link from document back to documentTemplate |
+| TMPL-15 | "Templates" in portal nav | Already done Phase 19 — no work needed |
+| TMPL-16 | Templates list page shows all templates with form name, field count, last-updated date | Already done Phase 19 — no work needed |
+
+
+---
+
+## Summary
+
+Phase 20 is the final wiring of the template system into the document creation flow. The infrastructure is fully in place (Phase 18: schema + API, Phase 19: editor + field saving). This phase adds three incremental features: a second tab in `AddDocumentModal`, an extension to `POST /api/documents`, and a hint chip in `PreparePanel`.
+
+The code surface is small and well-bounded. All state management patterns (tabs as `useState`, Quick Fill chips, selectedFieldId threading) are established. The only new data flow is `documentTemplateId` on the POST body causing the server to fetch the template, copy fields with fresh UUIDs, and set `signers` from role-to-email mapping.
+
+**TMPL-14 and TMPL-15/16 are already satisfied.** TMPL-14 is satisfied by the snapshot design (D-08) — no new code. TMPL-15 and TMPL-16 were completed in Phase 19 and need no changes.
+
+**Primary recommendation:** Three focused changes — AddDocumentModal tab UI, POST /api/documents template branch, PreparePanel hint chip — each independent and non-breaking.
+
+---
+
+## Standard Stack
+
+### Core (already installed — no new dependencies)
+
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| React `useState` | (React 19) | Tab switching, template selection state | Existing modal pattern |
+| Next.js App Router `fetch` | (Next.js 16.2) | `GET /api/templates` from modal | Existing pattern in AddDocumentModal |
+| Drizzle ORM | (existing) | DB read of `documentTemplates` + `clients.contacts` in POST handler | All DB calls use Drizzle in this project |
+| `crypto.randomUUID()` | Node built-in | Fresh UUID per copied field | Already used in `documents` route |
+
+### No new packages required
+
+The phase requires zero new npm dependencies. All libraries are already installed and in use.
+
+---
+
+## Architecture Patterns
+
+### Pattern 1: Two-Tab Modal (additive to AddDocumentModal)
+
+**What:** Add a `activeTab: 'forms' | 'templates'` state variable. Render tab buttons at modal top. Conditionally render either the existing forms list (completely unchanged) or the new My Templates list. All existing submit logic is preserved — only the selection state changes.
+
+**Key insight from reading the code:** `AddDocumentModal` currently has two submission paths (custom file vs form template), determined by which of `selectedTemplate` (form library) or `customFile` is set. The template tab adds a third selection type: `selectedDocumentTemplate`. When `selectedDocumentTemplate` is set, the JSON POST branch sends `documentTemplateId` instead of `formTemplateId`.
+
+**State shape needed:**
+```typescript
+// New state
+const [activeTab, setActiveTab] = useState<'forms' | 'templates'>('forms');
+const [documentTemplates, setDocumentTemplates] = useState([]);
+const [selectedDocumentTemplate, setSelectedDocumentTemplate] = useState(null);
+
+// DocumentTemplateRow type (from GET /api/templates response)
+type DocumentTemplateRow = {
+ id: string;
+ name: string;
+ formName: string | null;
+ fieldCount: number;
+ updatedAt: string;
+};
+```
+
+**Fetch:** The `GET /api/templates` endpoint already returns `id, name, formName, fieldCount, updatedAt`. The modal can call it once on mount (alongside the existing forms-library fetch) or lazily on first tab click. Lazy on first click avoids an unnecessary request when agent never uses My Templates.
+
+**Submit path for template:**
+```typescript
+// Inside handleSubmit — new third branch:
+if (selectedDocumentTemplate) {
+ await fetch('/api/documents', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ clientId,
+ name: docName.trim(),
+ documentTemplateId: selectedDocumentTemplate.id,
+ }),
+ });
+}
+```
+
+**Validation:** The submit button's disabled condition must include the new path:
+```
+disabled={saving || (!selectedTemplate && !customFile && !selectedDocumentTemplate) || !docName.trim()}
+```
+
+**Selection mutual exclusivity:** When agent picks a form-library template, clear `selectedDocumentTemplate`. When agent picks a document template, clear `selectedTemplate` and `customFile`. Tab switching itself does NOT clear selections — so switching back preserves prior selection.
+
+**ClientId availability:** `AddDocumentModal` receives `clientId` as a prop. The `client` object itself (with `contacts`) is NOT passed into `AddDocumentModal` — it stays in `ClientProfileClient`. The role-to-email mapping is done server-side in `POST /api/documents` by fetching the client record from DB.
+
+### Pattern 2: POST /api/documents — Template Branch
+
+**What:** Extend the existing JSON-body path in `POST /api/documents` to handle `documentTemplateId`. This is a new branch inside the existing `else` block (JSON content-type).
+
+**Existing flow (JSON path):** reads `formTemplateId`, looks up `formTemplates` to get `filename`, copies PDF file, inserts document with `signatureFields: null`.
+
+**New flow (template path):**
+1. Parse `documentTemplateId` from body alongside existing fields
+2. When `documentTemplateId` is present:
+ a. Fetch `documentTemplates` record with `formTemplateId` (to get `filename` from joined `formTemplates`) and `signatureFields`
+ b. Copy PDF from seeds dir using the form template's `filename` (same as existing path)
+ c. Copy `signatureFields` with fresh UUIDs: `template.signatureFields.map(f => ({ ...f, id: crypto.randomUUID() }))`
+ d. Extract unique role labels (order of first appearance) from copied fields' `signerEmail` values
+ e. Build `signers` array for the document: `roles[0] → client.email`, `roles[1] → contacts[0]?.email ?? roles[1]`, `roles[2+] → roles[n]` (kept as-is)
+ f. Fetch client record to get `email` and `contacts`
+ g. Set `formTemplateId` from the document template's `formTemplateId`
+ h. Insert document with `signatureFields: copiedFields`, `signers: mappedSigners`, `formTemplateId`
+
+**Critical: what "roles" means.** Template fields carry role labels (e.g., "Buyer", "Seller") in their `signerEmail` slot. These are NOT real emails. The role-to-email mapping replaces these strings with real emails in the `signers` array on the document record. The `signatureFields` themselves keep the role labels in `signerEmail` — the `signers` array is where real emails live. Verify this matches Phase 16's PreparePanel signer resolution pattern.
+
+**Client fetch:** The `POST /api/documents` handler currently does NOT fetch the client record. Adding `documentTemplateId` support requires a new DB query: `db.query.clients.findFirst({ where: eq(clients.id, clientId) })`.
+
+**Field ID guarantee:** `crypto.randomUUID()` is Node built-in — already imported implicitly (used on `documents.id` via Drizzle schema defaults). Explicitly call it per field in the map.
+
+**Code structure (pseudo):**
+```typescript
+// New branch inside the JSON else block:
+let signatureFields: SignatureFieldData[] | null = null;
+let mappedSigners: DocumentSigner[] | null = null;
+
+if (documentTemplateId) {
+ const docTemplate = await db.query.documentTemplates.findFirst({
+ where: eq(documentTemplates.id, documentTemplateId),
+ with: { formTemplate: true },
+ });
+ if (!docTemplate) return Response.json({ error: 'Document template not found' }, { status: 404 });
+
+ // Copy PDF using the associated form template's filename
+ const srcPath = path.join(SEEDS_DIR, docTemplate.formTemplate.filename);
+ await copyFile(srcPath, destPath);
+
+ // Copy fields with fresh UUIDs — hints are preserved verbatim (D-09)
+ const rawFields = docTemplate.signatureFields ?? [];
+ signatureFields = rawFields.map(f => ({ ...f, id: crypto.randomUUID() }));
+
+ // Build signers from role-to-email mapping (D-06)
+ const client = await db.query.clients.findFirst({ where: eq(clients.id, clientId) });
+ const uniqueRoles = [...new Set(
+ signatureFields.map(f => f.signerEmail).filter(Boolean)
+ )] as string[];
+ const contacts = [client?.email, ...(client?.contacts ?? []).map(c => c.email)];
+ mappedSigners = uniqueRoles.map((role, i) => ({
+ email: contacts[i] ?? role, // fallback to role label if no contact at that index
+ color: SIGNER_COLORS[i % SIGNER_COLORS.length],
+ }));
+
+ formTemplateId = docTemplate.formTemplateId; // override for DB insert
+} else if (formTemplateId) {
+ // existing form-library path unchanged
+}
+```
+
+**SIGNER_COLORS in API route:** `PreparePanel` and `DocumentPageClient` both define `SIGNER_COLORS = ['#6366f1', '#f43f5e', '#10b981', '#f59e0b']`. The API route needs the same palette to assign consistent colors. Define the same constant in the route or extract to a shared util. Recommendation: inline the constant in the route (same value, avoids client/server boundary import complexity).
+
+### Pattern 3: PreparePanel Hint Chip
+
+**What:** When `selectedFieldId` is set, look up the field's `hint` in the fields list. If present, render a chip in the Quick Fill section alongside existing suggestions.
+
+**Data access challenge:** PreparePanel currently receives `selectedFieldId` and `textFillData` but does NOT receive the list of fields — it doesn't know the hints. There are two approaches:
+
+**Option A (server-threaded prop):** Pass `fields: SignatureFieldData[]` from `DocumentPageClient` to `PreparePanel` as a new optional prop. `DocumentPageClient` would need to track fields state. But currently fields are fetched and owned by `FieldPlacer` — `DocumentPageClient` doesn't hold them.
+
+**Option B (hint-only prop, preferred):** Pass `fieldHints: Record` from `DocumentPageClient` to `PreparePanel`. `DocumentPageClient` initializes `fieldHints` when it has the hint data. But `DocumentPageClient` also doesn't receive fields on mount — they're fetched by FieldPlacer.
+
+**Option C (fetch in PreparePanel, simplest):** When `selectedFieldId` changes (and is non-null), PreparePanel fetches the hint for that field from `/api/documents/:id/fields`, picks out the selected field's `hint`. This adds a round-trip but is completely self-contained. However it fetches ALL fields just for the hint.
+
+**Option D (lift fields to DocumentPageClient via callback, recommended):** Introduce a `onFieldsLoaded` callback from `DocumentPageClient` → `FieldPlacer`. When FieldPlacer fetches fields from DB, it also calls this callback. `DocumentPageClient` stores `fields: SignatureFieldData[]` in state and passes them to `PreparePanel`. This is analogous to how `aiPlacementKey` causes FieldPlacer to re-fetch. However this requires more prop threading.
+
+**Simplest viable approach for this phase (recommended based on scope):** Pass `selectedFieldHint: string | undefined` as a new prop to `PreparePanel`. In `DocumentPageClient`, add a `fields` state initialized from a one-time fetch of `/api/documents/:docId/fields` on mount. When `selectedFieldId` changes, look up the hint from `fields`. This is one additional fetch (which is already done by FieldPlacer anyway — lightweight deduplication).
+
+Actually — the simplest correct approach: `DocumentPageClient` fetches fields once at mount to populate `fields` state. This is also needed for the role auto-seed check (`initialSigners.length === 0` check already there). The hint lookup is then: `const selectedFieldHint = selectedFieldId ? fields.find(f => f.id === selectedFieldId)?.hint : undefined`.
+
+Pass `selectedFieldHint?: string` as a new optional prop to `PreparePanel`. In PreparePanel's Quick Fill section:
+```typescript
+{selectedFieldHint && (
+
+)}
+```
+
+This is a backward-compatible optional prop (default undefined). No existing call sites break.
+
+**Where to fetch fields in DocumentPageClient:** Add a `useEffect` on mount (empty deps) that calls `GET /api/documents/:docId/fields` and stores in `useState([])`. This endpoint already exists (used by PreparePanel's handlePrepare). When `aiPlacementKey` changes (AI re-places), re-fetch to pick up new hints.
+
+### Pattern 4: GET /api/templates — Already Sufficient
+
+The existing endpoint already returns all required data:
+- `id`, `name`, `formName`, `fieldCount`, `updatedAt` — everything the My Templates tab needs.
+- It does NOT return `signatureFields` in the response (the `signatureFields` column is fetched but only used server-side to compute `fieldCount`). This is correct per D-02 — the modal only needs name/formName/fieldCount.
+- The server-side POST handler fetches `signatureFields` directly from DB, so no change to GET response needed.
+
+**Confidence:** HIGH — confirmed by reading `src/app/api/templates/route.ts` lines 1-36.
+
+### Recommended Project Structure (files touched)
+
+```
+teressa-copeland-homes/src/
+├── app/
+│ ├── portal/
+│ │ └── _components/
+│ │ └── AddDocumentModal.tsx ← Add My Templates tab + submit branch
+│ └── api/
+│ └── documents/
+│ └── route.ts ← Add documentTemplateId branch
+└── app/portal/(protected)/documents/[docId]/
+ └── _components/
+ ├── DocumentPageClient.tsx ← Add fields state + selectedFieldHint derivation
+ └── PreparePanel.tsx ← Add selectedFieldHint prop + chip render
+```
+
+### Anti-Patterns to Avoid
+
+- **Mutating template field IDs in place:** Always create new objects (`{ ...f, id: crypto.randomUUID() }`) — never mutate the JSONB objects fetched from DB.
+- **Setting signerEmail on signers array entries from role labels:** The `signers` array should always contain real emails (or the role label as fallback if no contact mapped). Fields' `signerEmail` slots keep the role labels — this is intentional for the existing color-assignment system.
+- **Fetching fields inside PreparePanel on every selectedFieldId change:** One fetch on mount (or on aiPlacementKey change) is sufficient. Don't fetch inside PreparePanel — keep it prop-driven.
+- **Breaking the existing forms-library tab:** D-04 is explicit — zero changes to existing behavior. Tab switching must not clear existing form/file selections unless the user actively picks something in the other tab.
+- **Passing `signatureFields` in GET /api/templates response:** The full JSONB column can be large (100+ fields). The modal only needs `id`, `name`, `formName`, `fieldCount`. Don't add `signatureFields` to the GET response.
+
+---
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| UUID generation per field | Custom ID generator | `crypto.randomUUID()` | Already used in schema defaults; Node built-in; no import needed |
+| Role deduplication in order | Custom Set logic | `[...new Set(array)]` | Standard JS, preserves insertion order |
+| Tab UI | Custom tab library | `useState` + conditional render | Project uses zero UI component libraries — plain Tailwind/inline styles |
+| Field lookup by ID | Custom index | `Array.prototype.find` | Fields array is small (< 200 items); no index needed |
+
+---
+
+## Runtime State Inventory
+
+This phase is not a rename/refactor. No runtime state inventory needed. All changes are code-only with no stored strings being renamed.
+
+---
+
+## Common Pitfalls
+
+### Pitfall 1: signerEmail slot carries role labels, not emails
+
+**What goes wrong:** Developer reads `field.signerEmail` expecting an email address, but in templates it contains "Buyer", "Seller", etc. Code that calls `isValidEmail(field.signerEmail)` will fail. The `getSignerEmail()` utility will return the role label, which is not a real email.
+
+**Why it happens:** Template fields use `signerEmail` as a role-label slot (v1.3 design decision). The field type `SignatureFieldData.signerEmail` is typed as `string | undefined` with no discrimination between role labels and real emails.
+
+**How to avoid:** In the POST /api/documents template branch, treat `field.signerEmail` values as role labels. The `signers` array on the document is where real emails go. Never pass template field `signerEmail` values directly to email-sending logic.
+
+**Warning signs:** `isValidEmail(field.signerEmail)` returning false in the POST handler. Email send step receiving strings like "Buyer" or "Seller".
+
+### Pitfall 2: Field copy shares object references (JSONB mutation)
+
+**What goes wrong:** `const copiedFields = template.signatureFields.map(f => ({ ...f, id: crypto.randomUUID() }))` — shallow spread is sufficient because `SignatureFieldData` has only primitive properties (strings, numbers). No nested objects. But if a future field type adds nested JSONB, mutation could propagate to the template record.
+
+**Why it happens:** Drizzle returns JSONB as JS objects, not frozen values.
+
+**How to avoid:** The shallow spread `{ ...f }` is correct for the current `SignatureFieldData` shape. Document this assumption with a comment in the code.
+
+### Pitfall 3: clientId not validated before client fetch
+
+**What goes wrong:** The POST handler fetches the client for role mapping but doesn't guard against an invalid/missing `clientId`. If the client doesn't exist, `db.query.clients.findFirst` returns `undefined` and `client?.email` is undefined, so `contacts[0]` becomes `undefined`.
+
+**Why it happens:** The existing POST handler validates `clientId` and `name` are non-empty strings but does not verify the client exists in DB.
+
+**How to avoid:** Add a client existence check before the template branch: `if (!clientId || !name) return 400`. The role mapping degrades gracefully (role labels used as fallback `email: contacts[i] ?? role`) — so even if client fetch fails, the insert doesn't crash. But a missing client means the document is orphaned, which is a separate concern the existing code already has.
+
+### Pitfall 4: "My Templates" tab fetch race with modal open
+
+**What goes wrong:** Modal opens → fetch to `/api/templates` is fired → agent has no templates → fetch returns empty array → agent is confused by empty state.
+
+**Why it happens:** Network latency on modal open.
+
+**How to avoid:** Show a loading state or "No templates yet" empty state. The empty state copy should be helpful: "No templates saved yet. Create one from the Templates page."
+
+### Pitfall 5: SIGNER_COLORS constant duplication
+
+**What goes wrong:** `PreparePanel`, `DocumentPageClient`, and now `POST /api/documents` all define the same `SIGNER_COLORS` array. If the palette is changed in one place, the others become inconsistent.
+
+**Why it happens:** The color array originated in `PreparePanel`, was duplicated in `DocumentPageClient` (Phase 16), and now needs to be in the API route.
+
+**How to avoid:** For this phase, inline the constant in the API route as-is (same 4-color array). The duplication is an existing project pattern — Phase 20 should not introduce a shared-util refactor that changes scope. Document the duplication in a comment for future cleanup.
+
+---
+
+## Code Examples
+
+### Apply template — field copy with fresh UUIDs
+
+```typescript
+// Source: CONTEXT.md D-07, confirmed against schema.ts SignatureFieldData shape
+const rawFields: SignatureFieldData[] = docTemplate.signatureFields ?? [];
+const copiedFields: SignatureFieldData[] = rawFields.map(f => ({
+ ...f, // copies: page, x, y, width, height, type, hint, signerEmail (role label)
+ id: crypto.randomUUID(), // fresh UUID — template field ID must never appear in documents (D-07)
+}));
+```
+
+### Role-to-email mapping
+
+```typescript
+// Source: CONTEXT.md D-06 + specifics pseudocode
+// Collect unique role labels in order of first appearance
+const seenRoles = new Set();
+const uniqueRoles: string[] = [];
+for (const f of copiedFields) {
+ if (f.signerEmail && !seenRoles.has(f.signerEmail)) {
+ seenRoles.add(f.signerEmail);
+ uniqueRoles.push(f.signerEmail);
+ }
+}
+
+// Build client contacts array: primary email first, then co-buyer emails
+const clientEmails = [
+ client?.email,
+ ...(client?.contacts ?? []).map((c: ClientContact) => c.email),
+].filter(Boolean) as string[];
+
+const SIGNER_COLORS = ['#6366f1', '#f43f5e', '#10b981', '#f59e0b'];
+const mappedSigners: DocumentSigner[] = uniqueRoles.map((role, i) => ({
+ email: clientEmails[i] ?? role, // fallback: keep role label if no client contact at index i
+ color: SIGNER_COLORS[i % SIGNER_COLORS.length],
+}));
+```
+
+### PreparePanel hint chip (additive)
+
+```typescript
+// Source: PreparePanel.tsx Quick Fill section pattern (lines 317-355)
+// New prop added to PreparePanelProps:
+// selectedFieldHint?: string;
+
+// Inside the Quick Fill section, after existing chips:
+{selectedFieldHint && selectedFieldId && (
+
+)}
+```
+
+### DocumentPageClient — fields state + hint derivation
+
+```typescript
+// New state in DocumentPageClient
+const [fields, setFields] = useState([]);
+
+// Fetch on mount and on AI placement key change
+useEffect(() => {
+ fetch(`/api/documents/${docId}/fields`)
+ .then(r => r.json())
+ .then((data: SignatureFieldData[]) => setFields(data))
+ .catch(() => {});
+}, [docId, aiPlacementKey]);
+
+// Derive hint for selected field
+const selectedFieldHint = selectedFieldId
+ ? fields.find(f => f.id === selectedFieldId)?.hint
+ : undefined;
+
+// Pass to PreparePanel:
+// selectedFieldHint={selectedFieldHint}
+```
+
+### AddDocumentModal — tab state and My Templates fetch
+
+```typescript
+// New state
+const [activeTab, setActiveTab] = useState<'forms' | 'templates'>('forms');
+const [docTemplates, setDocTemplates] = useState([]);
+const [docTemplatesLoaded, setDocTemplatesLoaded] = useState(false);
+const [selectedDocTemplate, setSelectedDocTemplate] = useState(null);
+
+// Lazy fetch on first tab click (not on mount, to avoid unnecessary request)
+function handleSwitchToTemplates() {
+ setActiveTab('templates');
+ if (!docTemplatesLoaded) {
+ fetch('/api/templates')
+ .then(r => r.json())
+ .then((data: DocumentTemplateRow[]) => { setDocTemplates(data); setDocTemplatesLoaded(true); })
+ .catch(console.error);
+ }
+}
+
+// In handleSelectTemplate (existing): also clear selectedDocTemplate
+// In template row click: clear selectedTemplate, setCustomFile(null), set selectedDocTemplate
+```
+
+---
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| Single tab in AddDocumentModal | Two tabs: Forms Library + My Templates | Phase 20 | Additive — forms tab unchanged |
+| POST /api/documents: form-library or custom-file only | Three paths: form-library, custom-file, document-template | Phase 20 | New branch with snapshot copy |
+| PreparePanel Quick Fill: 3 chips only (Name, Address, Email) | Quick Fill: up to 4 chips (+ Template Hint when applicable) | Phase 20 | Optional — only shown when selectedField has a hint |
+
+---
+
+## Environment Availability
+
+Step 2.6: SKIPPED — no external tools or services beyond the existing Next.js stack. All changes are to API routes and React components running in the existing Docker/Node.js environment.
+
+---
+
+## Validation Architecture
+
+**nyquist_validation:** Not explicitly set to `false` in config.json — treated as enabled.
+
+### Test Framework
+
+| Property | Value |
+|----------|-------|
+| Framework | Jest 29.7 + ts-jest |
+| Config file | `package.json` (jest.preset: ts-jest, testEnvironment: node) |
+| Quick run command | `cd teressa-copeland-homes && npx jest --testPathPattern=prepare-document` |
+| Full suite command | `cd teressa-copeland-homes && npx jest` |
+
+### Phase Requirements → Test Map
+
+| Req ID | Behavior | Test Type | Automated Command | File Exists? |
+|--------|----------|-----------|-------------------|-------------|
+| TMPL-10 | "My Templates" tab appears, fetches templates | manual-only (React UI) | n/a — no component test runner | ❌ not applicable |
+| TMPL-11 | Fields copied with fresh UUIDs, positions/types preserved | unit | `npx jest --testPathPattern=apply-template` | ❌ Wave 0 |
+| TMPL-12 | Role-to-email mapping logic | unit | `npx jest --testPathPattern=apply-template` | ❌ Wave 0 |
+| TMPL-13 | Hint chip appears when selected field has hint | manual-only (React UI) | n/a | ❌ not applicable |
+| TMPL-14 | Snapshot independence (no live link) | architectural (no code) | n/a — satisfied by D-08 design | n/a |
+| TMPL-15 | Templates nav link | already complete (Phase 19) | n/a | n/a |
+| TMPL-16 | Templates list page | already complete (Phase 19) | n/a | n/a |
+
+### Sampling Rate
+
+- **Per task commit:** `cd teressa-copeland-homes && npx jest` (full suite — only 1 test file currently)
+- **Per wave merge:** `cd teressa-copeland-homes && npx jest`
+- **Phase gate:** Full suite green before `/gsd:verify-work`
+
+### Wave 0 Gaps
+
+- [ ] `src/lib/utils/__tests__/apply-template.test.ts` — unit tests for field copy (UUID freshness) and role-to-email mapping logic. These are pure functions that can be extracted from the POST handler and tested in isolation.
+
+*(If extracting to a util is too heavy for this phase, the test can inline the logic and test it directly — following the `prepare-document.test.ts` pattern of testing the pure conversion math.)*
+
+---
+
+## Open Questions
+
+1. **Does `/api/documents/:docId/fields` endpoint exist?**
+ - What we know: `PreparePanel.handlePrepare` calls `fetch(`/api/documents/${docId}/fields`)` (line 262) — so the endpoint must exist.
+ - What's unclear: Not verified by reading the route file.
+ - Recommendation: Confirm endpoint exists before DocumentPageClient plan relies on it. Low risk — PreparePanel already uses it.
+
+2. **Where does `SignatureFieldData.hint` appear on the signing page for client-text fields?**
+ - What we know: TMPL-08 (Phase 19) added hint support to template editor. D-11 says hints are NOT shown as placeholder on the field.
+ - What's unclear: Whether the signing page currently renders hints as placeholder text for `client-text` fields — if so, hints will appear there in documents created from templates (which is probably correct behavior, just not PreparePanel's concern).
+ - Recommendation: Out of scope for Phase 20 planning — PreparePanel's D-11 constraint is clear. Note for future reference.
+
+3. **What happens when a document template has zero signatureFields (null/empty)?**
+ - What we know: `documentTemplates.signatureFields` is nullable JSONB (Phase 18 decision: "template starts empty"). An agent could create a template but never open the editor.
+ - What's unclear: Should applying an empty template be blocked or allowed?
+ - Recommendation: Allow it — `copiedFields = []` is valid. Document is created with no fields (agent places them manually). This matches the existing "create document with no fields" flow.
+
+---
+
+## Sources
+
+### Primary (HIGH confidence)
+
+- Direct reading of `src/app/portal/_components/AddDocumentModal.tsx` — existing modal structure, state patterns, submit branches
+- Direct reading of `src/app/api/documents/route.ts` — existing POST handler, DB insert pattern
+- Direct reading of `src/lib/db/schema.ts` — `documentTemplates`, `SignatureFieldData`, `ClientContact`, `DocumentSigner` types
+- Direct reading of `src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` — Quick Fill section implementation
+- Direct reading of `src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx` — state threading pattern, signers, aiPlacementKey
+- Direct reading of `src/app/api/templates/route.ts` — GET response shape, JOIN with formTemplates
+- Direct reading of `src/app/portal/_components/ClientProfileClient.tsx` — how AddDocumentModal is called (clientId only, no client object)
+- Direct reading of `.planning/phases/20-apply-template-and-portal-nav/20-CONTEXT.md` — locked decisions D-01 through D-12
+- Direct reading of `.planning/STATE.md` — accumulated pre-decisions, Phase 19 confirmation
+- Direct reading of `package.json` jest config — test framework details
+
+### Secondary (MEDIUM confidence)
+
+- None needed — all findings are from direct source code inspection.
+
+---
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack: HIGH — zero new dependencies; all libraries confirmed in package.json and in active use
+- Architecture: HIGH — all patterns confirmed by reading actual source files
+- Pitfalls: HIGH — derived from reading the exact code, not speculation
+- Test infrastructure: HIGH — confirmed from package.json and existing test file
+
+**Research date:** 2026-04-06
+**Valid until:** 2026-05-06 (stable codebase — no fast-moving dependencies)
+
+## Project Constraints (from CLAUDE.md)
+
+The following directives from `teressa-copeland-homes/CLAUDE.md` are binding on the planner:
+
+| Directive | Implication for Phase 20 |
+|-----------|--------------------------|
+| Extend existing patterns unless explicitly justified | AddDocumentModal gets additive tabs; PreparePanel gets additive optional prop; POST handler gets additive branch. No new abstractions. |
+| No greenfield assumptions — inspect repository first | Done: all 7 canonical files read and understood. |
+| Consistency is mandatory (file structure, naming, patterns) | Tab UI uses plain `useState` + Tailwind, not a component library. Fetch pattern matches existing `useEffect` + `fetch`. |
+| All solutions must reference real repo structure | Research is grounded in actual file contents. |
+| Security: path traversal guard | The PDF copy path in the template branch must pass through the same `!destPath.startsWith(UPLOADS_DIR)` guard as existing paths. |
+| Deviation must be justified | No deviations proposed. |