diff --git a/.planning/phases/13-setup-impact-preview/13-RESEARCH.md b/.planning/phases/13-setup-impact-preview/13-RESEARCH.md
new file mode 100644
index 0000000..61d2b55
--- /dev/null
+++ b/.planning/phases/13-setup-impact-preview/13-RESEARCH.md
@@ -0,0 +1,518 @@
+# Phase 13: Setup Impact Preview - Research
+
+**Researched:** 2026-03-17
+**Domain:** Pure frontend — delta computation + UI (React, Zustand, React Query)
+**Confidence:** HIGH
+
+---
+
+## Summary
+
+Phase 13 adds a setup-selector dropdown to the thread detail header. When a user picks a setup, every candidate card and list row gains two delta indicators: weight delta and cost delta. The computation has two modes determined at render time — replace mode (a setup item exists in the same category as the thread) and add mode (no category match). The entire feature is a pure frontend addition: all required data is already available through existing hooks with zero backend or schema changes needed.
+
+The delta logic is straightforward arithmetic over nullable numbers: `candidate.weightGrams - replacedItem.weightGrams` in replace mode, or `candidate.weightGrams` in add mode. The only real complexity is the null-weight indicator (IMPC-04) and driving the selected setup ID through Zustand state so it persists across view-mode switches within the same thread session.
+
+**Primary recommendation:** Add `selectedSetupId: number | null` to uiStore, render a setup dropdown in the thread header, compute deltas in a `useMemo` inside a new `useImpactDeltas` hook, and render inline delta indicators below the candidate weight/price badges in both list and grid views.
+
+---
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|-----------------|
+| IMPC-01 | User can select a setup and see weight and cost delta for each candidate | `useSetups()` returns all setups for dropdown; `useSetup(id)` returns items with categoryId for matching; delta computed in useMemo |
+| IMPC-02 | Impact preview auto-detects replace mode when a setup item exists in the same category as the thread | Thread has `categoryId` (from `threads.categoryId`); setup items have `categoryId` via join; match on `categoryId` equality |
+| IMPC-03 | Impact preview shows add mode (pure addition) when no category match exists in the selected setup | Default when no setup item matches `thread.categoryId`; label clearly as "+add" |
+| IMPC-04 | Candidates with missing weight data show a clear indicator instead of misleading zero deltas | `candidate.weightGrams == null` → render `"-- (no weight data)"` instead of computing |
+
+
+---
+
+## Standard Stack
+
+### Core
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| React 19 | ^19.2.4 | UI rendering | Project foundation |
+| Zustand | ^5.0.11 | `selectedSetupId` UI state | Established pattern for all UI-only state (panel open/close, view mode) |
+| TanStack React Query | ^5.90.21 | `useSetup(id)` for setup items | Established data fetching pattern |
+| Tailwind CSS v4 | ^4.2.1 | Delta badge styling | Project styling system |
+
+### Supporting
+| Library | Version | Purpose | When to Use |
+|---------|---------|---------|-------------|
+| framer-motion | ^12.37.0 | Optional entrance animation for delta indicators | Already installed; use AnimatePresence if subtle fade needed |
+| lucide-react | ^0.577.0 | Dropdown chevron icon, delta arrow icons | Project icon system |
+
+### No New Dependencies
+This phase requires zero new npm dependencies. All needed libraries are installed.
+
+**Installation:**
+```bash
+# No new packages needed
+```
+
+---
+
+## Architecture Patterns
+
+### Recommended Project Structure
+```
+src/client/
+├── stores/uiStore.ts # Add selectedSetupId: number | null + setter
+├── hooks/
+│ └── useImpactDeltas.ts # New: compute add/replace deltas per candidate
+├── components/
+│ ├── SetupImpactSelector.tsx # New: setup dropdown rendered in thread header
+│ └── ImpactDeltaBadge.tsx # New (or inline): weight/cost delta pill
+└── routes/threads/$threadId.tsx # Add selector to header, pass deltas down
+```
+
+### Pattern 1: selectedSetupId in Zustand
+
+**What:** Store selected setup ID as UI state in `uiStore.ts`, not as URL state or server state.
+
+**When to use:** The selection is ephemeral per session (no permalink needed), needs to survive view-mode switches (list/grid/compare), and must be accessible from any component in the thread page without prop drilling.
+
+**Example:**
+```typescript
+// In uiStore.ts — add to UIState interface
+selectedSetupId: number | null;
+setSelectedSetupId: (id: number | null) => void;
+
+// In create() initializer
+selectedSetupId: null,
+setSelectedSetupId: (id) => set({ selectedSetupId: id }),
+```
+
+### Pattern 2: useImpactDeltas Hook
+
+**What:** A pure computation hook that accepts candidates + a setup's item list + the thread's categoryId, and returns per-candidate delta objects.
+
+**When to use:** Delta computation must run in a single place so list, grid, and compare views all show consistent numbers.
+
+**Interface:**
+```typescript
+// src/client/hooks/useImpactDeltas.ts
+import type { SetupItemWithCategory } from "./useSetups";
+
+interface CandidateInput {
+ id: number;
+ weightGrams: number | null;
+ priceCents: number | null;
+}
+
+type DeltaMode = "replace" | "add" | "none"; // "none" = no setup selected
+
+interface CandidateDelta {
+ candidateId: number;
+ mode: DeltaMode;
+ weightDelta: number | null; // null = candidate has no weight data
+ priceDelta: number | null; // null = candidate has no price data
+ replacedItemName: string | null; // populated in replace mode for tooltip
+}
+
+interface ImpactDeltas {
+ mode: DeltaMode;
+ deltas: Record;
+}
+
+export function useImpactDeltas(
+ candidates: CandidateInput[],
+ setupItems: SetupItemWithCategory[] | undefined,
+ threadCategoryId: number,
+): ImpactDeltas
+```
+
+**Logic:**
+```typescript
+// Source: project codebase pattern — mirrors ComparisonTable useMemo
+const impactDeltas = useMemo(() => {
+ if (!setupItems) return { mode: "none", deltas: {} };
+
+ // Find replaced item: setup item whose categoryId matches thread's categoryId
+ const replacedItem = setupItems.find(
+ (item) => item.categoryId === threadCategoryId
+ ) ?? null;
+
+ const mode: DeltaMode = replacedItem ? "replace" : "add";
+
+ const deltas: Record = {};
+ for (const c of candidates) {
+ let weightDelta: number | null = null;
+ let priceDelta: number | null = null;
+
+ if (c.weightGrams != null) {
+ weightDelta = mode === "replace" && replacedItem?.weightGrams != null
+ ? c.weightGrams - replacedItem.weightGrams
+ : c.weightGrams;
+ }
+ // priceCents is integer (cents), same arithmetic
+ if (c.priceCents != null) {
+ priceDelta = mode === "replace" && replacedItem?.priceCents != null
+ ? c.priceCents - replacedItem.priceCents
+ : c.priceCents;
+ }
+
+ deltas[c.id] = {
+ candidateId: c.id,
+ mode,
+ weightDelta,
+ priceDelta,
+ replacedItemName: replacedItem?.name ?? null,
+ };
+ }
+
+ return { mode, deltas };
+}, [candidates, setupItems, threadCategoryId]);
+```
+
+### Pattern 3: SetupImpactSelector Component
+
+**What:** A compact `