diff --git a/.planning/phases/07-setup-wizard/07-RESEARCH.md b/.planning/phases/07-setup-wizard/07-RESEARCH.md
new file mode 100644
index 0000000..ac2e003
--- /dev/null
+++ b/.planning/phases/07-setup-wizard/07-RESEARCH.md
@@ -0,0 +1,433 @@
+# Phase 7: Setup Wizard - Research
+
+**Researched:** 2026-04-20
+**Domain:** React multi-step wizard UI with localStorage persistence, Supabase writes
+**Confidence:** HIGH
+
+## Summary
+
+Phase 7 builds a 3-step setup wizard that guides first-run users through budget template creation. The phase is primarily a frontend UI task: no new DB schema changes are needed (Phase 6 delivered `setup_completed`, presets, and `useFirstRunState`). The wizard must render a new `/setup` route outside the AppLayout (standalone page like login/register), persist state to localStorage, and on completion create categories + template items via existing hooks.
+
+The codebase already provides every data layer primitive needed: `useFirstRunState` for redirect gating, `useCategories().create` for category creation, `useTemplate().createItem` for template item creation, `PRESETS` array with 19 items, and full i18n keys for preset names. The work is purely component authoring, routing, and orchestration.
+
+**Primary recommendation:** Build the wizard as a standalone page component with internal step state management using React useState + localStorage sync. No additional libraries needed -- the existing stack (React 19, React Router 7, shadcn/ui, Tailwind, React Query, Supabase) handles everything.
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+- Numbered horizontal stepper (1-2-3 bar at top) -- minimal, clear progress indicator
+- Centered card layout (max-w-2xl) on clean background -- consistent with auth pages
+- Next/Back buttons at bottom + step indicator is clickable for going back
+- Single "monthly net income" number input pre-filled with 3000, required > 0
+- Full checklist: all 19 PRESETS shown, grouped by category type, with editable amounts
+- Bills (4) + variable_expense (5) pre-checked by default
+- Sticky bar: "Remaining to allocate: Income - sum(checked) = X" -- turns red when negative
+- Read-only review step with grouped summary
+- "Complete" creates categories + template items. Does NOT create first month's budget.
+- After completion: redirect to `/` with success toast
+- "Skip" on each step + global "Skip setup" to exit entirely
+- localStorage keyed by user_id for persistence across refresh
+- On complete or skip: clear localStorage, mark profiles.setup_completed = true
+- No animated transitions between steps (instant swap)
+
+### Claude's Discretion
+- Exact component decomposition (how many sub-components for the wizard)
+- Animation/transition between steps (decided: none)
+- Exact styling of category badges and grouping headers
+- Toast message wording variations
+- Error handling UX for failed API calls during completion
+
+### Deferred Ideas (OUT OF SCOPE)
+None -- discussion stayed within phase scope.
+
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|------------------|
+| SETUP-01 | New user is guided through a 3-step wizard: income, recurring items, review | useFirstRunState hook for redirect gating, /setup route outside AppLayout |
+| SETUP-02 | User sees pre-filled common budget items with sensible default amounts (~15-20 items) | PRESETS array (19 items) with defaultAmount + i18n keys already exist |
+| SETUP-03 | User can skip any wizard step or the entire wizard | Skip Step per step + Skip setup global link, marks setup_completed=true |
+| SETUP-04 | User sees a live "remaining to allocate" balance updating as items are selected | Computed from useState: income - sum(checked items amounts), re-renders on change |
+| SETUP-05 | User's template is created from wizard selections on completion | useCategories().create + useTemplate().createItem mutations in sequence |
+
+
+## Architectural Responsibility Map
+
+| Capability | Primary Tier | Secondary Tier | Rationale |
+|------------|-------------|----------------|-----------|
+| Wizard UI & navigation | Browser / Client | -- | Pure client-side multi-step form, no SSR |
+| State persistence | Browser / Client | -- | localStorage for mid-session persistence |
+| First-run redirect | Browser / Client | -- | useFirstRunState reads cached React Query data |
+| Category + template creation | API / Backend (Supabase) | Browser / Client | Supabase handles insert; client calls via existing hooks |
+| setup_completed flag update | API / Backend (Supabase) | Browser / Client | Direct Supabase update from client |
+
+## Standard Stack
+
+### Core (already installed)
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| React | ^19.2.4 | Component rendering | Project framework [VERIFIED: package.json] |
+| React Router DOM | ^7.13.1 | /setup route, Navigate redirect | Project router [VERIFIED: package.json] |
+| @tanstack/react-query | ^5.90.21 | Data fetching/mutations via hooks | Project data layer [VERIFIED: package.json] |
+| @supabase/supabase-js | ^2.99.1 | DB writes (categories, template_items, profiles) | Project backend [VERIFIED: package.json] |
+| react-i18next | ^16.5.8 | Bilingual copy (EN/DE) | Project i18n [VERIFIED: package.json] |
+| sonner | ^2.0.7 | Toast notifications on completion/error | Project toast library [VERIFIED: package.json] |
+| lucide-react | ^0.577.0 | Check icon for stepper | Project icon library [VERIFIED: package.json] |
+
+### Supporting (already installed)
+| Library | Version | Purpose | When to Use |
+|---------|---------|---------|-------------|
+| radix-ui | ^1.4.3 | Checkbox primitive (via shadcn) | Step 2 item selection |
+| tailwind-merge + clsx + cva | various | Conditional styling | All components |
+
+### New shadcn Component to Install
+| Component | Purpose |
+|-----------|---------|
+| checkbox | Item selection in step 2 (not yet in /src/components/ui/) |
+
+**Installation:**
+```bash
+npx shadcn@latest add checkbox
+```
+
+### Alternatives Considered
+| Instead of | Could Use | Tradeoff |
+|------------|-----------|----------|
+| useState + localStorage | react-hook-form + persist | Overkill for 2 fields (income + selections); useState simpler |
+| Custom stepper | @shadcn/stepper or third-party | Not available in shadcn registry; custom is trivial for 3 steps |
+| Zustand for wizard state | useState + localStorage | No global state needed; wizard is single-page isolated |
+
+## Architecture Patterns
+
+### System Architecture Diagram
+
+```
+[First Login] --> [AppLayout / DashboardPage]
+ |
+ useFirstRunState() --> isFirstRun?
+ | |
+ NO YES
+ | |
+ Dashboard
+ |
+ [SetupWizard Page]
+ |
+ localStorage read --> restore step & data
+ |
+ +----------+----------+----------+
+ | | | |
+ Step 1 Step 2 Step 3 Skip Setup
+ (Income) (Items) (Review) |
+ | | | |
+ +----------+----------+ |
+ | |
+ [Complete] [Skip]
+ | |
+ create categories clear localStorage
+ create template items set setup_completed=true
+ clear localStorage redirect to /
+ set setup_completed=true
+ redirect to / with toast
+```
+
+### Recommended Project Structure
+```
+src/
+├── pages/
+│ └── SetupPage.tsx # Page component, orchestrates wizard
+├── components/
+│ └── setup/
+│ ├── WizardStepper.tsx # Horizontal 1-2-3 stepper bar
+│ ├── IncomeStep.tsx # Step 1: income input
+│ ├── RecurringItemsStep.tsx # Step 2: checklist with amounts
+│ ├── ReviewStep.tsx # Step 3: read-only summary
+│ ├── AllocationBar.tsx # Sticky remaining balance
+│ ├── PresetItemRow.tsx # Single checkbox row
+│ └── CategoryGroupHeader.tsx # Section divider with colored dot
+```
+
+### Pattern 1: Wizard State in localStorage
+**What:** Single useState object synced to localStorage on every change
+**When to use:** Multi-step forms that must survive page refresh
+**Example:**
+```typescript
+// Source: project convention + React docs
+interface WizardState {
+ currentStep: 1 | 2 | 3
+ income: number
+ selectedItems: Record
+}
+
+const STORAGE_KEY = (userId: string) => `setup-wizard-${userId}`
+
+function useWizardState(userId: string) {
+ const [state, setState] = useState(() => {
+ const saved = localStorage.getItem(STORAGE_KEY(userId))
+ if (saved) return JSON.parse(saved)
+ return getDefaultState() // builds from PRESETS defaults
+ })
+
+ useEffect(() => {
+ localStorage.setItem(STORAGE_KEY(userId), JSON.stringify(state))
+ }, [state, userId])
+
+ return [state, setState] as const
+}
+```
+
+### Pattern 2: Completion Sequence (Category + Template Item Creation)
+**What:** Sequential creation of categories then template items, handling duplicates
+**When to use:** Wizard completion step
+**Example:**
+```typescript
+// Source: existing useCategories + useTemplate hooks
+async function completeWizard(
+ selectedItems: SelectedItem[],
+ income: number,
+ createCategory: MutateAsync,
+ createItem: MutateAsync,
+) {
+ // 1. Determine unique category types needed
+ const typesNeeded = [...new Set(selectedItems.map(i => i.type))]
+
+ // 2. Create categories (skip if already exist -- DB has unique constraint)
+ const categoryMap: Record = {}
+ for (const type of typesNeeded) {
+ try {
+ const cat = await createCategory({ name: categoryLabel(type), type })
+ categoryMap[type] = cat.id
+ } catch (e) {
+ // Unique constraint violation = category exists, fetch it
+ // Handle gracefully
+ }
+ }
+
+ // 3. Create template items for each selected preset
+ for (const item of selectedItems) {
+ await createItem({
+ category_id: categoryMap[item.type],
+ item_tier: item.item_tier,
+ budgeted_amount: item.amount,
+ })
+ }
+}
+```
+
+### Pattern 3: Route Structure (Standalone Page Outside AppLayout)
+**What:** /setup route as a sibling to login/register, not nested in AppLayout
+**When to use:** Wizard must be full-screen without sidebar nav
+**Example:**
+```typescript
+// Source: existing App.tsx routing pattern
+
+ {/* Public routes */}
+ } />
+ } />
+
+ {/* Setup wizard -- protected but outside AppLayout */}
+ } />
+
+ {/* Main app layout */}
+ }>
+ } />
+ ...
+
+
+```
+
+### Anti-Patterns to Avoid
+- **Nested wizard inside AppLayout:** Wizard must be standalone (no sidebar). Keep it as a separate protected route.
+- **Creating template without categories first:** Template items require `category_id` -- categories must be created first in completion flow.
+- **Using mutateAsync without error handling:** Each Supabase insert can fail. Wrap in try/catch, show partial-error toast if some items fail.
+- **Reading profile.setup_completed for redirect:** Use `useFirstRunState` (checks categories/template data), not setup_completed flag. The flag is set on completion, not read for gating.
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| Checkbox UI | Custom div with click handlers | shadcn Checkbox (Radix) | Keyboard accessibility, aria states, focus ring |
+| Toast notifications | Custom alert/banner | sonner (already integrated) | Consistent with app, auto-dismiss, queue |
+| Number formatting | Manual toFixed/locale | Intl.NumberFormat with profile currency | Handles EUR/USD/CHF, locale-aware separators |
+| i18n key resolution | String concatenation | `t(\`presets.${type}.${slug}\`)` | Already set up in en.json/de.json |
+
+**Key insight:** Every UI primitive and data operation is already available in the project. This phase is purely about composition and orchestration.
+
+## Common Pitfalls
+
+### Pitfall 1: Redirect Loop on Dashboard
+**What goes wrong:** User completes wizard, lands on dashboard, `useFirstRunState` still returns `isFirstRun=true` because React Query cache is stale.
+**Why it happens:** Categories/template items just created but query cache not invalidated yet.
+**How to avoid:** After completion, invalidate both `["categories"]` and `["template-items"]` query keys before redirecting. Or use `setup_completed` flag for the redirect condition (check profiles table).
+**Warning signs:** User bounces between dashboard and /setup infinitely.
+
+### Pitfall 2: Category Name Collision on Completion
+**What goes wrong:** User already has a "Bills" category (edge case: skipped wizard, created manually, re-enters). DB unique constraint rejects the insert.
+**Why it happens:** Phase 6 added `UNIQUE(user_id, name)` constraint on categories.
+**How to avoid:** Use upsert or catch constraint violation errors and fetch existing category ID instead. The completion logic must handle "category already exists" gracefully.
+**Warning signs:** Wizard completion fails silently or shows generic error.
+
+### Pitfall 3: localStorage Orphan After Logout
+**What goes wrong:** User starts wizard, logs out, different user logs in -- sees previous user's wizard state.
+**Why it happens:** localStorage key must include user_id.
+**How to avoid:** Key is `setup-wizard-${userId}` (already specified in CONTEXT). Always validate userId matches on load.
+**Warning signs:** Wrong income/selections appearing for a different user.
+
+### Pitfall 4: Race Condition in Completion
+**What goes wrong:** User double-clicks "Complete Setup" and creates duplicate template items.
+**Why it happens:** Button not disabled during async operation.
+**How to avoid:** Disable Complete button immediately on click, show spinner. Use a `completing` state flag.
+**Warning signs:** Duplicate rows in template_items table.
+
+### Pitfall 5: useTemplate() Needs Template to Exist Before createItem
+**What goes wrong:** `createItem` throws "Template not loaded" because `templateId` is undefined.
+**Why it happens:** `useTemplate()` auto-creates a template row on first query, but the query must complete before `createItem` works.
+**How to avoid:** Ensure `useTemplate()` is called early in the wizard (mount time) so the template row exists by completion time. Or call `getOrCreateTemplate()` directly in the completion flow.
+**Warning signs:** "Template not loaded" error on wizard completion.
+
+## Code Examples
+
+### Redirect Logic in Dashboard (or AppLayout)
+```typescript
+// Source: existing useFirstRunState pattern + CONTEXT decisions
+import { useFirstRunState } from "@/hooks/useFirstRunState"
+import { Navigate } from "react-router-dom"
+
+// Inside DashboardPage or a redirect wrapper:
+const { isFirstRun, loading } = useFirstRunState()
+if (loading) return // or null
+if (isFirstRun) return
+```
+
+### Updating setup_completed on Skip/Complete
+```typescript
+// Source: existing SettingsPage pattern for profile updates
+import { supabase } from "@/lib/supabase"
+
+async function markSetupComplete(userId: string) {
+ await supabase
+ .from("profiles")
+ .update({ setup_completed: true })
+ .eq("id", userId)
+}
+```
+
+### Live Allocation Calculation
+```typescript
+// Source: derived from CONTEXT decisions
+function computeRemaining(income: number, items: Record) {
+ const totalExpenses = Object.values(items)
+ .filter(i => i.checked)
+ .reduce((sum, i) => sum + i.amount, 0)
+ return income - totalExpenses
+}
+```
+
+### Default Selected Items State
+```typescript
+// Source: CONTEXT + PRESETS data structure
+import { PRESETS } from "@/data/presets"
+
+function getDefaultSelectedItems(): Record {
+ const result: Record = {}
+ for (const preset of PRESETS) {
+ result[preset.slug] = {
+ checked: preset.type === "bill" || preset.type === "variable_expense",
+ amount: preset.defaultAmount,
+ }
+ }
+ return result
+}
+```
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| Multi-page wizard with URL-per-step | Single-page with internal state | Standard for SPA wizards | Simpler routing, better state control |
+| Form library (react-hook-form) for wizards | useState for simple forms | When forms have < 5 fields | Less boilerplate for simple cases |
+| Redux/Zustand for wizard state | localStorage + useState | For isolated flows | No global store pollution |
+
+## Assumptions Log
+
+| # | Claim | Section | Risk if Wrong |
+|---|-------|---------|---------------|
+| A1 | shadcn checkbox installs via `npx shadcn@latest add checkbox` | Standard Stack | Low -- standard shadcn CLI pattern, easily correctable |
+| A2 | Category names for auto-creation should use i18n labels (e.g., t("categories.types.bill")) | Patterns | Medium -- if hardcoded English names used, DE users get English category names |
+
+## Open Questions
+
+1. **Category naming on creation**
+ - What we know: Categories need a `name` field. Presets are grouped by `type`.
+ - What's unclear: Should each preset create its own category (19 categories) or one category per type (6 categories)?
+ - Recommendation: One category per type (6 max). The CONTEXT says "Create categories (from checked item types that don't already exist)" -- this confirms one-per-type. Multiple template items share the same category.
+
+2. **Where to place the first-run redirect**
+ - What we know: `useFirstRunState` returns `isFirstRun` based on categories/template data.
+ - What's unclear: Should redirect live in DashboardPage, AppLayout, or a wrapper component?
+ - Recommendation: Place in DashboardPage (the index route) since that's where first-run users land. Avoids affecting other routes.
+
+## Validation Architecture
+
+### Test Framework
+| Property | Value |
+|----------|-------|
+| Framework | None detected (no test config or test files found) |
+| Config file | none -- Wave 0 must create |
+| Quick run command | N/A |
+| Full suite command | N/A |
+
+### Phase Requirements to Test Map
+| Req ID | Behavior | Test Type | Automated Command | File Exists? |
+|--------|----------|-----------|-------------------|-------------|
+| SETUP-01 | First-run user sees wizard, 3 steps | manual | Manual browser test | N/A |
+| SETUP-02 | 19 pre-filled items with amounts | manual | Visual verification | N/A |
+| SETUP-03 | Skip step/wizard works | manual | Click through wizard | N/A |
+| SETUP-04 | Remaining balance updates live | manual | Check/uncheck items | N/A |
+| SETUP-05 | Template created from selections | manual | Complete wizard, check /template | N/A |
+
+### Wave 0 Gaps
+No test framework exists in this project. All verification is manual/visual. This is acceptable per the project's established pattern -- no test files exist anywhere in `src/`.
+
+## Security Domain
+
+### Applicable ASVS Categories
+
+| ASVS Category | Applies | Standard Control |
+|---------------|---------|-----------------|
+| V2 Authentication | no | Already handled by ProtectedRoute |
+| V3 Session Management | no | Supabase session management |
+| V4 Access Control | yes | RLS policies on categories/template_items (already in place) |
+| V5 Input Validation | yes | Income > 0 validation, amounts must be positive numbers |
+| V6 Cryptography | no | No crypto needed |
+
+### Known Threat Patterns for This Phase
+
+| Pattern | STRIDE | Standard Mitigation |
+|---------|--------|---------------------|
+| localStorage tampering | Tampering | Validate localStorage data shape on load; sanitize numbers |
+| Mass insert abuse (spam completion) | Denial of Service | setup_completed flag prevents re-entry; RLS limits to own user |
+| XSS via preset slug injection | Spoofing | Presets are hardcoded in source, not user-input; i18n keys are safe |
+
+## Sources
+
+### Primary (HIGH confidence)
+- Project source code: `src/hooks/useFirstRunState.ts`, `src/hooks/useCategories.ts`, `src/hooks/useTemplate.ts`, `src/data/presets.ts`, `src/App.tsx`, `src/lib/types.ts` -- verified via direct file read
+- `package.json` -- verified all dependency versions
+- Phase 6 outputs: `supabase/migrations/007_setup_completed.sql` -- confirms DB schema ready
+
+### Secondary (MEDIUM confidence)
+- UI-SPEC.md (07-UI-SPEC.md) -- authored design contract for this phase
+- CONTEXT.md (07-CONTEXT.md) -- user decisions from discuss phase
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack: HIGH - all dependencies verified in package.json, no new installs except shadcn checkbox
+- Architecture: HIGH - follows exact patterns established in existing pages (Login, Register, Settings)
+- Pitfalls: HIGH - derived from reading actual hook implementations and understanding race conditions
+
+**Research date:** 2026-04-20
+**Valid until:** 2026-05-20 (stable -- no moving targets, all deps are pinned)