diff --git a/.planning/phases/12-comparison-view/12-RESEARCH.md b/.planning/phases/12-comparison-view/12-RESEARCH.md
new file mode 100644
index 0000000..1d20c6a
--- /dev/null
+++ b/.planning/phases/12-comparison-view/12-RESEARCH.md
@@ -0,0 +1,541 @@
+# Phase 12: Comparison View - Research
+
+**Researched:** 2026-03-17
+**Domain:** React tabular UI, CSS sticky columns, horizontal scroll, delta computation
+**Confidence:** HIGH
+
+## Summary
+
+Phase 12 is a pure frontend phase. No backend changes, no schema changes, no new npm packages. All required data is already returned by `useThread(threadId)` — candidates carry `weightGrams`, `priceCents`, `status`, `productUrl`, `notes`, `pros`, `cons`, `imageFilename`, `categoryIcon`, and rank is derived from sort_order position in the array. The work is entirely in building a `ComparisonTable` component, wiring a third toggle button into the existing view-mode bar, and extending the `candidateViewMode` Zustand union type.
+
+The core CSS challenge is the sticky-first-column + horizontal-scroll table pattern. Modern CSS handles this well as long as `overflow-x: auto` is placed on a wrapper `
`, not the `
` element itself, and the sticky `
` cells in the label column have an explicit background color (otherwise scrolling content bleeds through). Z-index layering is simple for this use case because there is only one sticky axis (the left label column); no sticky top header is needed since the table is not vertically scrollable.
+
+Delta computation is straightforward arithmetic: find the minimum `weightGrams` across candidates that have a value, subtract each candidate's value from that minimum to produce a delta, and render a `+Xg` or `—` string. The "best" cell gets `bg-blue-50` for weight (matching existing blue weight pill color) or `bg-green-50` for price (matching existing green price pill color). Missing data must never display as "0" — a dash placeholder is required by COMP-04, and `formatWeight(null)` already returns `"--"`.
+
+**Primary recommendation:** Build `ComparisonTable.tsx` as a self-contained component that accepts `candidates[]` and `resolvedCandidateId | null`, computes deltas internally with `useMemo`, renders a `
` wrapper around a plain `
`, and uses `sticky left-0 bg-white z-10` on the label `
` cells.
+
+---
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+- Compare mode entry point: Add a third icon to the existing list/grid toggle bar, making it list | grid | compare (three-way toggle)
+- Use `candidateViewMode: 'list' | 'grid' | 'compare'` in uiStore — extends the existing Zustand state
+- Compare icon only appears when 2+ candidates exist in the thread (hidden otherwise)
+- Table orientation: Candidates as columns, attribute labels as rows (classic product-comparison pattern — Amazon/Wirecutter style)
+- Sticky left column for attribute labels; table scrolls horizontally on narrow viewports
+- Attribute row order: Image → Name → Rank → Weight (with delta) → Price (with delta) → Status → Product Link → Notes → Pros → Cons
+- Delta highlighting style: Lightest candidate's weight cell gets a subtle colored background tint (e.g., bg-green-50); cheapest similarly
+- Non-best cells show delta text in neutral gray — no colored badges for deltas, only the "best" cell gets color
+
+### Claude's Discretion
+- "Add Candidate" button visibility when in compare view
+- Image thumbnail sizing in comparison cells (square crop vs wider aspect)
+- Multi-line text rendering strategy (clamped with expand vs full text)
+- Missing data indicator style (dash with label, empty cell, etc.)
+- Delta format: absolute value + delta underneath, or delta only for non-best cells
+- Winner column marking approach (column tint, trophy icon, or both)
+- Resolved thread interactivity (links clickable vs all read-only)
+- Resolution banner behavior in compare view
+- View mode persistence (already in Zustand — whether compare resets on navigation or persists)
+- Compare toggle icon choice (e.g., Lucide `columns-3`, `table-2`, or similar)
+- Table cell padding, border styling, and overall table chrome
+- Column minimum/maximum widths
+- Keyboard accessibility for horizontal scrolling
+
+### Deferred Ideas (OUT OF SCOPE)
+None — discussion stayed within phase scope
+
+
+---
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|-----------------|
+| COMP-01 | User can view candidates side-by-side in a tabular comparison layout (weight, price, images, notes, links, status) | ComparisonTable component; all fields available from useThread hook; no backend changes needed |
+| COMP-02 | User can see relative deltas highlighting the lightest and cheapest candidate with +/- differences | Delta computation via array reduce; best-cell highlight via bg-blue-50 (weight) / bg-green-50 (price); gray delta text for non-best |
+| COMP-03 | Comparison table scrolls horizontally with a sticky label column on narrow viewports | overflow-x-auto wrapper div + sticky left-0 bg-white z-10 on label td cells |
+| COMP-04 | Comparison view displays read-only summary for resolved threads | resolvedCandidateId from useThread; disable mutation actions; winner column visual tint; resolved check pattern established in Phase 11 |
+
+
+---
+
+## Standard Stack
+
+### Core (all already installed — no new packages needed)
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| React 19 | ^19.2.4 | Component rendering | Project stack |
+| Tailwind CSS | v4 | Utility styling | Project stack |
+| Zustand | ^5.0.11 | candidateViewMode state | Already used for list/grid toggle |
+| lucide-react | ^0.577.0 | Toggle icon (`columns-3` confirmed present) | All icons use LucideIcon helper |
+| framer-motion | ^12.37.0 | Optional AnimatePresence for view transition | Already installed |
+
+### Supporting Utilities (already in project)
+| Utility | Location | Purpose |
+|---------|----------|---------|
+| `formatWeight(grams, unit)` | `src/client/lib/formatters.ts` | Weight cell values and delta strings; returns `"--"` for null |
+| `formatPrice(cents, currency)` | `src/client/lib/formatters.ts` | Price cell values and delta strings; returns `"--"` for null |
+| `useWeightUnit()` | `src/client/hooks/useWeightUnit.ts` | Current unit setting |
+| `useCurrency()` | `src/client/hooks/useCurrency.ts` | Current currency setting |
+| `useThread(threadId)` | `src/client/hooks/useThreads.ts` | All candidate data |
+| `RankBadge` | `src/client/components/CandidateListItem.tsx` | Rank medal icons (exported) |
+| `LucideIcon` | `src/client/lib/iconData.tsx` | Icon rendering with fallback |
+
+---
+
+## Architecture Patterns
+
+### Recommended File Structure
+```
+src/client/
+├── components/
+│ └── ComparisonTable.tsx # New: tabular comparison component
+├── stores/
+│ └── uiStore.ts # Modify: extend candidateViewMode union type
+└── routes/threads/
+ └── $threadId.tsx # Modify: add compare branch + third toggle button
+```
+
+### Pattern 1: Sticky Left Column with Horizontal Scroll
+
+**What:** Wrap `
` in `
`. Apply `sticky left-0 bg-white z-10` to every `
` and `
` in the first (label) column.
+
+**When to use:** Any time a table needs a frozen left column with horizontal scrolling.
+
+**Critical pitfall:** The sticky `td` cells MUST have a solid background color. Without `bg-white`, scrolling content bleeds through the "sticky" cell because the cell is transparent.
+
+**Example:**
+```tsx
+// Outer wrapper enables horizontal scroll
+