diff --git a/.planning/phases/19-pipeline-crm-kanban/19-RESEARCH.md b/.planning/phases/19-pipeline-crm-kanban/19-RESEARCH.md
new file mode 100644
index 0000000..dfd796a
--- /dev/null
+++ b/.planning/phases/19-pipeline-crm-kanban/19-RESEARCH.md
@@ -0,0 +1,433 @@
+# Phase 19: Pipeline CRM Kanban — Research
+
+**Researched:** 2026-06-19
+**Domain:** @dnd-kit drag-drop, Next.js App Router client components, CRM leads view toggle
+**Confidence:** HIGH — all findings verified directly from codebase
+
+---
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|------------------|
+| PIPE-01 | I lead sono visualizzabili in una board Kanban stile Pipedrive con colonne per stage e drag-drop per cambiare stage | `leads.status` enum verified (6 stages); `@dnd-kit/core` v6.3.1 already installed; exact analog in `KanbanBoard.tsx` |
+| PIPE-02 | Spostare un lead nelle colonne "Vinto"/"Perso" è il cambio-stato manuale dell'esito | `won`/`lost` are existing LEAD_STAGES values; `updateLeadField(id, "status", value)` handles this today via the table dropdown |
+
+
+---
+
+## Summary
+
+Phase 14 delivered a complete inline-edit table view of leads (`LeadTable.tsx`) backed by a solid data layer: `getLeadsWithTags()`, `updateLeadField()`, typed `LEAD_STAGES`, and a polymorphic tag system. Phase 19 adds a second view — Kanban — toggled from the same page, without replacing or changing any of that.
+
+The project already ships a working `KanbanBoard.tsx` (for project tasks) that uses exactly the `@dnd-kit` primitives needed here. The new `LeadsKanbanBoard` is a direct structural analog: swap task status columns (`todo/in_progress/done`) for lead stage columns (`contacted/qualified/proposal_sent/negotiating/won/lost`), swap task cards for lead cards, swap `updateTaskStatus` for `updateLeadField(id, "status", newStage)`.
+
+The view-toggle pattern is also ready in `PhasesViewToggle.tsx` — a client component that holds `useState<"list" | "kanban">` and renders either `listView` (a `ReactNode` passed as prop) or the kanban. The leads page only needs a `LeadsViewToggle` wrapper that receives the existing `LeadsSearch` as the `listView` slot and the new `LeadsKanbanBoard` as the kanban.
+
+No schema changes. No new server actions. No new dependencies. This is a pure UI addition.
+
+**Primary recommendation:** Copy the `KanbanBoard.tsx` structure exactly; adapt for 6 lead-stage columns; wire to `updateLeadField`; wrap with a `LeadsViewToggle` component in `LeadsSearch` or at the page level.
+
+---
+
+## Architectural Responsibility Map
+
+| Capability | Primary Tier | Secondary Tier | Rationale |
+|------------|-------------|----------------|-----------|
+| Kanban board rendering + drag state | Browser / Client | — | Drag-drop is inherently client-side; `"use client"` required |
+| Lead status persistence on drop | API / Backend (Server Action) | — | `updateLeadField` is already a `"use server"` action |
+| Lead data fetching | Frontend Server (SSR) | — | `LeadsPage` is a server component; passes data down as props |
+| View toggle state (table / kanban) | Browser / Client | — | `useState` in a client wrapper component |
+| Column definitions (stage labels, colors) | Browser / Client | — | Derived from `LEAD_STAGES` constant, purely presentational |
+
+---
+
+## Standard Stack
+
+### Core (already installed — no new installs needed)
+
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| @dnd-kit/core | ^6.3.1 [VERIFIED: package.json] | DndContext, useDraggable, useDroppable, sensors | Already used in KanbanBoard.tsx |
+| @dnd-kit/sortable | ^10.0.0 [VERIFIED: package.json] | Available but NOT used by existing KanbanBoard | Not needed; existing pattern uses useDraggable + useDroppable directly |
+| @dnd-kit/utilities | ^3.2.2 [VERIFIED: package.json] | CSS.Transform helper | Imported if transform style needed |
+| React (useTransition, useState) | via Next.js 16 | Optimistic state + async server action bridging | Project pattern |
+
+**Installation:** None required. All dependencies already present.
+
+### Existing Primitives Used by KanbanBoard.tsx [VERIFIED: src/components/admin/kanban/KanbanBoard.tsx]
+
+```typescript
+import {
+ DndContext, // Root context — wraps the entire board
+ DragEndEvent, // Event type for onDragEnd handler
+ DragOverlay, // Ghost card rendered at cursor during drag
+ PointerSensor, // Mouse/touch activation
+ KeyboardSensor, // Accessibility
+ useSensor,
+ useSensors,
+ useDroppable, // Applied to column containers
+ useDraggable, // Applied to individual cards
+} from "@dnd-kit/core";
+```
+
+Note: `@dnd-kit/sortable` / `SortableContext` / `useSortable` are NOT used. The existing pattern uses the lower-level `useDraggable` + `useDroppable` primitives, which is appropriate for cross-column drag (not intra-column reordering).
+
+---
+
+## Architecture Patterns
+
+### System Architecture Diagram
+
+```
+LeadsPage (server component)
+ ├─ getLeadsWithTags() ──────────────────────────────► Postgres / leads + tags
+ ├─ getLeadFieldOptions() ───────────────────────────► Postgres / tags
+ └─ renders LeadsViewToggle (client component)
+ ├─ [view="list"] → LeadsSearch → LeadTable (existing, unchanged)
+ └─ [view="kanban"] → LeadsKanbanBoard (new)
+ ├─ DndContext (onDragEnd → updateLeadField server action)
+ ├─ DroppableColumn × 6 (one per LEAD_STAGES value)
+ └─ DraggableLeadCard × N (one per lead)
+ └─ useTransition + router.refresh() (after persist)
+```
+
+### Recommended File Structure
+
+```
+src/
+├─ components/admin/leads/
+│ ├─ LeadTable.tsx # EXISTING — unchanged
+│ └─ LeadsKanbanBoard.tsx # NEW — analogous to KanbanBoard.tsx
+├─ app/admin/leads/
+│ ├─ page.tsx # MODIFIED — wrap with LeadsViewToggle
+│ ├─ LeadsSearch.tsx # MODIFIED — receives view toggle or replaced by LeadsViewToggle
+│ └─ actions.ts # EXISTING — updateLeadField already handles status changes
+```
+
+The view toggle can live either at the page level (simpler) or inside `LeadsSearch` (keeps search state alive across views). Recommended: extract a `LeadsViewToggle` client wrapper at the page level (same pattern as `PhasesViewToggle`), passing `` as the `listView` ReactNode and `` as the kanban.
+
+### Pattern 1: Column definition for 6 lead stages
+
+```typescript
+// Source: VERIFIED from src/lib/lead-validators.ts + src/components/admin/leads/LeadTable.tsx
+
+type LeadStage = "contacted" | "qualified" | "proposal_sent" | "negotiating" | "won" | "lost";
+
+const LEAD_COLUMNS: {
+ id: LeadStage;
+ label: string;
+ headerClass: string;
+ dotClass: string;
+}[] = [
+ { id: "contacted", label: "Contattato", headerClass: "text-[#71717a]", dotClass: "bg-[#d4d4d8]" },
+ { id: "qualified", label: "Qualificato", headerClass: "text-[#1A463C]", dotClass: "bg-purple-400" },
+ { id: "proposal_sent", label: "Offerta inviata", headerClass: "text-amber-700", dotClass: "bg-amber-400" },
+ { id: "negotiating", label: "Trattativa", headerClass: "text-orange-700", dotClass: "bg-orange-400" },
+ { id: "won", label: "Vinto", headerClass: "text-green-700", dotClass: "bg-green-500" },
+ { id: "lost", label: "Perso", headerClass: "text-red-700", dotClass: "bg-red-400" },
+];
+```
+
+### Pattern 2: Lead Kanban Board (adapted from KanbanBoard.tsx)
+
+```typescript
+// Source: VERIFIED structure from src/components/admin/kanban/KanbanBoard.tsx
+// Key adaptation: replace taskStatuses/updateTaskStatus with leadStatuses/updateLeadField
+
+"use client";
+import { useState, useTransition } from "react";
+import { useRouter } from "next/navigation";
+import {
+ DndContext, DragEndEvent, DragOverlay,
+ PointerSensor, KeyboardSensor, useSensor, useSensors,
+ useDroppable, useDraggable,
+} from "@dnd-kit/core";
+import { updateLeadField } from "@/app/admin/leads/actions";
+import type { LeadWithTags } from "@/lib/admin-queries";
+
+export function LeadsKanbanBoard({ leads }: { leads: LeadWithTags[] }) {
+ const router = useRouter();
+ const [, startTransition] = useTransition();
+ const [activeId, setActiveId] = useState(null);
+ const [leadStatuses, setLeadStatuses] = useState>(
+ () => Object.fromEntries(leads.map((l) => [l.id, l.status as LeadStage]))
+ );
+
+ const sensors = useSensors(
+ useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
+ useSensor(KeyboardSensor)
+ );
+
+ function handleDragEnd(event: DragEndEvent) {
+ const { active, over } = event;
+ setActiveId(null);
+ if (!over) return;
+ const leadId = active.id as string;
+ const newStage = over.id as LeadStage;
+ if (newStage === leadStatuses[leadId]) return;
+ // Optimistic update
+ setLeadStatuses((prev) => ({ ...prev, [leadId]: newStage }));
+ // Persist
+ startTransition(async () => {
+ await updateLeadField(leadId, "status", newStage);
+ router.refresh();
+ });
+ }
+
+ // ... render DndContext with LEAD_COLUMNS mapped to DroppableColumn
+}
+```
+
+### Pattern 3: View toggle (adapted from PhasesViewToggle.tsx)
+
+```typescript
+// Source: VERIFIED from src/components/admin/kanban/PhasesViewToggle.tsx
+"use client";
+import { useState, type ReactNode } from "react";
+import { LeadsKanbanBoard } from "@/components/admin/leads/LeadsKanbanBoard";
+import type { LeadWithTags, LeadFieldOptions } from "@/lib/admin-queries";
+
+export function LeadsViewToggle({
+ listView,
+ leads,
+}: {
+ listView: ReactNode;
+ leads: LeadWithTags[];
+}) {
+ const [view, setView] = useState<"list" | "kanban">("list");
+ return (
+
+ {/* Toggle buttons — same pill pattern as PhasesViewToggle */}
+ {view === "list" ? listView : }
+
+ );
+}
+```
+
+### Pattern 4: Lead card content
+
+Each Kanban card should show: `name` (primary), `company` (secondary/optional), `next_action` (hint text, optional). Avoid showing `email`/`phone`/`tags` on the card to keep it compact — these are available in the table view.
+
+```typescript
+// Fields available on LeadWithTags (VERIFIED: src/lib/admin-queries.ts line 890)
+// Lead & { tags: string[] }
+// Relevant for card: name, company, next_action, status
+```
+
+### Anti-Patterns to Avoid
+
+- **Using `useSortable` / `SortableContext`:** The existing project pattern does NOT use these. They are for intra-column reordering. Use `useDraggable` + `useDroppable` to match the established `KanbanBoard.tsx` pattern.
+- **Calling `router.refresh()` before `await updateLeadField`:** Always await the server action first, then refresh. The existing KanbanBoard does this correctly inside `startTransition`.
+- **Dropping `react-hook-form` / Zod on drag-drop:** No form validation needed for a status change — `updateLeadField` already validates via `LEAD_STAGES.includes(value)` check.
+- **Removing `LeadsSearch` / `LeadTable`:** PIPE-01 requires the table to remain as an alternative view. Do not replace it.
+
+---
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| Drag detection (distance threshold) | Custom mouse event tracking | `PointerSensor` with `activationConstraint: { distance: 5 }` | Already proven in KanbanBoard.tsx; prevents accidental drag on click |
+| Keyboard accessibility for drag | Custom key handlers | `KeyboardSensor` from @dnd-kit/core | a11y for free |
+| Drag ghost/overlay | CSS clone positioning | `DragOverlay` from @dnd-kit/core | Correct portal rendering, no z-index fights |
+| Optimistic UI update | Complex local state with rollback | `useState` + `useTransition` (React pattern) | Already used in KanbanBoard.tsx and LeadTable.tsx |
+| Status validation | Re-implementing LEAD_STAGES check | `updateLeadField` server action already validates status | DRY — the action throws on invalid stage |
+
+**Key insight:** The entire drag-drop + persist pattern is already implemented and tested in `KanbanBoard.tsx`. This phase is a structural copy with domain adaptation, not a new implementation.
+
+---
+
+## Common Pitfalls
+
+### Pitfall 1: Columns wider than viewport on 6-stage board
+**What goes wrong:** 6 columns in `grid-cols-6` become too narrow on typical laptop screens (1280–1440px). The 3-column project kanban uses `grid-cols-3` with comfortable card width.
+**Why it happens:** 6 × min-width ≈ 720px+ is tight.
+**How to avoid:** Use `grid-cols-3 lg:grid-cols-6` or a horizontally scrollable container (`overflow-x-auto` on the grid wrapper). Alternatively, `min-w-[200px]` per column inside a scroll container.
+**Warning signs:** Cards truncate before the lead name is visible.
+
+### Pitfall 2: Won/Lost columns need visual distinction
+**What goes wrong:** Dropping to "won" or "lost" looks identical to other columns — user may not notice the semantic weight of these terminal states.
+**Why it happens:** Uniform column styling.
+**How to avoid:** Use visually distinct `headerClass` (green for won, red for lost) and consider a stronger `isOver` highlight for these columns. The STAGE_COLOR map in `LeadTable.tsx` already defines these colors — reuse them.
+
+### Pitfall 3: Leads not sorted consistently between views
+**What goes wrong:** Table shows leads ordered by `updated_at DESC`; kanban derived from the same array shows different visual order depending on column grouping.
+**Why it happens:** No explicit sort on the kanban card order within a column.
+**How to avoid:** Sort leads within each column by `updated_at DESC` (same as the existing query order). The `getLeadsWithTags` query already returns `orderBy(desc(leads.updated_at))` so inheriting that order is sufficient.
+
+### Pitfall 4: `router.refresh()` causes full re-mount of kanban
+**What goes wrong:** After a drag-drop, `router.refresh()` rehydrates the server component, re-running `getLeadsWithTags()`. If the drag animation hasn't completed, it can cause a visual flicker.
+**Why it happens:** Next.js App Router refresh re-renders the whole tree.
+**How to avoid:** The existing `KanbanBoard.tsx` uses the same pattern without issue. The `setActiveId(null)` call in `handleDragEnd` clears the overlay before the refresh arrives, so the flicker is acceptable. This is the project's established pattern — do not deviate.
+
+### Pitfall 5: Search/filter not available in kanban view
+**What goes wrong:** The search bar lives in `LeadsSearch.tsx` and only filters `LeadTable`. If the user switches to kanban, they lose the ability to filter.
+**Why it happens:** The view toggle renders either `LeadsSearch` (with its internal state) or the bare `LeadsKanbanBoard`.
+**How to avoid:** Two acceptable approaches: (a) wrap both views together inside `LeadsSearch` and pass filtered leads to both (preferred — search state persists across view switches); or (b) accept that kanban shows all leads unfiltered (simpler, acceptable for now given the single-user context). Document the choice in the plan.
+
+---
+
+## Code Examples
+
+### Existing updateLeadField signature (server action)
+
+```typescript
+// Source: VERIFIED from src/app/admin/leads/actions.ts line 174
+// EDITABLE_FIELDS includes "status" — drag-drop can call this directly
+export async function updateLeadField(
+ leadId: string,
+ fieldName: "name" | "email" | "phone" | "company" | "status" | "next_action",
+ value: string
+): Promise
+// Validates: status must be in LEAD_STAGES; throws on invalid value
+// Side effects: revalidatePath("/admin/leads") + revalidatePath(`/admin/leads/${leadId}`)
+```
+
+### LEAD_STAGES canonical values
+
+```typescript
+// Source: VERIFIED from src/lib/lead-validators.ts line 4
+export const LEAD_STAGES = [
+ "contacted",
+ "qualified",
+ "proposal_sent",
+ "negotiating",
+ "won",
+ "lost",
+] as const;
+```
+
+### Existing STAGE_COLOR map (reuse for kanban column headers)
+
+```typescript
+// Source: VERIFIED from src/components/admin/leads/LeadTable.tsx line 20
+const STAGE_COLOR: Record = {
+ contacted: "bg-blue-100 text-blue-800",
+ qualified: "bg-purple-100 text-purple-800",
+ proposal_sent: "bg-amber-100 text-amber-800",
+ negotiating: "bg-orange-100 text-orange-800",
+ won: "bg-green-100 text-green-800",
+ lost: "bg-red-100 text-red-800",
+};
+// Move to a shared constant (e.g., src/lib/lead-constants.ts) if reused in both components
+```
+
+### PhasesViewToggle pattern (exact analog)
+
+```typescript
+// Source: VERIFIED from src/components/admin/kanban/PhasesViewToggle.tsx
+// State: useState<"list" | "kanban">("list")
+// Toggle: pill button group (bg-[#f4f4f5] rounded-lg p-1 w-fit)
+// Active: bg-white text-[#1A463C] shadow-sm
+// Inactive: text-[#71717a] hover:text-[#1a1a1a]
+```
+
+---
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| Lead status change via modal form | Inline dropdown in table cell (`StatusCell`) | Phase 14 | Drag-drop is the third mechanism; all write to same `updateLeadField` action |
+| Separate `/admin/analytics` route | Fused into `/admin` dashboard | Phase 18 | No impact on leads page |
+| `SendQuoteModal` with dead branch | Dead branch removed | Phase 18 | No impact |
+
+---
+
+## Project Constraints (from CLAUDE.md)
+
+| Directive | Impact on This Phase |
+|-----------|---------------------|
+| `clients.token` = rotatable, never PK | Not relevant (leads have no token) |
+| `quote_items` never exposed via client API | Not relevant (Kanban is admin-only) |
+| `deliverables.approved_at` immutable once set | Not relevant |
+| Auth: `/admin/*` → Auth.js session | Kanban lives at `/admin/leads` — already protected |
+| No file hosting v1 | Not relevant |
+| Migration safety: never drop/truncate rows | Phase 19 is UI-only — no schema changes, no migration needed |
+| Security: confirm before destructive commands | No destructive operations |
+| No package installs without showing name+version | No new packages needed |
+
+---
+
+## Environment Availability
+
+Step 2.6: SKIPPED — Phase 19 is a pure UI addition. All required libraries (`@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`) are already installed. No external services, databases (beyond the existing Neon Postgres connection), or CLI tools are needed.
+
+---
+
+## Validation Architecture
+
+`nyquist_validation: false` in `.planning/config.json` — section omitted per config.
+
+---
+
+## Security Domain
+
+Phase 19 adds a new interaction path to an existing admin-only route (`/admin/leads`). No new auth surface is introduced.
+
+| ASVS Category | Applies | Standard Control |
+|---------------|---------|-----------------|
+| V2 Authentication | yes (existing) | Auth.js session via `requireAdmin()` in server action |
+| V4 Access Control | yes (existing) | `requireAdmin()` guard in `updateLeadField` — drag-drop calls same action |
+| V5 Input Validation | yes | `updateLeadField` validates status via `LEAD_STAGES.includes(value)` — no new validation needed |
+
+No new threat surface beyond what Phase 14 already addressed. The drag-drop `handleDragEnd` validates the `over.id` is a known stage before calling the server action — follow the same guard pattern as in `KanbanBoard.tsx` (line 190: `if (!(["todo", "in_progress", "done"] as string[]).includes(newStatus)) return;`).
+
+---
+
+## Assumptions Log
+
+| # | Claim | Section | Risk if Wrong |
+|---|-------|---------|---------------|
+| A1 | The `won` and `lost` columns are terminal states with no special side-effects beyond setting `leads.status` (no auto-creation of client/project, no email trigger) | Architecture Patterns | If the plan later requires auto-provisioning on "won", a new server action will be needed — but PIPE-01/02 say nothing about this, and PROP-04 (auto-provisioning) is deferred to backlog post-R5 |
+| A2 | Card content (name, company, next_action) is sufficient for the kanban view; no additional fields are needed per card | Code Examples | If the user wants tags or email visible on cards, the `LeadWithTags` type already provides them — no data-layer change, only card template change |
+| A3 | The search filter covering only the table view (not the kanban) is acceptable for v1 of this feature | Common Pitfalls | If the user wants search in kanban too, the fix is to lift filtered state into the toggle wrapper — straightforward but adds scope |
+
+---
+
+## Open Questions
+
+1. **Search/filter scope in kanban view**
+ - What we know: `LeadsSearch` holds the search `useState` and passes `filtered` leads to `LeadTable`. The kanban would receive all leads from the page.
+ - What's unclear: Does the user want the search bar to filter the kanban board too, or is it acceptable that kanban shows all leads?
+ - Recommendation: Default to wrapping both views inside a new `LeadsViewToggle` that receives `leads` (unfiltered) and `options`, manages the view toggle, and passes `filtered` leads to both `LeadTable` and `LeadsKanbanBoard`. This is a clean pattern and handles it gracefully.
+
+2. **Column layout: scroll vs. wrap on 6 columns**
+ - What we know: The existing kanban uses `grid-cols-3`. Six columns need more space.
+ - What's unclear: Target viewport is unknown (likely 1440px+ since this is a single-admin tool).
+ - Recommendation: Use `min-w-[180px]` per column inside an `overflow-x-auto` wrapper. This makes it work on any viewport without content truncation.
+
+---
+
+## Sources
+
+### Primary (HIGH confidence)
+- `src/components/admin/kanban/KanbanBoard.tsx` — exact @dnd-kit usage pattern, drag primitives, sensors, DragOverlay, optimistic update + router.refresh()
+- `src/components/admin/kanban/PhasesViewToggle.tsx` — view toggle pattern (list/kanban state, pill button UI)
+- `src/components/admin/leads/LeadTable.tsx` — STAGE_COLOR map, LeadWithTags usage, StatusCell inline dropdown
+- `src/app/admin/leads/actions.ts` — `updateLeadField` signature, EDITABLE_FIELDS, `requireAdmin()` guard
+- `src/lib/lead-validators.ts` — canonical LEAD_STAGES array (6 values)
+- `src/lib/admin-queries.ts` lines 883–942 — `LeadWithTags` type, `getLeadsWithTags()` query (all fields), `LeadFieldOptions`
+- `src/db/schema.ts` lines 441–462 — `leads` table definition, `status` column with all 6 stage values documented
+- `src/app/admin/leads/LeadsSearch.tsx` — search filter pattern, `LeadWithTags` + `LeadFieldOptions` prop interface
+- `src/app/admin/leads/page.tsx` — server component structure, data fetching pattern, `revalidate = 0`
+- `src/components/admin/AdminSidebar.tsx` — `/admin/leads` is already in NAV_ITEMS, no sidebar change needed
+- `package.json` — @dnd-kit/core ^6.3.1, @dnd-kit/sortable ^10.0.0, @dnd-kit/utilities ^3.2.2
+
+### Secondary (MEDIUM confidence)
+- `.planning/config.json` — `nyquist_validation: false` confirmed
+
+---
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack: HIGH — all packages verified in package.json; exact primitives verified in KanbanBoard.tsx
+- Architecture: HIGH — all patterns verified from existing codebase analogs
+- Pitfalls: HIGH — derived from direct code inspection and known Next.js App Router behaviors
+- Data layer: HIGH — schema, actions, and query functions all read directly
+
+**Research date:** 2026-06-19
+**Valid until:** Stable indefinitely (no external dependencies; codebase-derived findings)
diff --git a/design-reference/DESIGN-SYSTEM.md b/design-reference/DESIGN-SYSTEM.md
new file mode 100644
index 0000000..af4633c
--- /dev/null
+++ b/design-reference/DESIGN-SYSTEM.md
@@ -0,0 +1,148 @@
+# Design System: iamcavalli Admin & Client Portal (v1.0)
+
+> "Minimalist Premium / Quiet Luxury" — the aesthetic that replaces the old
+> mixed token/hardcoded styling across ClientHub's admin area. First applied
+> to the Lead Pipeline page (Phase: Pipeline redesign); the tokens and
+> primitives documented here are the base for restyling every other section.
+
+## Philosophy
+
+- **Quiet, not loud.** Elevation is nearly invisible (shadow opacity ≤ 0.02),
+ borders are hairline, color is used sparingly and only to carry meaning
+ (status, semantics) — never for decoration.
+- **Density with air.** Generous padding (`py-4 px-6` in tables, `p-4` in
+ kanban columns) paired with small type (`text-xs`, `text-[11px]`) reads as
+ premium rather than cramped.
+- **Numbers are monospace.** Prices, phone numbers, dates, counts — anything
+ tabular/numeric — use `font-mono` and right-alignment so columns of digits
+ line up.
+- **Dual-theme by construction.** Every surface is built from semantic
+ tokens (`bg-card`, `text-muted-foreground`, `border-border`, …), never raw
+ Tailwind palette classes (`bg-white`, `text-slate-900`, …) or hex literals.
+ This is what makes dark mode "just work" without a parallel dark stylesheet.
+- **Motion is functional.** Transitions exist to explain state change (sidebar
+ collapse, hover, drag) — `duration-200`–`duration-350`,
+ `ease-[cubic-bezier(0.4,0,0.2,1)]` for the sidebar specifically. No
+ decorative animation.
+
+## Typography
+
+- **Sans**: Plus Jakarta Sans (weights 300–700), loaded via `next/font/google`
+ as `--font-plus-jakarta-sans`, mapped to Tailwind's `--font-sans` in
+ `@theme`. Used for all UI text.
+- **Mono**: Geist Mono, `--font-geist-mono` → `--font-mono`. Used for numeric
+ data cells only (prices, phone numbers, counts, dates in tables).
+- **Scale conventions**:
+ - Page title: `text-2xl font-semibold tracking-tight`
+ - Page subtitle: `text-xs text-muted-foreground`
+ - Table header cells: `text-[11px] font-semibold uppercase tracking-wider text-muted-foreground`
+ - Table body: `text-sm`
+ - Badge/pill label: `text-[10px]`–`text-xs font-semibold uppercase tracking-wide`
+
+## Color Tokens
+
+Raw values live in `:root` (light) / `.dark` (dark) in `src/app/globals.css`
+and are mapped to Tailwind utilities via `@theme inline`. Components must
+consume the mapped utility, never the raw hex or a Tailwind palette shade.
+
+| Utility | Light | Dark | Usage |
+|---|---|---|---|
+| `bg-background` / `text-foreground` | `#ffffff` / `#1a1a1a` | `#0e1512` / `#f2f4f3` | Page canvas |
+| `bg-card` / `text-card-foreground` | `#ffffff` | `#131a16` | Cards, table, kanban cards, header bar |
+| `bg-muted` / `text-muted-foreground` | `#f9f9f9` / `#71717a` | `#171f1b` / `#9aa39e` | Subtle backgrounds, secondary text, kanban columns |
+| `bg-primary` / `text-primary-foreground` | `#1A463C` | `#3FA88C` | Primary actions, sidebar, active nav state |
+| `bg-accent` | `#DEF168` | `#DEF168` | Rare highlight accent (lime) |
+| `border-border` / `border-input` | `#e5e7eb` | `#26302b` | All hairline borders |
+| `ring-ring` | `#1A463C` | `#3FA88C` | Focus rings |
+| `bg-destructive` | `#ef4444` | `#f87171` | Destructive actions |
+
+**Sidebar exception**: the sidebar stays brand green `bg-[#1A463C]` in both
+themes (it is not a themed surface — it's the constant brand anchor).
+
+**Status/semantic colors** (lead stages, badges) use Tailwind's default
+palette (blue/purple/amber/orange/emerald/red) directly, each paired with an
+explicit `dark:` variant for legibility on dark surfaces — see `StatusBadge`.
+
+## Elevation & Radius
+
+- `--radius: 0.5rem` (base) is wired into `@theme` as `--radius-lg` (`=
+ --radius`) and `--radius-xl` (`= --radius + 0.25rem`), so `rounded-lg` /
+ `rounded-xl` utilities resolve consistently off one token.
+- `--shadow-card: 0 4px 20px rgba(0,0,0,0.01)` → `shadow-card` utility.
+ Default resting elevation for cards, table containers, kanban cards.
+- `--shadow-card-hover: 0 8px 30px rgba(0,0,0,0.02)` → `shadow-card-hover`
+ utility. Applied on hover for interactive cards.
+
+## Layout Conventions
+
+- **Table container**: `bg-card rounded-xl border border-border shadow-card
+ overflow-hidden`.
+- **Table header row**: `bg-muted/50 border-b border-border text-[11px]
+ uppercase tracking-wider text-muted-foreground`.
+- **Table body rows**: `py-4 px-6`, `divide-y divide-border`,
+ `hover:bg-muted/40 transition-colors`.
+- **Kanban column**: `bg-muted/60 rounded-xl border border-border p-4`.
+- **Kanban card**: `bg-card rounded-lg border border-border shadow-sm
+ hover:border-primary/30 hover:shadow-card-hover transition-all`.
+- **Page header**: title `text-2xl font-semibold tracking-tight
+ text-foreground`, subtitle `text-xs text-muted-foreground mt-1`, primary
+ action button `bg-primary text-primary-foreground hover:bg-primary/90`.
+
+## Color/Class Migration Map (dual-theme principle)
+
+When restyling any surface, replace hardcoded classes with token utilities:
+
+| Old (hardcoded) | New (token) |
+|---|---|
+| `bg-white` | `bg-card` |
+| `bg-slate-50` / `bg-gray-50` | `bg-muted` |
+| `border-slate-100` / `border-slate-200` | `border-border` |
+| `text-slate-900` / `text-[#1a1a1a]` | `text-foreground` |
+| `text-slate-500` / `text-slate-400` / `text-[#71717a]` | `text-muted-foreground` |
+| `bg-brand-dark` / `text-[#1A463C]` (as action color) | `bg-primary` / `text-primary` |
+
+## Motion
+
+- **Sidebar collapse**: `transition-[width] duration-350
+ ease-[cubic-bezier(0.4,0,0.2,1)]` on the `