diff --git a/.planning/phases/03-setups-and-dashboard/03-RESEARCH.md b/.planning/phases/03-setups-and-dashboard/03-RESEARCH.md
new file mode 100644
index 0000000..2ae86cc
--- /dev/null
+++ b/.planning/phases/03-setups-and-dashboard/03-RESEARCH.md
@@ -0,0 +1,540 @@
+# Phase 3: Setups and Dashboard - Research
+
+**Researched:** 2026-03-15
+**Domain:** Full-stack CRUD (Drizzle + Hono + React) with navigation restructure
+**Confidence:** HIGH
+
+## Summary
+
+Phase 3 adds two features: (1) named setups (loadouts) that compose collection items with live weight/cost totals, and (2) a dashboard home page with summary cards. The codebase has strong established patterns from Phases 1 and 2 -- database schema in Drizzle, service layer with DB injection, Hono routes with Zod validation, TanStack Query hooks, and Zustand UI state. This phase follows identical patterns with one significant difference: the many-to-many relationship between items and setups via a junction table (`setup_items`).
+
+The navigation restructure moves the current `/` (gear + planning tabs) to `/collection` and replaces `/` with a dashboard. This requires moving the existing `index.tsx` route content to a new `collection/index.tsx` route, creating new `/setups` routes, and making TotalsBar route-aware for contextual stats.
+
+**Primary recommendation:** Follow the exact thread CRUD pattern for setups (schema, service, routes, hooks, components), add a `setup_items` junction table for the many-to-many relationship, compute setup totals server-side via SQL aggregation, and restructure routes with TanStack Router file-based routing.
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+- Setup item selection: Checklist picker in SlideOutPanel, items grouped by category with emoji headers, toggle via checkboxes, "Done" button to confirm, items shared across setups
+- Setup creation: Inline name input + create button (same pattern as thread creation)
+- Setup display: Card grid grouped by category reusing ItemCard with small x remove icon, per-category subtotals in CategoryHeader
+- Setup totals: Sticky bar at top showing setup name, item count, total weight, total cost (reuses TotalsBar pattern)
+- Dashboard cards: Three equal-width cards (Collection, Planning, Setups) side by side on desktop, stacking on mobile, with summary stats on each card
+- Dashboard header: "GearBox" title only, no welcome message
+- Navigation: `/` = Dashboard, `/collection` = Gear|Planning tabs, `/setups` = Setups list, `/setups/:id` = Setup detail
+- "GearBox" title in TotalsBar is always a clickable link back to dashboard
+- No breadcrumbs or back arrows -- GearBox title link is the only back navigation
+
+### Claude's Discretion
+- Setup list card design (stats/info per setup card beyond name and totals)
+- Exact Tailwind styling, spacing, and transitions for dashboard cards
+- Setup detail page layout specifics beyond card grid + sticky totals
+- How checklist picker handles large number of items (scroll behavior)
+- Error states and loading skeletons
+
+### Deferred Ideas (OUT OF SCOPE)
+None -- discussion stayed within phase scope
+
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|-----------------|
+| SETP-01 | User can create named setups (e.g. "Summer Bikepacking") | Setup CRUD: schema, service, routes, hooks -- follows thread creation pattern exactly |
+| SETP-02 | User can add/remove collection items to a setup | Junction table `setup_items`, checklist picker in SlideOutPanel, batch sync endpoint |
+| SETP-03 | User can see total weight and cost for a setup | Server-side SQL aggregation via `setup_items` JOIN `items`, setup totals endpoint |
+| DASH-01 | User sees dashboard home page with cards linking to collection, threads, and setups | New `/` route with three summary cards, existing content moves to `/collection` |
+
+
+## Standard Stack
+
+### Core (already installed, no new dependencies)
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| drizzle-orm | 0.45.1 | Database schema + queries | Already used for items, threads |
+| hono | 4.12.8 | API routes | Already used for all server routes |
+| @hono/zod-validator | 0.7.6 | Request validation | Already used on all routes |
+| zod | 4.3.6 | Schema validation | Already used in shared schemas |
+| @tanstack/react-query | 5.90.21 | Data fetching + cache | Already used for items, threads, totals |
+| @tanstack/react-router | 1.167.0 | File-based routing | Already used, auto-generates route tree |
+| zustand | 5.0.11 | UI state | Already used for panel/dialog state |
+| tailwindcss | 4.2.1 | Styling | Already used throughout |
+
+### Supporting
+No new libraries needed. Phase 3 uses only existing dependencies.
+
+### Alternatives Considered
+None -- all decisions are locked to existing stack patterns.
+
+**Installation:**
+```bash
+# No new packages needed
+```
+
+## Architecture Patterns
+
+### New Files Structure
+```
+src/
+ db/
+ schema.ts # ADD: setups + setup_items tables
+ shared/
+ schemas.ts # ADD: setup Zod schemas
+ types.ts # ADD: Setup + SetupItem types
+ server/
+ routes/
+ setups.ts # NEW: setup CRUD routes
+ services/
+ setup.service.ts # NEW: setup business logic
+ index.ts # UPDATE: mount setup routes
+ client/
+ routes/
+ index.tsx # REWRITE: dashboard page
+ collection/
+ index.tsx # NEW: moved from current index.tsx (gear + planning tabs)
+ setups/
+ index.tsx # NEW: setups list page
+ $setupId.tsx # NEW: setup detail page
+ hooks/
+ useSetups.ts # NEW: setup query/mutation hooks
+ components/
+ SetupCard.tsx # NEW: setup list card
+ ItemPicker.tsx # NEW: checklist picker for SlideOutPanel
+ DashboardCard.tsx # NEW: dashboard summary card
+ stores/
+ uiStore.ts # UPDATE: add setup-related UI state
+tests/
+ helpers/
+ db.ts # UPDATE: add setups + setup_items tables
+ services/
+ setup.service.test.ts # NEW: setup service tests
+ routes/
+ setups.test.ts # NEW: setup route tests
+```
+
+### Pattern 1: Many-to-Many Junction Table
+**What:** `setup_items` links setups to items (items can belong to multiple setups)
+**When to use:** This is the only new DB pattern in this phase
+
+```typescript
+// In src/db/schema.ts
+export const setups = sqliteTable("setups", {
+ id: integer("id").primaryKey({ autoIncrement: true }),
+ name: text("name").notNull(),
+ createdAt: integer("created_at", { mode: "timestamp" })
+ .notNull()
+ .$defaultFn(() => new Date()),
+ updatedAt: integer("updated_at", { mode: "timestamp" })
+ .notNull()
+ .$defaultFn(() => new Date()),
+});
+
+export const setupItems = sqliteTable("setup_items", {
+ id: integer("id").primaryKey({ autoIncrement: true }),
+ setupId: integer("setup_id")
+ .notNull()
+ .references(() => setups.id, { onDelete: "cascade" }),
+ itemId: integer("item_id")
+ .notNull()
+ .references(() => items.id, { onDelete: "cascade" }),
+ addedAt: integer("added_at", { mode: "timestamp" })
+ .notNull()
+ .$defaultFn(() => new Date()),
+});
+```
+
+**Key design decisions:**
+- `onDelete: "cascade"` on both FKs: deleting a setup removes its setup_items; deleting a collection item removes it from all setups
+- No unique constraint on (setupId, itemId) at DB level -- enforce in service layer for better error messages
+- `addedAt` for potential future ordering, but not critical for v1
+
+### Pattern 2: Batch Sync for Setup Items
+**What:** Instead of individual add/remove endpoints, use a single "sync" endpoint that receives the full list of selected item IDs
+**When to use:** When the checklist picker submits all selections at once via "Done" button
+
+```typescript
+// In setup.service.ts
+export function syncSetupItems(db: Db = prodDb, setupId: number, itemIds: number[]) {
+ return db.transaction((tx) => {
+ // Delete all existing setup_items for this setup
+ tx.delete(setupItems).where(eq(setupItems.setupId, setupId)).run();
+ // Insert new ones
+ if (itemIds.length > 0) {
+ tx.insert(setupItems)
+ .values(itemIds.map(itemId => ({ setupId, itemId })))
+ .run();
+ }
+ });
+}
+```
+
+**Why batch sync over individual add/remove:**
+- The checklist picker has a "Done" button that submits all at once
+- Simpler than tracking individual toggles
+- Single transaction = atomic operation
+- Still need a single-item remove for the x button on cards (separate endpoint)
+
+### Pattern 3: Setup Totals via SQL Aggregation
+**What:** Compute setup weight/cost totals server-side by joining `setup_items` with `items`
+**When to use:** For the setup detail page totals bar and setup list cards
+
+```typescript
+// In setup.service.ts
+export function getSetupWithItems(db: Db = prodDb, setupId: number) {
+ const setup = db.select().from(setups).where(eq(setups.id, setupId)).get();
+ if (!setup) return null;
+
+ const itemList = db
+ .select({
+ id: items.id,
+ name: items.name,
+ weightGrams: items.weightGrams,
+ priceCents: items.priceCents,
+ categoryId: items.categoryId,
+ notes: items.notes,
+ productUrl: items.productUrl,
+ imageFilename: items.imageFilename,
+ createdAt: items.createdAt,
+ updatedAt: items.updatedAt,
+ categoryName: categories.name,
+ categoryEmoji: categories.emoji,
+ })
+ .from(setupItems)
+ .innerJoin(items, eq(setupItems.itemId, items.id))
+ .innerJoin(categories, eq(items.categoryId, categories.id))
+ .where(eq(setupItems.setupId, setupId))
+ .all();
+
+ return { ...setup, items: itemList };
+}
+```
+
+**Totals are computed client-side from the items array** (not a separate endpoint) since the setup detail page already fetches all items. This avoids an extra API call and keeps totals always in sync with displayed data.
+
+For the setup list cards (showing totals per setup), use a SQL subquery:
+
+```typescript
+export function getAllSetups(db: Db = prodDb) {
+ return db
+ .select({
+ id: setups.id,
+ name: setups.name,
+ createdAt: setups.createdAt,
+ updatedAt: setups.updatedAt,
+ itemCount: sql`(
+ SELECT COUNT(*) FROM setup_items
+ WHERE setup_items.setup_id = setups.id
+ )`.as("item_count"),
+ totalWeight: sql`COALESCE((
+ SELECT SUM(items.weight_grams) FROM setup_items
+ INNER JOIN items ON setup_items.item_id = items.id
+ WHERE setup_items.setup_id = setups.id
+ ), 0)`.as("total_weight"),
+ totalCost: sql`COALESCE((
+ SELECT SUM(items.price_cents) FROM setup_items
+ INNER JOIN items ON setup_items.item_id = items.id
+ WHERE setup_items.setup_id = setups.id
+ ), 0)`.as("total_cost"),
+ })
+ .from(setups)
+ .orderBy(desc(setups.updatedAt))
+ .all();
+}
+```
+
+### Pattern 4: Route-Aware TotalsBar
+**What:** Make TotalsBar show different content based on the current route
+**When to use:** Dashboard shows "GearBox" title only; collection shows global totals; setup detail shows setup-specific totals
+
+```typescript
+// TotalsBar accepts optional props to override default behavior
+interface TotalsBarProps {
+ title?: string; // Override the title text
+ stats?: TotalsStat[]; // Override stats display (empty = title only)
+ linkTo?: string; // Make title a link (defaults to "/")
+}
+```
+
+**Approach:** Rather than making TotalsBar read route state internally, have each page pass the appropriate stats. This keeps TotalsBar a pure presentational component.
+
+- Dashboard page: `` (title only, no stats, no link since already on dashboard)
+- Collection page: `` (current behavior)
+- Setup detail: ``
+- Thread detail: keep current behavior
+
+The "GearBox" title becomes a `` on all pages except the dashboard itself.
+
+### Pattern 5: TanStack Router File-Based Routing
+**What:** New route files auto-register via TanStack Router plugin
+**When to use:** Creating `/collection`, `/setups`, `/setups/:id` routes
+
+```
+src/client/routes/
+ __root.tsx # Existing root layout
+ index.tsx # REWRITE: Dashboard
+ collection/
+ index.tsx # NEW: current index.tsx content moves here
+ setups/
+ index.tsx # NEW: setups list
+ $setupId.tsx # NEW: setup detail
+ threads/
+ $threadId.tsx # Existing, unchanged
+```
+
+The TanStack Router plugin will auto-generate `routeTree.gen.ts` with the new routes. Route files use `createFileRoute("/path")` -- the path must match the file location.
+
+### Pattern 6: Dashboard Summary Stats
+**What:** Dashboard cards need aggregate data from multiple domains
+**When to use:** The dashboard page
+
+The dashboard needs: collection item count + total weight + total cost, active thread count, setup count. Two approaches:
+
+**Recommended: Aggregate on client from existing hooks**
+- `useTotals()` already provides collection stats
+- `useThreads()` provides thread list (count from `.length`)
+- New `useSetups()` provides setup list (count from `.length`)
+
+This avoids a new dashboard-specific API endpoint. Three parallel queries that TanStack Query handles efficiently with its deduplication.
+
+### Anti-Patterns to Avoid
+- **Don't add setup state to Zustand beyond UI concerns:** Setup data belongs in TanStack Query cache, not Zustand. Zustand is only for panel open/close state.
+- **Don't compute totals in the component loop:** Use SQL aggregation for list views, and derive from the fetched items array for detail views.
+- **Don't create a separate "dashboard totals" API:** Reuse existing totals endpoint + new setup/thread counts from their list endpoints.
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| Many-to-many sync | Custom diff logic | Delete-all + re-insert in transaction | Simpler, atomic, handles edge cases |
+| Route generation | Manual route registration | TanStack Router file-based plugin | Already configured, auto-generates types |
+| Data fetching cache | Custom cache | TanStack Query | Already used, handles invalidation |
+| SQL totals aggregation | Client-side loops over raw data | SQL COALESCE + SUM subqueries | Consistent with existing totals.service.ts pattern |
+
+**Key insight:** Every pattern in this phase has a direct precedent in Phases 1-2. The only new concept is the junction table.
+
+## Common Pitfalls
+
+### Pitfall 1: Stale Setup Totals After Item Edit
+**What goes wrong:** User edits a collection item's weight/price, but setup detail page shows old totals
+**Why it happens:** Setup query cache not invalidated when items change
+**How to avoid:** In `useUpdateItem` and `useDeleteItem` mutation `onSuccess`, also invalidate `["setups"]` query key
+**Warning signs:** Totals don't update until page refresh
+
+### Pitfall 2: Orphaned Setup Items After Collection Item Deletion
+**What goes wrong:** Deleting a collection item leaves dangling references in `setup_items`
+**Why it happens:** Missing cascade or no FK constraint
+**How to avoid:** `onDelete: "cascade"` on `setupItems.itemId` FK -- already specified in schema pattern above
+**Warning signs:** Setup shows items that no longer exist in collection
+
+### Pitfall 3: Route Migration Breaking Existing Links
+**What goes wrong:** Moving `/` content to `/collection` breaks hardcoded links like the "Back to planning" link in thread detail
+**Why it happens:** Thread detail page currently links to `{ to: "/", search: { tab: "planning" } }`
+**How to avoid:** Update ALL internal links: thread detail back link, resolution dialog redirect, floating add button visibility check
+**Warning signs:** Clicking links after restructure navigates to wrong page
+
+### Pitfall 4: TanStack Router Route Tree Not Regenerating
+**What goes wrong:** New route files exist but routes 404
+**Why it happens:** Vite dev server needs restart, or route file doesn't export `Route` correctly
+**How to avoid:** Use `createFileRoute("/correct/path")` matching the file location. Restart dev server after adding new route directories.
+**Warning signs:** `routeTree.gen.ts` doesn't include new routes
+
+### Pitfall 5: Floating Add Button Showing on Wrong Pages
+**What goes wrong:** The floating "+" button (for adding items) appears on dashboard or setups pages
+**Why it happens:** Current logic only hides it on thread pages (`!threadMatch`)
+**How to avoid:** Update __root.tsx to only show the floating add button on `/collection` route (gear tab)
+**Warning signs:** "+" button visible on dashboard or setup pages
+
+### Pitfall 6: TotalsBar in Root Layout vs Per-Page
+**What goes wrong:** TotalsBar in `__root.tsx` shows global stats on every page including dashboard
+**Why it happens:** TotalsBar is currently rendered unconditionally in root layout
+**How to avoid:** Either (a) make TotalsBar route-aware via props from root, or (b) move TotalsBar out of root layout and render per-page. Option (a) is simpler -- pass a mode/props based on route matching.
+**Warning signs:** Dashboard shows stats in TotalsBar instead of just the title
+
+## Code Examples
+
+### Setup Zod Schemas
+```typescript
+// In src/shared/schemas.ts
+export const createSetupSchema = z.object({
+ name: z.string().min(1, "Setup name is required"),
+});
+
+export const updateSetupSchema = z.object({
+ name: z.string().min(1).optional(),
+});
+
+export const syncSetupItemsSchema = z.object({
+ itemIds: z.array(z.number().int().positive()),
+});
+```
+
+### Setup Hooks Pattern
+```typescript
+// In src/client/hooks/useSetups.ts -- follows useThreads.ts pattern exactly
+export function useSetups() {
+ return useQuery({
+ queryKey: ["setups"],
+ queryFn: () => apiGet("/api/setups"),
+ });
+}
+
+export function useSetup(setupId: number | null) {
+ return useQuery({
+ queryKey: ["setups", setupId],
+ queryFn: () => apiGet(`/api/setups/${setupId}`),
+ enabled: setupId != null,
+ });
+}
+
+export function useSyncSetupItems(setupId: number) {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (itemIds: number[]) =>
+ apiPut<{ success: boolean }>(`/api/setups/${setupId}/items`, { itemIds }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["setups"] });
+ },
+ });
+}
+```
+
+### Remove Single Item from Setup
+```typescript
+// Separate from batch sync -- used by the x button on item cards in setup detail
+export function useRemoveSetupItem(setupId: number) {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (itemId: number) =>
+ apiDelete<{ success: boolean }>(`/api/setups/${setupId}/items/${itemId}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["setups"] });
+ },
+ });
+}
+```
+
+### Dashboard Route
+```typescript
+// src/client/routes/index.tsx -- new dashboard
+import { createFileRoute, Link } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/")({
+ component: DashboardPage,
+});
+
+function DashboardPage() {
+ // Three hooks in parallel -- TanStack Query deduplicates
+ const { data: totals } = useTotals();
+ const { data: threads } = useThreads();
+ const { data: setups } = useSetups();
+
+ const activeThreadCount = threads?.filter(t => t.status === "active").length ?? 0;
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+```
+
+### Collection Route (moved from current index.tsx)
+```typescript
+// src/client/routes/collection/index.tsx
+import { createFileRoute } from "@tanstack/react-router";
+import { z } from "zod";
+
+const searchSchema = z.object({
+ tab: z.enum(["gear", "planning"]).catch("gear"),
+});
+
+export const Route = createFileRoute("/collection/")({
+ validateSearch: searchSchema,
+ component: CollectionPage,
+});
+
+function CollectionPage() {
+ // Exact same content as current HomePage in src/client/routes/index.tsx
+ // Just update navigation targets (e.g., handleTabChange navigates to "/collection")
+}
+```
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| Current `/` has gear+planning | `/` becomes dashboard, content moves to `/collection` | Phase 3 | All internal links must update |
+| TotalsBar always shows global stats | TotalsBar becomes route-aware with contextual stats | Phase 3 | Root layout needs route matching logic |
+| No many-to-many relationships | `setup_items` junction table | Phase 3 | New Drizzle pattern for this project |
+
+## Open Questions
+
+1. **Should setup deletion require confirmation?**
+ - What we know: CONTEXT.md mentions using ConfirmDialog for setup deletion
+ - What's unclear: Whether to also confirm when removing all items from a setup
+ - Recommendation: Use ConfirmDialog for setup deletion (destructive). No confirmation for removing individual items from setup (non-destructive, per CONTEXT.md decision).
+
+2. **Should `useThreads` on dashboard include resolved threads for the count?**
+ - What we know: Dashboard "Planning" card shows active thread count
+ - What's unclear: Whether to show "3 active" or "3 active / 5 total"
+ - Recommendation: Show only active count for simplicity. `useThreads(false)` already filters to active.
+
+## Validation Architecture
+
+### Test Framework
+| Property | Value |
+|----------|-------|
+| Framework | bun:test (built into Bun) |
+| Config file | None (Bun built-in, runs from `package.json` `"test": "bun test"`) |
+| Quick run command | `bun test tests/services/setup.service.test.ts` |
+| Full suite command | `bun test` |
+
+### Phase Requirements to Test Map
+| Req ID | Behavior | Test Type | Automated Command | File Exists? |
+|--------|----------|-----------|-------------------|-------------|
+| SETP-01 | Create/list/delete named setups | unit + integration | `bun test tests/services/setup.service.test.ts` | No - Wave 0 |
+| SETP-01 | Setup CRUD API routes | integration | `bun test tests/routes/setups.test.ts` | No - Wave 0 |
+| SETP-02 | Add/remove items to setup (junction table) | unit | `bun test tests/services/setup.service.test.ts` | No - Wave 0 |
+| SETP-02 | Setup items sync API route | integration | `bun test tests/routes/setups.test.ts` | No - Wave 0 |
+| SETP-03 | Setup totals (weight/cost aggregation) | unit | `bun test tests/services/setup.service.test.ts` | No - Wave 0 |
+| DASH-01 | Dashboard summary data | manual-only | Manual browser verification | N/A (UI-only, data from existing endpoints) |
+
+### Sampling Rate
+- **Per task commit:** `bun test tests/services/setup.service.test.ts && bun test tests/routes/setups.test.ts`
+- **Per wave merge:** `bun test`
+- **Phase gate:** Full suite green before `/gsd:verify-work`
+
+### Wave 0 Gaps
+- [ ] `tests/services/setup.service.test.ts` -- covers SETP-01, SETP-02, SETP-03
+- [ ] `tests/routes/setups.test.ts` -- covers SETP-01, SETP-02 API layer
+- [ ] `tests/helpers/db.ts` -- needs `setups` and `setup_items` CREATE TABLE statements added
+
+## Sources
+
+### Primary (HIGH confidence)
+- Existing codebase: `src/db/schema.ts`, `src/server/services/thread.service.ts`, `src/server/routes/threads.ts` -- direct pattern references
+- Existing codebase: `src/client/hooks/useThreads.ts`, `src/client/stores/uiStore.ts` -- client-side patterns
+- Existing codebase: `tests/services/thread.service.test.ts`, `tests/helpers/db.ts` -- test infrastructure patterns
+- Existing codebase: `src/client/routes/__root.tsx`, `src/client/routes/index.tsx` -- routing patterns
+
+### Secondary (MEDIUM confidence)
+- TanStack Router file-based routing conventions -- verified against existing `routeTree.gen.ts` auto-generation
+
+### Tertiary (LOW confidence)
+- None
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack: HIGH -- no new dependencies, all patterns established in Phases 1-2
+- Architecture: HIGH -- direct 1:1 mapping from thread patterns to setup patterns, only new concept is junction table
+- Pitfalls: HIGH -- identified from direct codebase analysis (hardcoded links, TotalsBar in root, cascade behavior)
+
+**Research date:** 2026-03-15
+**Valid until:** 2026-04-15 (stable -- no external dependencies changing)