No shadcn gate required: `components.json` is absent. Stack is Next.js 15 App Router + Tailwind CSS v4 (`@tailwindcss/postcss`). All existing portal components are hand-crafted with Tailwind utility classes and inline `style={}` objects. This phase continues that pattern.
---
## Spacing Scale
Declared values (must be multiples of 4):
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon gaps, dot-to-label gap in signer rows, badge inner padding |
| sm | 8px | Compact element spacing, signer row internal gap, input padding |
| md | 16px | Default element spacing, panel section gaps, field box padding |
| lg | 24px | Section padding within PreparePanel, table cell padding |
| xl | 32px | Layout gaps between major panel sections |
| 2xl | 48px | Major section breaks (not used within PreparePanel) |
Source: extracted directly from existing components.
- PreparePanel `<h2>`: `font-semibold` (600) — maps to Heading
- PreparePanel body labels: `text-sm` = 14px regular — maps to Body
- PreparePanel secondary labels (`text-xs text-gray-400`): 12px medium — maps to Label
- DocumentsTable `<th>`: `0.75rem font-weight:600 uppercase letter-spacing:0.05em` — maps to Label (uppercase table headers are an established portal pattern)
Only 2 weights used throughout the portal: 400 (regular) and 600 (semibold). Weight 500 is used for table data cells and badge text — treat as part of the semibold family for this contract (no additional weight token needed).
Accent reserved for: document link hover, primary heading text, Download Signed PDF button background. NOT used for signer colors or type-palette colors.
### Signer Color Palette (locked — source: CONTEXT.md D-01)
Auto-assigned in order when signers are added. Stored in `documents.signers[].color`.
| Signer Index | Color | Hex |
|-------------|-------|-----|
| 0 (first) | Indigo | `#6366f1` |
| 1 (second) | Rose | `#f43f5e` |
| 2 (third) | Emerald | `#10b981` |
| 3 (fourth) | Amber | `#f59e0b` |
| 4+ | Cycles back | same order |
### Field Type Color Palette (existing — source: `FieldPlacer.tsx` `PALETTE_TOKENS`)
These colors are UNCHANGED. They apply only when a field has no `signerEmail`.
| Field Type | Color | Hex |
|------------|-------|-----|
| client-signature | Blue | `#2563eb` |
| initials | Purple | `#7c3aed` |
| checkbox | Green | `#059669` |
| date | Amber | `#d97706` |
| text | Slate | `#64748b` |
| agent-signature | Red | `#dc2626` |
| agent-initials | Orange | `#ea580c` |
### Color Override Rule (locked — source: CONTEXT.md D-06, D-07)
When `field.signerEmail` is set: field box border, background tint, and drag ghost use the **signer color** from `documents.signers`. When `field.signerEmail` is absent (agent-owned fields or untagged fields): use existing **type color** from `PALETTE_TOKENS`.
### Validation State Color
- Unassigned client-facing field outline: `2px solid #ef4444` (red-500, slightly lighter than destructive `#dc2626` to distinguish validation from permanent agent-red fields)
- Error message text below Send button: `text-red-600` (`#dc2626`), consistent with existing `result.ok === false` style in PreparePanel
3. Input validates: non-empty, valid email format, not duplicate
4. On valid: signer appended to list with auto-assigned color, input cleared
5. On invalid: input border turns red (`border-red-400`), no addition, no persistent error message (transient only)
6. Signers saved to DB: either on each add/remove OR on "Prepare and Send" click (planner decides, per CONTEXT.md discretion)
### Signer Remove Flow
1. Agent clicks × on a signer row
2. Signer removed immediately from local list
3. If any fields had that signer's email assigned: those fields lose their `signerEmail` and revert to type color — no confirmation dialog required (reversible action, not destructive data)
4. Signer saved/removed from DB per same timing as add
### Active Signer Selection
1. Agent selects signer from dropdown before dragging a field
2. All subsequent field drops auto-assign `signerEmail` to selected signer's email
3. Field renders in signer color immediately on drop
4. Agent can switch active signer and drag more fields — each new field picks up current active signer
### Send Validation
1. Agent clicks "Prepare and Send"
2. PreparePanel counts `isClientVisibleField` fields with no `signerEmail`
3. If count > 0 AND `signers.length > 0`: block call, show error, pass `unassignedFieldIds` to FieldPlacer for red highlight
4. If `signers.length === 0` AND client-visible fields exist: block call, show "Add at least one signer before sending."
5. Red highlights clear as agent assigns signers to each flagged field (reactive, no manual dismiss)
### N/M Signed Badge
- Computed server-side by counting `signingTokens WHERE documentId = X AND usedAt IS NOT NULL` vs total tokens
- Refreshes on page load (server component — no client polling)
No destructive confirmation dialogs in this phase. Removing a signer is reversible (add them back), so no confirm dialog is needed.
---
## State Architecture Notes
These are not visual specs but inform the executor's component wiring.
-`signers: DocumentSigner[]` lives in `DocumentPageClient` — passed as prop to both `PreparePanel` (for signer list UI + send validation) and `FieldPlacer` (for active signer selector + color lookup)
-`unassignedFieldIds: Set<string>` computed in `DocumentPageClient` (or `PreparePanel`) at send-click time — passed to `FieldPlacer` via prop
-`activeSigner: DocumentSigner | null` lives in `FieldPlacer` (local state) — default to `signers[0]` on load; reset when `signers` prop changes
-`FieldPlacer.handleDragEnd` sets `newField.signerEmail = activeSigner?.email` when placing a field