diff --git a/.planning/phases/07-setup-wizard/07-PATTERNS.md b/.planning/phases/07-setup-wizard/07-PATTERNS.md
new file mode 100644
index 0000000..62d6a78
--- /dev/null
+++ b/.planning/phases/07-setup-wizard/07-PATTERNS.md
@@ -0,0 +1,284 @@
+# Phase 7: Setup Wizard - Pattern Map
+
+**Mapped:** 2026-04-20
+**Files analyzed:** 9
+**Analogs found:** 9 / 9
+
+## File Classification
+
+| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
+|-------------------|------|-----------|----------------|---------------|
+| `src/pages/SetupPage.tsx` | page | request-response | `src/pages/LoginPage.tsx` | exact |
+| `src/components/setup/WizardStepper.tsx` | component | transform | `src/components/dashboard/SummaryStrip.tsx` | role-match |
+| `src/components/setup/IncomeStep.tsx` | component | request-response | `src/pages/SettingsPage.tsx` (form section) | role-match |
+| `src/components/setup/RecurringItemsStep.tsx` | component | transform | `src/pages/DashboardPage.tsx` (grouped data) | partial |
+| `src/components/setup/ReviewStep.tsx` | component | transform | `src/components/dashboard/SummaryStrip.tsx` | role-match |
+| `src/components/setup/AllocationBar.tsx` | component | transform | `src/components/dashboard/StatCard.tsx` | role-match |
+| `src/components/setup/PresetItemRow.tsx` | component | request-response | `src/components/dashboard/CategorySection.tsx` | partial |
+| `src/App.tsx` (modify) | route | request-response | `src/App.tsx` (existing) | exact |
+| `src/pages/DashboardPage.tsx` (modify) | page | request-response | `src/pages/DashboardPage.tsx` (existing) | exact |
+
+## Pattern Assignments
+
+### `src/pages/SetupPage.tsx` (page, request-response)
+
+**Analog:** `src/pages/LoginPage.tsx`
+
+**Imports pattern** (lines 1-9):
+```typescript
+import { useState } from "react"
+import { Link, useNavigate } from "react-router-dom"
+import { useTranslation } from "react-i18next"
+import { useAuth } from "@/hooks/useAuth"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+```
+
+**Page layout pattern** (lines 34-36):
+```typescript
+// Standalone centered card outside AppLayout -- same pattern as LoginPage
+return (
+
+
+```
+
+**Async action with loading state** (lines 20-31):
+```typescript
+const [loading, setLoading] = useState(false)
+
+async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ setError("")
+ setLoading(true)
+ try {
+ await signIn(email, password)
+ navigate("/")
+ } catch (err) {
+ setError(err instanceof Error ? err.message : t("common.error"))
+ } finally {
+ setLoading(false)
+ }
+}
+```
+
+**Key adaptation:** SetupPage uses `max-w-2xl` (not `max-w-sm`) per CONTEXT decision. It manages multi-step state internally with useState + localStorage sync.
+
+---
+
+### `src/components/setup/IncomeStep.tsx` (component, request-response)
+
+**Analog:** `src/pages/SettingsPage.tsx` (form input section)
+
+**Form field pattern** (lines 88-96):
+```typescript
+
+
+
+ setProfile((p) => p && { ...p, display_name: e.target.value })
+ }
+ />
+
+```
+
+**Button with disabled state** (line 133):
+```typescript
+
+```
+
+---
+
+### `src/components/setup/WizardStepper.tsx` (component, transform)
+
+**Analog:** `src/components/dashboard/SummaryStrip.tsx`
+
+**Presentational component pattern** (lines 1-16):
+```typescript
+// Props interface with clear typing, named export, no hooks beyond props
+interface SummaryStripProps {
+ income: { value: string; budgeted: string }
+ expenses: { value: string; budgeted: string }
+ balance: { value: string; isPositive: boolean }
+ t: (key: string) => string
+}
+
+export function SummaryStrip({ income, expenses, balance, t }: SummaryStripProps) {
+ return (
+
+ {/* child components */}
+
+ )
+}
+```
+
+**Key adaptation:** WizardStepper accepts `currentStep: 1|2|3` and `onStepClick: (step) => void` props. Renders 3 numbered circles with connecting lines.
+
+---
+
+### `src/components/setup/RecurringItemsStep.tsx` (component, transform)
+
+**Analog:** `src/pages/DashboardPage.tsx` (grouped items pattern)
+
+**Grouping by category type** (lines 138-155):
+```typescript
+const groupedSections = useMemo(() =>
+ CATEGORY_TYPES_ALL
+ .map((type) => {
+ const groupItems = items.filter((i) => i.category?.type === type)
+ if (groupItems.length === 0) return null
+ return {
+ type,
+ label: t(`categories.types.${type}`),
+ items: groupItems,
+ }
+ })
+ .filter((g): g is NonNullable => g !== null),
+ [items, t]
+)
+```
+
+**Key adaptation:** Groups PRESETS by `type` field. Each group has a header and list of PresetItemRow components.
+
+---
+
+### `src/components/setup/AllocationBar.tsx` (component, transform)
+
+**Analog:** `src/components/dashboard/SummaryStrip.tsx`
+
+**Conditional color class pattern** (line 47):
+```typescript
+valueClassName={balance.isPositive ? "text-on-budget" : "text-over-budget"}
+```
+
+**Key adaptation:** Displays "Remaining: {amount}" with red text when negative. Uses sticky positioning.
+
+---
+
+### `src/App.tsx` (modify - add /setup route)
+
+**Existing routing pattern** (lines 28-65):
+```typescript
+export default function App() {
+ return (
+
+ } />
+ } />
+ {/* Add /setup here -- protected but outside AppLayout */}
+ }>
+ } />
+ ...
+
+ } />
+
+ )
+}
+```
+
+**New route insertion point** -- after public routes, before AppLayout route:
+```typescript
+} />
+```
+
+---
+
+### `src/pages/DashboardPage.tsx` (modify - add first-run redirect)
+
+**Pattern to add** (at top of DashboardPage component, before existing logic):
+```typescript
+// Source: useFirstRunState hook (src/hooks/useFirstRunState.ts)
+import { useFirstRunState } from "@/hooks/useFirstRunState"
+import { Navigate } from "react-router-dom"
+
+// Inside component body, early return:
+const { isFirstRun, loading: firstRunLoading } = useFirstRunState()
+if (firstRunLoading) return
+if (isFirstRun) return
+```
+
+---
+
+## Shared Patterns
+
+### Toast Notifications
+**Source:** `src/pages/SettingsPage.tsx` lines 4, 56-59
+**Apply to:** SetupPage (on completion and on error)
+```typescript
+import { toast } from "sonner"
+
+// Success:
+toast.success(t("settings.saved"))
+// Error:
+toast.error(t("common.error"))
+```
+
+### Supabase Profile Update
+**Source:** `src/pages/SettingsPage.tsx` lines 44-62
+**Apply to:** SetupPage (marking setup_completed = true)
+```typescript
+import { supabase } from "@/lib/supabase"
+
+const { error } = await supabase
+ .from("profiles")
+ .update({ setup_completed: true })
+ .eq("id", user.id)
+```
+
+### useCategories Mutation
+**Source:** `src/hooks/useCategories.ts` lines 21-38
+**Apply to:** SetupPage completion logic
+```typescript
+const { create } = useCategories()
+
+// Usage: create.mutateAsync({ name, type })
+const cat = await create.mutateAsync({ name: t(`categories.types.${type}`), type })
+// Returns Category with .id for linking template items
+```
+
+### React Query Invalidation After Completion
+**Source:** `src/hooks/useCategories.ts` line 37
+**Apply to:** SetupPage (prevent redirect loop after completion)
+```typescript
+import { useQueryClient } from "@tanstack/react-query"
+
+const queryClient = useQueryClient()
+// After creating categories + template items:
+await queryClient.invalidateQueries({ queryKey: ["categories"] })
+await queryClient.invalidateQueries({ queryKey: ["template-items"] })
+```
+
+### i18n Translation Pattern
+**Source:** `src/pages/LoginPage.tsx` lines 3, 12, 39
+**Apply to:** All setup components
+```typescript
+import { useTranslation } from "react-i18next"
+const { t } = useTranslation()
+// Usage: t("setup.stepTitle") or t(`presets.${type}.${slug}`) for preset names
+```
+
+### PRESETS Data Access
+**Source:** `src/data/presets.ts`
+**Apply to:** RecurringItemsStep, ReviewStep, SetupPage state initialization
+```typescript
+import { PRESETS } from "@/data/presets"
+// Group by type:
+const grouped = Object.groupBy(PRESETS, (p) => p.type)
+// Or filter: PRESETS.filter(p => p.type === "bill")
+```
+
+## No Analog Found
+
+| File | Role | Data Flow | Reason |
+|------|------|-----------|--------|
+| `src/components/setup/PresetItemRow.tsx` | component | request-response | No existing checkbox-list-row pattern; closest is CategorySection but quite different. Use shadcn Checkbox + Input + Badge inline. |
+| `src/components/setup/CategoryGroupHeader.tsx` | component | transform | Simple heading; trivial enough to not need an analog. |
+
+## Metadata
+
+**Analog search scope:** `src/pages/`, `src/components/`, `src/hooks/`, `src/data/`
+**Files scanned:** 40+
+**Pattern extraction date:** 2026-04-20