---
phase: 01-level-up-pf2e-regelkonform
plan: 05
type: execute
wave: 5
depends_on: ["01-01", "01-02", "01-03", "01-03b", "01-04"]
files_modified:
  - client/src/features/characters/components/level-up/wizard-state-reducer.ts
  - client/src/features/characters/components/level-up/use-level-up-session.ts
  - client/src/features/characters/components/level-up/level-up-choice-card.tsx
  - client/src/features/characters/components/level-up/level-up-prereq-confirm-dialog.tsx
  - client/src/features/characters/components/level-up/level-up-resume-banner.tsx
  - client/src/features/characters/components/level-up/level-up-violations-banner.tsx
  - client/src/features/characters/components/level-up/level-up-step-class-features.tsx
  - client/src/features/characters/components/level-up/level-up-step-class-feature-choice.tsx
  - client/src/features/characters/components/level-up/level-up-step-boost.tsx
  - client/src/features/characters/components/level-up/level-up-step-skill-increase.tsx
  - client/src/features/characters/components/level-up/level-up-step-feat-class.tsx
  - client/src/features/characters/components/level-up/level-up-step-feat-skill.tsx
  - client/src/features/characters/components/level-up/level-up-step-feat-general.tsx
  - client/src/features/characters/components/level-up/level-up-step-feat-ancestry.tsx
  - client/src/features/characters/components/level-up/level-up-step-feat-archetype.tsx
  - client/src/features/characters/components/level-up/level-up-step-spellcaster.tsx
  - client/src/features/characters/components/level-up/level-up-step-review.tsx
  - client/src/features/characters/components/level-up/level-up-wizard.tsx
  - client/src/features/characters/components/character-sheet-page.tsx
  - client/src/shared/lib/api.ts
  - client/src/shared/types/index.ts
  - client/src/shared/hooks/use-character-socket.ts
autonomous: false
requirements: [LVL-01, LVL-02, LVL-03, LVL-04, LVL-05, LVL-06, LVL-07, LVL-08, LVL-09, LVL-10, LVL-11, LVL-12, LVL-13, LVL-14, LVL-15]
tags: [react, ui, wizard, level-up, character-sheet, mobile-first, websocket-client]
must_haves:
  truths:
    - "Player on a character sheet can click 'Stufe steigen' (Sparkles icon) and a modal wizard opens, ranging from 4 to 11 steps depending on level/class/FA/caster, ending in a Review step."
    - "Wizard chrome (header, stepper with progress label, body, footer) matches `01-UI-SPEC.md` exactly — no redesign."
    - "Choice-cards show source-color badges (Klassen/Abstammung/etc. — reuse existing featSourceColors) and a yellow AlertTriangle for non-evaluable prereqs (D-03)."
    - "Boost step uses +/- counters at h-11 w-11 (44×44 touch targets), shows 'wird {newScore}' live with cap-bei-18 chip when applicable."
    - "Review step shows Vorher/Nachher cards (HP-Max, RK, Klassen-DC, Wahrnehmung, Saves) using server-computed preview, with Ändern links to revise upstream choices."
    - "Bestätigen runs the commit; wizard closes; toast shows; WebSocket level_up_committed event arrives at all other open clients of the character within ~1s."
    - "DRAFT-Resume banner appears at the top of the character-sheet when an open DRAFT exists, with Fortsetzen + Verwerfen actions."
    - "Pathbuilder-import-violations banner appears below the avatar header when Character.prereqViolations is non-null."
    - "All UI text is in German; no emojis; only Lucide icons; mobile-first (44px touch targets)."
    - "Human verification checkpoint passes — user walks through the wizard for a real character on a mobile viewport (375×667) and confirms each step matches UI-SPEC."
  artifacts:
    - path: "client/src/features/characters/components/level-up/level-up-wizard.tsx"
      provides: "Outer modal container — header, stepper, motion orchestration, footer wiring"
      exports: ["LevelUpWizard"]
    - path: "client/src/features/characters/components/level-up/wizard-state-reducer.ts"
      provides: "useReducer + WizardState/WizardEvent discriminated unions"
      exports: ["wizardReducer", "WizardState", "WizardEvent"]
    - path: "client/src/features/characters/components/level-up/use-level-up-session.ts"
      provides: "react-query hooks: start, patch, preview, commit, discard"
      exports: ["useStartLevelUpMutation", "usePatchLevelUpMutation", "useCommitLevelUpMutation", "useDiscardLevelUpMutation", "useLevelUpPreviewQuery"]
    - path: "client/src/features/characters/components/level-up/level-up-step-*.tsx (12 files)"
      provides: "Per-step components — class-features, class-feature-choice, boost, skill-increase, feat-class, feat-skill, feat-general, feat-ancestry, feat-archetype, spellcaster, review (and choice-card primitive)"
    - path: "client/src/features/characters/components/level-up/level-up-resume-banner.tsx"
      provides: "DRAFT-Resume banner — Fortsetzen / Verwerfen"
      exports: ["LevelUpResumeBanner"]
    - path: "client/src/features/characters/components/level-up/level-up-violations-banner.tsx"
      provides: "Pathbuilder-import-violations banner with collapsible list"
      exports: ["LevelUpViolationsBanner"]
    - path: "client/src/features/characters/components/character-sheet-page.tsx (extended)"
      provides: "Header button + 2 banner mounts + wizard mount"
    - path: "client/src/shared/lib/api.ts (extended)"
      provides: "8 new methods: startLevelUp, patchLevelUp, getLevelUpPreview, commitLevelUp, discardLevelUp, getOpenLevelUpDraft, getLevelUpFeats, getLevelUpClassFeatureOptions"
    - path: "client/src/shared/types/index.ts (extended)"
      provides: "LevelUpSession, LevelUpPreview, WizardChoices types + extended Character with freeArchetype, prereqViolations"
    - path: "client/src/shared/hooks/use-character-socket.ts (extended)"
      provides: "Adds 'level_up_committed' to CharacterUpdateType union + onLevelUpCommitted callback"
  gotchas:
    - "Ändern (revision) does NOT auto-clear downstream choices that depended on the revised upstream choice. Per D-12 (review-only recompute, no live per-step recompute) the wizard intentionally keeps stale downstream picks; commit-time validation in Plan 04 (LevelingService.commit + isValidBoostSet/prereq guards) is the source of truth and rejects invalid combinations with a German BadRequestException that the wizard surfaces inline. Per-dependency clearing is a v2 enhancement."
  key_links:
    - from: "level-up-wizard.tsx"
      to: "LevelingService REST API"
      via: "use-level-up-session hooks → api methods → axios"
      pattern: "/characters/.*level-up"
    - from: "character-sheet-page.tsx"
      to: "LevelUpWizard mount"
      via: "showLevelUpWizard state + conditional render"
      pattern: "<LevelUpWizard"
    - from: "use-character-socket.ts"
      to: "LevelUpWizard / character-sheet"
      via: "onLevelUpCommitted callback → react-query invalidate"
      pattern: "level_up_committed"
    - from: "All step components"
      to: "level-up-choice-card.tsx"
      via: "import and reuse"
      pattern: "from.*level-up-choice-card"
---

<objective>
Build the React wizard UI exactly as specified in `01-UI-SPEC.md` — no redesign. The wizard is the user-facing surface that drives the LevelingService REST endpoints (Plan 04). It is mobile-first (Bottom-Sheet on small screens, centered modal on `sm:` and up), German throughout, and reuses existing project primitives (`Button`, `Card`, `Spinner`, `ActionIcon`, `featSourceColors`). The plan also extends the character-sheet page with the "Stufe steigen" button, the DRAFT-Resume banner, and the Pathbuilder-Import-Violations banner.

Purpose: Without this plan, the entire backend (Plans 01-04) has no consumer. The wizard is the product surface — it converts the regelkonform engine into a usable feature for the player and GM at the table.

Output: 18 new client files (1 reducer + 1 hooks file + 1 choice-card + 1 prereq dialog + 2 banners + 12 step components) + 1 modal container + 4 extended files (character-sheet-page, api, shared types, use-character-socket). One human-verification checkpoint at the end.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/REQUIREMENTS.md
@.planning/phases/01-level-up-pf2e-regelkonform/01-CONTEXT.md
@.planning/phases/01-level-up-pf2e-regelkonform/01-RESEARCH.md
@.planning/phases/01-level-up-pf2e-regelkonform/01-PATTERNS.md
@.planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md
@.planning/phases/01-level-up-pf2e-regelkonform/01-01-SUMMARY.md
@.planning/phases/01-level-up-pf2e-regelkonform/01-04-SUMMARY.md
@client/src/features/characters/components/character-sheet-page.tsx
@client/src/features/characters/components/add-feat-modal.tsx
@client/src/features/characters/components/add-condition-modal.tsx
@client/src/features/characters/components/rest-modal.tsx
@client/src/features/characters/components/hp-control.tsx
@client/src/features/characters/components/feat-detail-modal.tsx
@client/src/shared/lib/api.ts
@client/src/shared/hooks/use-character-socket.ts
@client/src/shared/components/ui/index.ts
@client/src/index.css

<interfaces>
<!-- Plan 04 REST contract that this plan consumes -->
<!--   POST   /characters/:characterId/level-up                 (StartLevelUpDto) → LevelUpSession -->
<!--   PATCH  /characters/:characterId/level-up/:sessionId      (PatchLevelUpDto) → LevelUpSession -->
<!--   GET    /characters/:characterId/level-up/:sessionId/preview            → LevelUpPreviewDto -->
<!--   POST   /characters/:characterId/level-up/:sessionId/commit             → Character -->
<!--   DELETE /characters/:characterId/level-up/:sessionId                    → 204 -->

<!-- WebSocket payload added by Plan 04 (mirror in client) -->
```typescript
// Server → client (server/src/modules/characters/characters.gateway.ts)
// type='level_up_committed' data: { level: number; derived: { hpMax, ac, classDc, perception, fortitude, reflex, will } }
```

<!-- Existing client API base (client/src/shared/lib/api.ts:381-388) — analog: getRestPreview / performRest -->
```typescript
async getRestPreview(campaignId: string, characterId: string) {
  const response = await this.client.get(`/campaigns/${campaignId}/characters/${characterId}/rest/preview`);
  return response.data;
}
```

<!-- Existing socket hook (client/src/shared/hooks/use-character-socket.ts:15) — extend union -->
```typescript
export type CharacterUpdateType = 'hp' | 'conditions' | ... | 'dying';
// Add: | 'level_up_committed'
```

<!-- Existing source-color map (client/src/features/characters/components/feat-detail-modal.tsx:22-29) — REUSE, don't redefine -->
```typescript
const featSourceColors = {
  Class: 'bg-red-500/20 text-red-400',
  Ancestry: 'bg-blue-500/20 text-blue-400',
  General: 'bg-yellow-500/20 text-yellow-400',
  Skill: 'bg-green-500/20 text-green-400',
  Archetype: 'bg-purple-500/20 text-purple-400',
  Bonus: 'bg-cyan-500/20 text-cyan-400',
};
```

<!-- Existing modal chrome canonical pattern (client/src/features/characters/components/add-feat-modal.tsx:246-260) -->
```tsx
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
  <div className="absolute inset-0 bg-black/60" onClick={onClose} />
  <div className="relative w-full sm:max-w-2xl max-h-[90vh] bg-bg-secondary
                  rounded-t-2xl sm:rounded-2xl flex flex-col overflow-hidden">
    {/* Header / Body / Footer */}
  </div>
</div>
```

<!-- Existing character-sheet header button cluster (character-sheet-page.tsx:1607-1626) — insert "Stufe steigen" first -->
<!-- Existing character-sheet modal mounts (character-sheet-page.tsx:1652-1666) — append <LevelUpWizard> -->

<!-- StepKind type from server lib must be mirrored client-side (or imported via shared/types) -->
```typescript
export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'skill-increase'
  | 'feat-class' | 'feat-skill' | 'feat-general' | 'feat-ancestry' | 'feat-archetype'
  | 'spellcaster' | 'review';
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="false">
  <name>Task 1: Extend shared types + client API client + use-character-socket hook</name>
  <files>
    client/src/shared/types/index.ts,
    client/src/shared/lib/api.ts,
    client/src/shared/hooks/use-character-socket.ts
  </files>
  <read_first>
    - client/src/shared/types/index.ts (entire file — must understand existing Character + Campaign types)
    - client/src/shared/lib/api.ts (lines 381-388 — getRestPreview/performRest analog; entire file for structure)
    - client/src/shared/hooks/use-character-socket.ts (entire file — must understand CharacterUpdateType union, existing onXxxUpdate callbacks)
    - server/src/modules/leveling/dto/level-up-state.dto.ts (Plan 04 — LevelUpSessionDto + LevelUpPreviewDto field shapes)
    - server/src/modules/characters/characters.gateway.ts (Plan 04 — extended payload doc)
    - .planning/phases/01-level-up-pf2e-regelkonform/01-PATTERNS.md (lines 769-808 — api.ts extension pattern)
  </read_first>
  <action>
    **Step 1 — Add types to `client/src/shared/types/index.ts`:**

    Append to the existing types file:

    ```typescript
    // ============ Phase 1 Level-Up types ============

    export type AbilityAbbreviation = 'STR' | 'DEX' | 'CON' | 'INT' | 'WIS' | 'CHA';
    export type Proficiency = 'UNTRAINED' | 'TRAINED' | 'EXPERT' | 'MASTER' | 'LEGENDARY';

    export type StepKind =
      | 'class-features'
      | 'class-feature-choice'
      | 'boost'
      | 'skill-increase'
      | 'feat-class'
      | 'feat-skill'
      | 'feat-general'
      | 'feat-ancestry'
      | 'feat-archetype'
      | 'spellcaster'
      | 'review';

    export interface WizardChoices {
      boostTargets?: AbilityAbbreviation[];
      skillIncrease?: { skillName: string; toRank: Proficiency };
      featClassId?: string;
      featSkillId?: string;
      featGeneralId?: string;
      featAncestryId?: string;
      featArchetypeId?: string;
      classFeatureChoices?: Record<string, string>;     // optionsRef → optionKey
      spellcasterRepertoirePicks?: string[];            // spell IDs
    }

    export interface LevelUpSession {
      id: string;
      characterId: string;
      targetLevel: number;
      state: { choices: WizardChoices; acknowledgedNonEvaluablePrereqs?: string[] };
      committedAt: string | null;
      createdAt: string;
      updatedAt: string;
    }

    export interface DerivedStats {
      level: number;
      hpMax: number;
      ac: number;
      classDc: number;
      perception: number;
      fortitude: number;
      reflex: number;
      will: number;
    }

    export interface LevelUpPreview {
      before: DerivedStats;
      after: DerivedStats;
      spellcaster?: {
        slotIncrements: { tradition: string; spellLevel: number; count: number }[];
        cantripDelta?: number;
        repertoireDelta?: number;
      };
    }

    export interface PrereqViolation {
      featId: string;
      featName: string;
      prereqText: string;
    }
    ```

    **Step 2 — Extend the existing `Character` interface** in the same file. Find the `interface Character {` block and append two fields:

    ```typescript
    export interface Character {
      // ... existing fields ...
      freeArchetype?: boolean;
      prereqViolations?: { violations: PrereqViolation[] } | null;
    }
    ```

    **Step 3 — Add 5 methods to `client/src/shared/lib/api.ts`** alongside the existing methods (analog: `getRestPreview` at lines 381-388):

    ```typescript
    // ============ Phase 1 Level-Up methods ============

    async startLevelUp(characterId: string, targetLevel?: number): Promise<LevelUpSession> {
      const response = await this.client.post(
        `/characters/${characterId}/level-up`,
        { targetLevel },
      );
      return response.data;
    }

    async patchLevelUp(
      characterId: string,
      sessionId: string,
      state: Partial<{ choices: WizardChoices; acknowledgedNonEvaluablePrereqs?: string[] }>,
    ): Promise<LevelUpSession> {
      const response = await this.client.patch(
        `/characters/${characterId}/level-up/${sessionId}`,
        { state },
      );
      return response.data;
    }

    async getLevelUpPreview(characterId: string, sessionId: string): Promise<LevelUpPreview> {
      const response = await this.client.get(
        `/characters/${characterId}/level-up/${sessionId}/preview`,
      );
      return response.data;
    }

    async commitLevelUp(
      characterId: string,
      sessionId: string,
      acknowledgePrereqWarnings?: boolean,
    ): Promise<Character> {
      const response = await this.client.post(
        `/characters/${characterId}/level-up/${sessionId}/commit`,
        { acknowledgePrereqWarnings },
      );
      return response.data;
    }

    async discardLevelUp(characterId: string, sessionId: string): Promise<void> {
      await this.client.delete(
        `/characters/${characterId}/level-up/${sessionId}`,
      );
    }

    /** GET open DRAFT for resume-banner detection. Returns null on 404. */
    async getOpenLevelUpDraft(characterId: string): Promise<LevelUpSession | null> {
      try {
        const response = await this.client.get(`/characters/${characterId}/level-up`);
        return response.data;
      } catch (err) {
        if (axios.isAxiosError(err) && err.response?.status === 404) return null;
        throw err;
      }
    }

    /** GET filtered feat list for a wizard slot. Driven by Plan 04 FeatFilterService. */
    async getLevelUpFeats(
      characterId: string,
      sessionId: string,
      slot: 'class' | 'skill' | 'general' | 'ancestry' | 'archetype',
      includeUnavailable: boolean = false,
    ): Promise<unknown[]> {
      const response = await this.client.get(
        `/characters/${characterId}/level-up/${sessionId}/feats`,
        { params: { slot, includeUnavailable: includeUnavailable ? 'true' : 'false' } },
      );
      return response.data;
    }

    /** GET ClassFeatureOption rows for a class-feature choice step. */
    async getLevelUpClassFeatureOptions(
      characterId: string,
      sessionId: string,
      optionsRef: string,
    ): Promise<unknown[]> {
      const response = await this.client.get(
        `/characters/${characterId}/level-up/${sessionId}/class-feature-options/${encodeURIComponent(optionsRef)}`,
      );
      return response.data;
    }
    ```

    Add the imports at the top of api.ts:
    ```typescript
    import type { Character, LevelUpSession, LevelUpPreview, WizardChoices } from '@/shared/types';
    ```

    **Step 4 — Extend `client/src/shared/hooks/use-character-socket.ts`:**

    Find the `CharacterUpdateType` union (line 15) and append `'level_up_committed'`:

    ```typescript
    export type CharacterUpdateType =
      | 'hp' | 'conditions' | 'item' | 'inventory' | 'money' | 'level' | 'equipment_status'
      | 'rest' | 'alchemy_vials' | 'alchemy_formulas' | 'alchemy_prepared' | 'alchemy_state'
      | 'dying'
      | 'level_up_committed';
    ```

    Find the `UseCharacterSocketOptions` interface and add a new optional callback field (analog: `onRestUpdate` ~line 53):

    ```typescript
    export interface UseCharacterSocketOptions {
      // ... existing fields ...
      onLevelUpCommitted?: (data: {
        level: number;
        derived: import('@/shared/types').DerivedStats;
      }) => void;
    }
    ```

    Find the place in the hook body where `onAlchemyUpdate` and similar callbacks are dispatched (look for the switch on `payload.type` / similar). Add the new branch:

    ```typescript
    if (payload.type === 'level_up_committed') {
      options.onLevelUpCommitted?.(payload.data as { level: number; derived: DerivedStats });
    }
    ```

    Import `DerivedStats` if not already.

    **Constraints (per CLAUDE.md):**
    - TypeScript strict — no `any` outside the existing untyped places.
    - Use `import type` for type-only imports.
    - All file naming kebab-case (these files already exist and are kebab-case).
    - All UI text added later is German.
  </action>
  <verify>
    <automated>cd client &amp;&amp; npx tsc --noEmit -p tsconfig.app.json 2&gt;&amp;1 | grep -E "level-up|LevelUp|level_up_committed" || echo "tsc clean"</automated>
  </verify>
  <acceptance_criteria>
    - `client/src/shared/types/index.ts` exports `LevelUpSession`, `LevelUpPreview`, `DerivedStats`, `WizardChoices`, `StepKind`, `Proficiency`, `AbilityAbbreviation`, `PrereqViolation`
    - `client/src/shared/types/index.ts` Character interface has `freeArchetype?: boolean` and `prereqViolations?` fields
    - `client/src/shared/lib/api.ts` contains 8 new method names: `startLevelUp`, `patchLevelUp`, `getLevelUpPreview`, `commitLevelUp`, `discardLevelUp`, `getOpenLevelUpDraft`, `getLevelUpFeats`, `getLevelUpClassFeatureOptions`
    - `client/src/shared/hooks/use-character-socket.ts` CharacterUpdateType union contains `'level_up_committed'`
    - `client/src/shared/hooks/use-character-socket.ts` UseCharacterSocketOptions interface contains `onLevelUpCommitted?:`
    - `cd client && npx tsc --noEmit -p tsconfig.app.json` exits 0
  </acceptance_criteria>
  <done>
    Shared client surface (types + api + socket) extended to talk to the Plan 04 server endpoints + receive the new WebSocket event.
  </done>
</task>

<task type="auto" tdd="false">
  <name>Task 2: wizard-state-reducer.ts + use-level-up-session.ts (state machine + react-query hooks)</name>
  <files>
    client/src/features/characters/components/level-up/wizard-state-reducer.ts,
    client/src/features/characters/components/level-up/use-level-up-session.ts
  </files>
  <read_first>
    - .planning/phases/01-level-up-pf2e-regelkonform/01-RESEARCH.md (lines 354-401 — exact reducer shape; lines 812-816 — react-query hook recommendation)
    - .planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md (entire — reducer must support every interaction the UI needs, especially the Ändern revision contract)
    - .planning/phases/01-level-up-pf2e-regelkonform/01-PATTERNS.md (lines 819-823 — wizard-state-reducer; no analog — first useReducer in codebase)
    - server/src/modules/leveling/lib/types.ts (Plan 02 — StepKind, WizardChoices types — mirror in client)
    - client/src/shared/types/index.ts (Task 1 added the wire types — reuse)
  </read_first>
  <action>
    **Step 1 — Create `wizard-state-reducer.ts`:**

    ```typescript
    import type { StepKind, WizardChoices } from '@/shared/types';

    /**
     * The full wizard state — JSON-serializable so it survives PATCH to the DRAFT.
     * Mirror of server/src/modules/leveling/lib/types.ts WizardChoices, plus client-only
     * navigation/UI fields.
     */
    export interface WizardState {
      sessionId: string;
      targetLevel: number;
      steps: StepKind[];
      currentIdx: number;
      choices: WizardChoices;
      acknowledgedNonEvaluablePrereqs: string[];
      revisionMode: { fromStep: number } | null;       // tracks "Ändern" → "Zurück zur Übersicht"
    }

    export type WizardEvent =
      | { type: 'GO_NEXT' }
      | { type: 'GO_PREV' }
      | { type: 'GO_TO_STEP'; idx: number }
      | { type: 'GO_TO_STEP_FROM_REVIEW'; idx: number }      // sets revisionMode
      // RETURN_TO_REVIEW: clears revisionMode flag and routes back to review step. NOTE: downstream picks are NOT auto-cleared (v1) — commit-time validation in Plan 04 surfaces invalid combos. See must_haves.gotchas.
      | { type: 'RETURN_TO_REVIEW' }
      | { type: 'SET_BOOST_TARGETS'; targets: WizardChoices['boostTargets'] }
      | { type: 'SET_SKILL_INCREASE'; pick: WizardChoices['skillIncrease'] }
      | { type: 'SET_FEAT'; slot: 'class' | 'skill' | 'general' | 'ancestry' | 'archetype'; featId: string }
      | { type: 'SET_CLASS_FEATURE_CHOICE'; key: string; optionId: string }
      | { type: 'SET_REPERTOIRE_PICKS'; picks: string[] }
      | { type: 'ACKNOWLEDGE_PREREQ_WARNING'; featId: string };

    export function wizardReducer(state: WizardState, ev: WizardEvent): WizardState {
      switch (ev.type) {
        case 'GO_NEXT':
          return state.currentIdx < state.steps.length - 1
            ? { ...state, currentIdx: state.currentIdx + 1 }
            : state;
        case 'GO_PREV':
          return state.currentIdx > 0
            ? { ...state, currentIdx: state.currentIdx - 1 }
            : state;
        case 'GO_TO_STEP':
          if (ev.idx < 0 || ev.idx > state.currentIdx) return state;  // can't jump forward to incomplete
          return { ...state, currentIdx: ev.idx };
        case 'GO_TO_STEP_FROM_REVIEW': {
          const reviewIdx = state.steps.findIndex(s => s === 'review');
          return { ...state, currentIdx: ev.idx, revisionMode: { fromStep: reviewIdx } };
        }
        case 'RETURN_TO_REVIEW': {
          const reviewIdx = state.steps.findIndex(s => s === 'review');
          // GOTCHA (intentional v1 behaviour): RETURN_TO_REVIEW does NOT auto-clear downstream
          // choices that depended on an upstream revision. Per Plan-05 must_haves.gotchas and
          // D-12 (no live recompute per step — review-only), commit-time validation in Plan 04
          // is the source of truth: invalid combinations surface as a German BadRequestException
          // when the user clicks Bestätigen, and the wizard surfaces the message inline.
          // Refinement to per-dependency clearing is a v2 enhancement.
          return { ...state, currentIdx: reviewIdx, revisionMode: null };
        }
        case 'SET_BOOST_TARGETS':
          return { ...state, choices: { ...state.choices, boostTargets: ev.targets } };
        case 'SET_SKILL_INCREASE':
          return { ...state, choices: { ...state.choices, skillIncrease: ev.pick } };
        case 'SET_FEAT': {
          const fieldName = `feat${ev.slot.charAt(0).toUpperCase() + ev.slot.slice(1)}Id` as keyof WizardChoices;
          return { ...state, choices: { ...state.choices, [fieldName]: ev.featId } };
        }
        case 'SET_CLASS_FEATURE_CHOICE':
          return {
            ...state,
            choices: {
              ...state.choices,
              classFeatureChoices: {
                ...(state.choices.classFeatureChoices ?? {}),
                [ev.key]: ev.optionId,
              },
            },
          };
        case 'SET_REPERTOIRE_PICKS':
          return { ...state, choices: { ...state.choices, spellcasterRepertoirePicks: ev.picks } };
        case 'ACKNOWLEDGE_PREREQ_WARNING':
          return state.acknowledgedNonEvaluablePrereqs.includes(ev.featId)
            ? state
            : { ...state, acknowledgedNonEvaluablePrereqs: [...state.acknowledgedNonEvaluablePrereqs, ev.featId] };
      }
    }

    /** Initialize the wizard state from a freshly-fetched LevelUpSession. */
    export function initWizardState(session: {
      id: string;
      targetLevel: number;
      state: { choices: WizardChoices; acknowledgedNonEvaluablePrereqs?: string[] };
    }, steps: StepKind[]): WizardState {
      return {
        sessionId: session.id,
        targetLevel: session.targetLevel,
        steps,
        currentIdx: 0,
        choices: session.state.choices ?? {},
        acknowledgedNonEvaluablePrereqs: session.state.acknowledgedNonEvaluablePrereqs ?? [],
        revisionMode: null,
      };
    }
    ```

    **Step 2 — Create `use-level-up-session.ts`** (react-query hooks):

    ```typescript
    import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
    import { api } from '@/shared/lib/api';
    import type { LevelUpSession, LevelUpPreview, WizardChoices } from '@/shared/types';

    /**
     * Start or resume a LevelUpSession for the given character.
     */
    export function useStartLevelUpMutation(characterId: string) {
      const qc = useQueryClient();
      return useMutation({
        mutationFn: (targetLevel?: number) => api.startLevelUp(characterId, targetLevel),
        onSuccess: (session) => {
          qc.setQueryData(['levelUpSession', characterId], session);
        },
      });
    }

    export function usePatchLevelUpMutation(characterId: string, sessionId: string) {
      const qc = useQueryClient();
      return useMutation({
        mutationFn: (state: Partial<{ choices: WizardChoices; acknowledgedNonEvaluablePrereqs?: string[] }>) =>
          api.patchLevelUp(characterId, sessionId, state),
        onSuccess: (session) => {
          qc.setQueryData(['levelUpSession', characterId], session);
        },
      });
    }

    export function useLevelUpPreviewQuery(characterId: string, sessionId: string, enabled: boolean) {
      return useQuery<LevelUpPreview>({
        queryKey: ['levelUpPreview', characterId, sessionId],
        queryFn: () => api.getLevelUpPreview(characterId, sessionId),
        enabled,
        staleTime: 0,    // always re-fetch when the user reaches Review
      });
    }

    export function useCommitLevelUpMutation(characterId: string, sessionId: string) {
      const qc = useQueryClient();
      return useMutation({
        mutationFn: (acknowledgePrereqWarnings?: boolean) =>
          api.commitLevelUp(characterId, sessionId, acknowledgePrereqWarnings),
        onSuccess: () => {
          qc.invalidateQueries({ queryKey: ['character', characterId] });
          qc.removeQueries({ queryKey: ['levelUpSession', characterId] });
        },
      });
    }

    export function useDiscardLevelUpMutation(characterId: string) {
      const qc = useQueryClient();
      return useMutation({
        mutationFn: (sessionId: string) => api.discardLevelUp(characterId, sessionId),
        onSuccess: () => {
          qc.removeQueries({ queryKey: ['levelUpSession', characterId] });
        },
      });
    }
    ```

    **Constraint:** No `any` types. The mutation payload types must match the Plan 04 DTOs exactly.
  </action>
  <verify>
    <automated>cd client &amp;&amp; npx tsc --noEmit -p tsconfig.app.json 2&gt;&amp;1 | grep -E "wizard-state-reducer|use-level-up-session" || echo "tsc clean"</automated>
  </verify>
  <acceptance_criteria>
    - File `client/src/features/characters/components/level-up/wizard-state-reducer.ts` exists
    - File exports `wizardReducer`, `WizardState`, `WizardEvent`, `initWizardState`
    - File contains a `case 'RETURN_TO_REVIEW':` (proves revision-mode contract)
    - File contains NO `: any` outside comments
    - File `client/src/features/characters/components/level-up/use-level-up-session.ts` exists
    - File exports `useStartLevelUpMutation`, `usePatchLevelUpMutation`, `useLevelUpPreviewQuery`, `useCommitLevelUpMutation`, `useDiscardLevelUpMutation`
    - File imports `useMutation`, `useQuery`, `useQueryClient` from `@tanstack/react-query`
    - `cd client && npx tsc --noEmit -p tsconfig.app.json` exits 0
  </acceptance_criteria>
  <done>
    Reducer + hooks compile cleanly; ready to be consumed by the wizard container.
  </done>
</task>

<task type="auto" tdd="false">
  <name>Task 3: Choice-Card primitive + Prereq-Confirm dialog + 2 banners (the shared building blocks)</name>
  <files>
    client/src/features/characters/components/level-up/level-up-choice-card.tsx,
    client/src/features/characters/components/level-up/level-up-prereq-confirm-dialog.tsx,
    client/src/features/characters/components/level-up/level-up-resume-banner.tsx,
    client/src/features/characters/components/level-up/level-up-violations-banner.tsx
  </files>
  <read_first>
    - .planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md (entire — these four components are fully specified):
      - Choice-Card: lines 308-359 (canonical layout, locked / selected / warning states)
      - Prereq-Confirm Dialog: lines 570-579 (z-60 layered modal, AlertTriangle + raw prereq quote)
      - DRAFT-Resume Banner: lines 583-613 (full JSX provided in UI-SPEC)
      - Violations Banner: lines 617-649 (full JSX provided in UI-SPEC)
    - client/src/features/characters/components/feat-detail-modal.tsx (lines 22-29 — featSourceColors map to REUSE)
    - client/src/features/characters/components/add-feat-modal.tsx (lines 272-330 — feat card pattern + add-condition-modal.tsx:91-103 — modal chrome)
    - client/src/shared/components/ui/index.ts (Button, Card, etc. exports)
    - client/src/index.css (design tokens — bg-bg-tertiary, text-warning-500, etc.)
  </read_first>
  <action>
    Implement each component EXACTLY per `01-UI-SPEC.md`. Do NOT redesign — UI-SPEC is the contract.

    **1. `level-up-choice-card.tsx`** — UI-SPEC §"Component Contract — Choice-Card" (lines 308-359):

    ```tsx
    import { Lock, AlertTriangle, Check } from 'lucide-react';
    import { cn } from '@/shared/lib/utils';

    export interface ChoiceCardOption {
      id: string;
      title: string;             // German display name
      englishName?: string;      // sub-line if German != English
      description?: string;      // truncated to 2 lines via line-clamp-2
      sourceBadge?: { label: string; colorClass: string };  // colorClass from featSourceColors map
      levelChip?: number;
      actionCost?: 'A' | 'AA' | 'AAA' | 'R' | 'F' | null;   // ActionIcon
      rarityChip?: 'Uncommon' | 'Rare' | 'Unique';
      isSelected: boolean;
      isLocked: boolean;          // shows Lock icon, opacity 60%, disabled
      lockReason?: string;        // tooltip
      hasNonEvaluablePrereq: boolean;  // shows yellow AlertTriangle
      rawPrereqText?: string;     // for AlertTriangle tooltip
      onSelect: (id: string) => void;
      onMore?: () => void;        // opens FeatDetailModal at z-60
    }

    export function ChoiceCard(props: ChoiceCardOption) {
      // Implement per UI-SPEC §Choice-Card layout.
      // - Container: <button> with the className composition from UI-SPEC line 318-326
      // - Top row: title + sub-line + icon stack (Lock, AlertTriangle, Check) in correct order
      // - Meta row: source badge, level chip, action-cost icon, rarity chip
      // - Description with line-clamp-2 + Mehr link
      // - States per UI-SPEC §Locked / Selected / Prereq-warning
      return (
        <button
          onClick={() => !props.isLocked && props.onSelect(props.id)}
          disabled={props.isLocked}
          className={cn(
            'w-full text-left p-4 rounded-xl border transition-colors',
            'min-h-[64px]',
            !props.isSelected && !props.isLocked && 'bg-bg-tertiary border-border hover:border-border-hover',
            props.isSelected && 'bg-bg-tertiary border-primary-500 ring-1 ring-primary-500/40',
            props.isLocked && 'bg-bg-tertiary/60 border-border opacity-60 cursor-not-allowed',
          )}
          title={props.isLocked ? props.lockReason : undefined}
        >
          {/* Top row */}
          <div className="flex items-start justify-between gap-2">
            <div className="flex-1 min-w-0">
              <h4 className="text-sm font-semibold text-text-primary">{props.title}</h4>
              {props.englishName && props.englishName !== props.title && (
                <p className="text-xs text-text-muted">{props.englishName}</p>
              )}
            </div>
            <div className="flex items-center gap-1 flex-shrink-0">
              {props.isLocked && <Lock className="h-4 w-4 text-text-muted" />}
              {props.hasNonEvaluablePrereq && (
                <AlertTriangle
                  className="h-4 w-4 text-warning-500"
                  aria-label="Voraussetzung nicht prüfbar"
                />
              )}
              {props.isSelected && <Check className="h-4 w-4 text-primary-500" />}
            </div>
          </div>

          {/* Meta row */}
          <div className="mt-2 flex flex-wrap gap-2 items-center text-xs">
            {props.sourceBadge && (
              <span className={cn('px-1.5 py-0.5 rounded font-semibold', props.sourceBadge.colorClass)}>
                {props.sourceBadge.label}
              </span>
            )}
            {props.levelChip !== undefined && (
              <span className="text-text-secondary">Stufe {props.levelChip}+</span>
            )}
            {/* Action-cost icon, rarity chip per UI-SPEC */}
          </div>

          {/* Description */}
          {props.description && (
            <p className="mt-2 text-sm text-text-secondary line-clamp-2">{props.description}</p>
          )}
          {props.onMore && (
            <button
              type="button"
              onClick={(e) => { e.stopPropagation(); props.onMore!(); }}
              className="mt-1 text-xs text-primary-500 hover:underline"
            >
              Mehr
            </button>
          )}
        </button>
      );
    }
    ```

    **2. `level-up-prereq-confirm-dialog.tsx`** — UI-SPEC §"Component Contract — Prereq-Confirm Dialog" (lines 570-579):

    ```tsx
    import { AlertTriangle } from 'lucide-react';
    import { Button } from '@/shared/components/ui';

    export interface PrereqConfirmDialogProps {
      featName: string;
      rawPrereqText: string;
      onCancel: () => void;
      onConfirm: () => void;
    }

    /** Layered modal at z-60 (sits over the wizard z-50). Used for non-evaluable prereqs (D-03). */
    export function PrereqConfirmDialog({ rawPrereqText, onCancel, onConfirm }: PrereqConfirmDialogProps) {
      return (
        <div className="fixed inset-0 z-[60] flex items-end sm:items-center justify-center" role="dialog" aria-modal="true">
          <div className="absolute inset-0 bg-black/40" onClick={onCancel} />
          <div className="relative w-full sm:max-w-md p-6 bg-bg-secondary rounded-t-2xl sm:rounded-2xl border border-warning-500/30">
            <div className="flex items-center gap-3 mb-3">
              <AlertTriangle className="h-5 w-5 text-warning-500" />
              <h3 className="text-base font-semibold text-text-primary">Voraussetzung nicht prüfbar</h3>
            </div>
            <p className="text-sm text-text-secondary mb-2">Voraussetzung:</p>
            <p className="text-sm font-semibold text-text-primary mb-4 p-3 rounded-lg bg-bg-tertiary border-l-4 border-warning-500">
              „{rawPrereqText}"
            </p>
            <p className="text-sm text-text-secondary">
              Dimension47 kann diese Bedingung nicht automatisch prüfen. Erfüllst du sie?
            </p>
            <div className="flex gap-3 mt-4 justify-end">
              <Button variant="ghost" onClick={onCancel}>Abbrechen</Button>
              <Button variant="default" onClick={onConfirm}>Trotzdem wählen</Button>
            </div>
          </div>
        </div>
      );
    }
    ```

    **3. `level-up-resume-banner.tsx`** — UI-SPEC §"Component Contract — DRAFT-Resume Banner" (lines 583-613). Use the exact JSX from UI-SPEC lines 591-613 with these tweaks:

    ```tsx
    import { RotateCcw, Trash2 } from 'lucide-react';
    import { Button } from '@/shared/components/ui';

    export interface LevelUpResumeBannerProps {
      targetLevel: number;
      lastEditedRelative: string;     // e.g. "vor 3 Tagen" via Intl.RelativeTimeFormat
      onResume: () => void;
      onDiscard: () => void;
    }

    export function LevelUpResumeBanner({ targetLevel, lastEditedRelative, onResume, onDiscard }: LevelUpResumeBannerProps) {
      return (
        <div className="bg-bg-tertiary border-l-4 border-primary-500 p-4 rounded-r-lg flex items-start justify-between gap-3">
          <div className="flex items-start gap-3">
            <RotateCcw className="h-5 w-5 text-primary-500 flex-shrink-0 mt-0.5" />
            <div>
              <h3 className="text-sm font-semibold text-text-primary">
                Du hast eine offene Stufenaufstiegs-Session — Stufe {targetLevel}.
              </h3>
              <p className="text-xs text-text-secondary mt-0.5">
                Zuletzt bearbeitet: {lastEditedRelative}.
              </p>
            </div>
          </div>
          <div className="flex items-center gap-2">
            <Button variant="ghost" size="sm" onClick={onDiscard} className="text-error-500">
              <Trash2 className="h-4 w-4" />
              <span className="ml-1">Verwerfen</span>
            </Button>
            <Button variant="default" size="sm" onClick={onResume}>
              <RotateCcw className="h-4 w-4" />
              <span className="ml-1">Fortsetzen</span>
            </Button>
          </div>
        </div>
      );
    }
    ```

    **4. `level-up-violations-banner.tsx`** — UI-SPEC §"Component Contract — Pathbuilder-Import-Violations Banner" (lines 617-649). Use the exact JSX from UI-SPEC with these tweaks:

    ```tsx
    import { useState } from 'react';
    import { AlertTriangle, ChevronDown, ChevronRight } from 'lucide-react';
    import { Button } from '@/shared/components/ui';
    import type { PrereqViolation } from '@/shared/types';

    export interface LevelUpViolationsBannerProps {
      violations: PrereqViolation[];
    }

    export function LevelUpViolationsBanner({ violations }: LevelUpViolationsBannerProps) {
      const [expanded, setExpanded] = useState(false);
      if (violations.length === 0) return null;
      return (
        <div className="bg-warning-500/10 border-l-4 border-warning-500 p-4 rounded-r-lg">
          <div className="flex items-start gap-3">
            <AlertTriangle className="h-5 w-5 text-warning-500 flex-shrink-0 mt-0.5" />
            <div className="flex-1">
              <h3 className="text-sm font-semibold text-warning-500">
                {violations.length} Talente mit nicht erfüllter Voraussetzung
              </h3>
              <p className="text-sm text-text-secondary mt-1">
                Beim Import wurde festgestellt, dass folgende Talente Voraussetzungen haben,
                die der Charakter nicht erfüllt. Liste prüfen und ggf. nachträglich anpassen.
              </p>
              <Button variant="ghost" size="sm" onClick={() => setExpanded(!expanded)} className="mt-2 -ml-2">
                {expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
                <span className="ml-1">Liste {expanded ? 'verbergen' : 'anzeigen'}</span>
              </Button>
              {expanded && (
                <ul className="mt-2 space-y-1 text-sm text-text-secondary">
                  {violations.map(v => (
                    <li key={v.featId}>
                      <span className="font-semibold text-text-primary">{v.featName}</span>
                      <span className="text-text-muted"> — Voraussetzung: {v.prereqText}</span>
                    </li>
                  ))}
                </ul>
              )}
            </div>
          </div>
        </div>
      );
    }
    ```

    **Constraints:** All copy in German. Touch targets meet 44px floor. Lucide icons only (no emojis).
  </action>
  <verify>
    <automated>cd client &amp;&amp; npx tsc --noEmit -p tsconfig.app.json 2&gt;&amp;1 | grep -E "level-up-(choice-card|prereq-confirm|resume-banner|violations-banner)" || echo "tsc clean"</automated>
  </verify>
  <acceptance_criteria>
    - All 4 files exist in `client/src/features/characters/components/level-up/`
    - `level-up-choice-card.tsx` exports `ChoiceCard` and `ChoiceCardOption` interface
    - `level-up-choice-card.tsx` contains `Lock`, `AlertTriangle`, `Check` icon imports + the conditional-render block for each
    - `level-up-prereq-confirm-dialog.tsx` exports `PrereqConfirmDialog` + uses `z-[60]`
    - `level-up-prereq-confirm-dialog.tsx` contains the German strings: `Voraussetzung nicht prüfbar`, `Trotzdem wählen`, `Abbrechen`
    - `level-up-resume-banner.tsx` exports `LevelUpResumeBanner` + contains German `Du hast eine offene Stufenaufstiegs-Session`
    - `level-up-violations-banner.tsx` exports `LevelUpViolationsBanner` + contains the German violations heading
    - All 4 files use Lucide icons only (no emojis); verify with `grep -E "[\\u{1F000}-\\u{1FFFF}]"` returning no matches
    - All 4 files use `Button` from `@/shared/components/ui`
    - `cd client && npx tsc --noEmit -p tsconfig.app.json` exits 0
  </acceptance_criteria>
  <done>
    Four shared UI primitives exist matching UI-SPEC; ready for consumption by step components and the wizard container.
  </done>
</task>

<task type="auto" tdd="false">
  <name>Task 4: 12 step components (class-features, class-feature-choice, boost, skill-increase, 5×feat, spellcaster, review)</name>
  <files>
    client/src/features/characters/components/level-up/level-up-step-class-features.tsx,
    client/src/features/characters/components/level-up/level-up-step-class-feature-choice.tsx,
    client/src/features/characters/components/level-up/level-up-step-boost.tsx,
    client/src/features/characters/components/level-up/level-up-step-skill-increase.tsx,
    client/src/features/characters/components/level-up/level-up-step-feat-class.tsx,
    client/src/features/characters/components/level-up/level-up-step-feat-skill.tsx,
    client/src/features/characters/components/level-up/level-up-step-feat-general.tsx,
    client/src/features/characters/components/level-up/level-up-step-feat-ancestry.tsx,
    client/src/features/characters/components/level-up/level-up-step-feat-archetype.tsx,
    client/src/features/characters/components/level-up/level-up-step-spellcaster.tsx,
    client/src/features/characters/components/level-up/level-up-step-review.tsx
  </files>
  <read_first>
    - .planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md (per-step contracts):
      - Class-Features step: §Wizard-step screen headings (line 156)
      - Class-Feature-Choice (D-19): §Component Contract — Choice-Klassenmerkmal Sub-Step (lines 480-489)
      - Boost: §Component Contract — Boost-Set Step (lines 362-423) — FULL JSX provided
      - Skill-Increase: §Component Contract — Skill-Increase Step (lines 426-450)
      - Feat steps: §Component Contract — Choice-Card (lines 308-359) — all 5 feat steps reuse ChoiceCard with different filters
      - FA-Slot: §Component Contract — Free-Archetype-Slot Step (lines 454-463)
      - Spellcaster: §Component Contract — Spellcaster Step (lines 466-477)
      - Review: §Component Contract — Review Step (lines 492-565) — Section A choices summary + Section B Vorher/Nachher cards + Section C spellcaster diff + Ändern revision contract
    - client/src/features/characters/components/level-up/level-up-choice-card.tsx (Task 3 — primitive)
    - client/src/features/characters/components/level-up/wizard-state-reducer.ts (Task 2 — events to dispatch)
    - client/src/features/characters/components/feat-detail-modal.tsx (lines 22-29 — featSourceColors map to REUSE)
    - client/src/features/characters/components/hp-control.tsx (analog: +/- counter pattern for Boost step)
  </read_first>
  <action>
    Implement each of the 11 step components per UI-SPEC. Each component:
    - Receives `(state: WizardState, dispatch: (ev: WizardEvent) => void, characterId: string, ...)` as props OR uses a context provider — planner discretion.
    - Uses the ChoiceCard primitive from Task 3 for talent picks (5 feat steps + class-feature-choice + spellcaster repertoire picks).
    - Uses the existing FeatDetailModal (or a new `level-up-detail-modal.tsx` if needed) for the Mehr / detail-popover, opened at `z-60` per UI-SPEC.

    **Critical implementation notes per step:**

    **A. `level-up-step-class-features.tsx`** — Auto-summary, no input. Renders the `ClassProgression.grants` list for the new level as a read-only list. Header + sub-line per UI-SPEC. Server endpoint to fetch the list: GET ClassProgression for `(className, targetLevel)` — Plan 04 may need a small read endpoint OR the wizard can fetch from a public ClassProgression endpoint. If neither exists, the wizard receives the data via the LevelUpSession's preview endpoint.

    **B. `level-up-step-class-feature-choice.tsx`** -- D-19 sub-step (Cleric Doctrine, Wizard School, etc.). Calls `api.getLevelUpClassFeatureOptions(characterId, sessionId, optionsRef)` (added in Task 1 alongside the other api.* methods) which hits `GET /characters/:characterId/level-up/:sessionId/class-feature-options/:optionsRef` -- this endpoint is provided by Plan 04's LevelingController (see 01-04-PLAN.md Task 5). Renders one ChoiceCard per option (single-select / radio-group semantics). Dispatches `SET_CLASS_FEATURE_CHOICE`. No server-side edits in this plan -- the endpoint is already present.

    **C. `level-up-step-boost.tsx`** — UI-SPEC lines 362-423 give the FULL JSX. Use it verbatim with these wires:
    - State source: `state.choices.boostTargets` (current selection — array of 0..4 strings)
    - Per attribute, count = 1 if in boostTargets else 0
    - Dec: removes the ability from boostTargets. Inc: adds (only if length < 4 and not already present)
    - Dispatches `SET_BOOST_TARGETS`
    - Live-preview "wird {newScore}" calls a local `applyBoostLocal(currentScore)` helper (mirror of server's applyAttributeBoost — could import shared lib if a shared package were set up; for now, inline a 5-line client helper)
    - Footer-helper "X von 4 Boosts gewählt"

    **D. `level-up-step-skill-increase.tsx`** — UI-SPEC lines 426-450. Renders all skills as compact rows. For each row: current rank → next rank (or "Cap erreicht" chip if `canIncreaseSkill` would return false; mirror Plan 02 logic client-side). Dispatch `SET_SKILL_INCREASE` on click.

    **E. `level-up-step-feat-class.tsx`** through **`level-up-step-feat-archetype.tsx`** (5 files) -- All five feat steps share the same shape:
    - Fetch filtered feat list via `api.getLevelUpFeats(characterId, sessionId, slot, includeUnavailable)` (added in Task 1 alongside the other api.* methods). It hits `GET /characters/:characterId/level-up/:sessionId/feats?slot=class|skill|general|ancestry|archetype&includeUnavailable=true|false` returning `FeatWithEval[]` -- this endpoint is provided by Plan 04's LevelingController (see 01-04-PLAN.md Task 5). No server-side edits in this plan.
    - Render each feat as a ChoiceCard.
    - For non-evaluable prereqs: show yellow AlertTriangle on the card; clicking the card opens the PrereqConfirmDialog (Task 3) -> on confirm, dispatch `ACKNOWLEDGE_PREREQ_WARNING` + `SET_FEAT`.
    - For failed prereqs (`{ok:false}`): hidden by default; toggle "Auch nicht erfuellbare anzeigen" passes `includeUnavailable=true` to the api call to reveal them grayed.
    - Slot-specific: feat-archetype only renders if `state.steps.includes('feat-archetype')` (FA enabled); shows the "vor/nach Dedication" filter per UI-SPEC line 462.

    **F. `level-up-step-spellcaster.tsx`** — UI-SPEC lines 466-477. Two sub-shapes:
    - Prepared caster: info strip "+N Slot auf Grad M (automatisch)" + immediate `Weiter` enable.
    - Spontaneous caster: info strip + repertoire-pick sub-section. Renders spell ChoiceCards filtered by tradition. Dispatch `SET_REPERTOIRE_PICKS`.

    **G. `level-up-step-review.tsx`** — UI-SPEC lines 492-565. THIS IS THE LOAD-BEARING STEP:
    - **Section A** — Wahlen-Zusammenfassung: Card with one row per choice. Each row has an `Ändern` link that dispatches `GO_TO_STEP_FROM_REVIEW` (entering revision mode).
    - **Section B** — Vorher/Nachher cards: Use `useLevelUpPreviewQuery(characterId, sessionId, enabled=true)` from Task 2. Render two `Card`s side-by-side on `sm:`, stacked on mobile. Display `before` / `after` HP-Max, RK, Klassen-DC, Wahrnehmung, Saves. Use `font-mono text-2xl` for stat numbers. Show success-tinted delta chips (`+X`).
    - **Section C** — Spellcaster diff (only if `preview.spellcaster` is non-null): table of slot increments + repertoire delta.
    - **Bestätigen button** in the wizard footer (handled by the wizard container, NOT this step) calls `useCommitLevelUpMutation(characterId, sessionId).mutateAsync(...)`.

    **Constraint specific to all step files:**
    - Each is a function component with named export.
    - Props interface ends with `Props`.
    - All copy in German (UI-SPEC §Wizard-step screen headings line 156 supplies the exact strings).
    - All touch targets ≥ 44px.
    - No `: any`.

    **Server endpoints used by these steps (already provided by Plan 04 -- no patches in this plan):**
      - `GET /characters/:characterId/level-up/:sessionId/feats?slot=<kind>&includeUnavailable=<bool>` -> `FeatWithEval[]`
      - `GET /characters/:characterId/level-up/:sessionId/class-feature-options/:optionsRef` -> `ClassFeatureOption[]`
      - `GET /characters/:characterId/level-up` -> open DRAFT or 404 (used by character-sheet-page banner detection)
    All three are declared in 01-04-PLAN.md Task 5 (LevelingController) and 01-04-PLAN.md Task 4 (LevelingService).
    LevelUpSessionDto already includes `steps: StepKind[]` (Plan 04 Task 1) -- the wizard reads it directly from the start/resume response.
  </action>
  <verify>
    <automated>cd client &amp;&amp; npx tsc --noEmit -p tsconfig.app.json 2&gt;&amp;1 | grep -E "level-up-step" || echo "tsc clean"</automated>
  </verify>
  <acceptance_criteria>
    - All 11 step files exist in `client/src/features/characters/components/level-up/`
    - Each file uses `function` export with PascalCase name (e.g. `LevelUpStepBoost`)
    - Each file imports `Button` and other primitives from `@/shared/components/ui`
    - Each file uses German strings matching UI-SPEC §Copywriting (line 156)
    - `level-up-step-boost.tsx` contains the literal string `wird` (live preview text)
    - `level-up-step-boost.tsx` contains `h-11 w-11` (44px +/- buttons)
    - `level-up-step-boost.tsx` contains the literal string `Cap bei 18` (warning chip)
    - `level-up-step-skill-increase.tsx` contains the literal string `Cap erreicht`
    - `level-up-step-review.tsx` uses `useLevelUpPreviewQuery` (Task 2 hook)
    - `level-up-step-review.tsx` contains the literal string `Vorher` AND `Nachher`
    - All step files contain NO `: any` outside comments
    - All step files contain NO emoji literals
    - `cd client && npx tsc --noEmit -p tsconfig.app.json` exits 0
    - `cd server && npm run build` exits 0 (no server changes in this plan; the build remains green from Plan 04)
  </acceptance_criteria>
  <done>
    All 11 step components implemented per UI-SPEC; all use German copy; all use ChoiceCard for talent picks; Review step wires up the preview query. Server endpoints consumed are owned by Plan 04 -- no server-side edits in this plan.
  </done>
</task>

<task type="auto" tdd="false">
  <name>Task 5: Wizard container (level-up-wizard.tsx) — chrome, stepper, motion, footer wiring</name>
  <files>client/src/features/characters/components/level-up/level-up-wizard.tsx</files>
  <read_first>
    - .planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md (entire — chrome/header/stepper/footer all specified):
      - Chrome §Component Contract — Wizard Chrome (lines 221-305)
      - Motion §Motion (lines 654-665)
      - States §States Inventory (lines 671-687)
    - client/src/features/characters/components/level-up/wizard-state-reducer.ts (Task 2)
    - client/src/features/characters/components/level-up/use-level-up-session.ts (Task 2)
    - client/src/features/characters/components/level-up/level-up-step-*.tsx (Task 4 — all 11)
    - client/src/features/characters/components/level-up/level-up-prereq-confirm-dialog.tsx (Task 3)
    - client/src/features/characters/components/rest-modal.tsx (analog: REST + commit lifecycle)
    - client/src/features/characters/components/add-feat-modal.tsx (analog: modal chrome)
  </read_first>
  <action>
    Create `level-up-wizard.tsx` — the outer container that:
    1. Renders the modal chrome per UI-SPEC §Component Contract — Wizard Chrome.
    2. Loads the session via `useStartLevelUpMutation` on mount (start-or-resume).
    3. Reads the step list from the session response: `LevelUpSessionDto.steps: StepKind[]` is populated server-side by Plan 04 (LevelingService.startOrResume runs `computeApplicableSteps` on the character + targetLevel and stores the result in the response payload). The wizard simply uses `session.steps` directly -- no client-side computation, no extra round-trip.
    4. Initializes the reducer via `initWizardState(session, steps)`.
    5. Renders the active step component based on `state.steps[state.currentIdx]`.
    6. PATCHes the DRAFT after each meaningful state change (debounced — every 500ms after a SET_* event).
    7. Wraps the active step in `<AnimatePresence mode="wait">` + `<motion.div ...>` for slide transitions per UI-SPEC §Motion.
    8. Renders the stepper, header (with X close button + character name), and footer (Zurück / Weiter / Bestätigen / Zurück zur Übersicht based on state).
    9. Handles the backdrop-click and X-close → opens the "Wizard mid-flow abbrechen" confirm dialog (reuse PrereqConfirmDialog with different copy, OR a separate small dialog component — planner discretion).
    10. On Bestätigen: `useCommitLevelUpMutation.mutateAsync()` → on success, close the wizard, fire `onCommitted` callback (parent invalidates character query + shows toast).

    **Skeleton:**

    ```tsx
    import { useEffect, useReducer, useMemo, useState } from 'react';
    import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
    import { X, ChevronLeft, ChevronRight, Check, ArrowLeft } from 'lucide-react';
    import { Button, Spinner } from '@/shared/components/ui';
    import { cn } from '@/shared/lib/utils';
    import { wizardReducer, initWizardState } from './wizard-state-reducer';
    import {
      useStartLevelUpMutation,
      usePatchLevelUpMutation,
      useCommitLevelUpMutation,
    } from './use-level-up-session';
    import { LevelUpStepClassFeatures } from './level-up-step-class-features';
    import { LevelUpStepClassFeatureChoice } from './level-up-step-class-feature-choice';
    import { LevelUpStepBoost } from './level-up-step-boost';
    import { LevelUpStepSkillIncrease } from './level-up-step-skill-increase';
    import { LevelUpStepFeatClass } from './level-up-step-feat-class';
    import { LevelUpStepFeatSkill } from './level-up-step-feat-skill';
    import { LevelUpStepFeatGeneral } from './level-up-step-feat-general';
    import { LevelUpStepFeatAncestry } from './level-up-step-feat-ancestry';
    import { LevelUpStepFeatArchetype } from './level-up-step-feat-archetype';
    import { LevelUpStepSpellcaster } from './level-up-step-spellcaster';
    import { LevelUpStepReview } from './level-up-step-review';
    import type { Character, StepKind } from '@/shared/types';

    export interface LevelUpWizardProps {
      character: Character;
      onClose: () => void;
      onCommitted: () => void;
    }

    const STEP_LABELS: Record<StepKind, string> = {
      'class-features': 'Merkmale',
      'class-feature-choice': 'Wahl',
      'boost': 'Boost',
      'skill-increase': 'Skill',
      'feat-class': 'Klasse',
      'feat-skill': 'Fertigkeit',
      'feat-general': 'Allgemein',
      'feat-ancestry': 'Abstammung',
      'feat-archetype': 'Archetyp',
      'spellcaster': 'Zauber',
      'review': 'Übersicht',
    };

    const STEP_HEADINGS: Record<StepKind, { heading: string; sub: string }> = {
      'class-features': { heading: 'Klassenmerkmale auf Stufe {N}', sub: 'Diese Merkmale werden automatisch übernommen.' },
      // ... all 11 per UI-SPEC line 156 table
    };

    export function LevelUpWizard({ character, onClose, onCommitted }: LevelUpWizardProps) {
      const startMut = useStartLevelUpMutation(character.id);
      const [state, dispatch] = useReducer(wizardReducer, undefined as never);
      const [direction, setDirection] = useState<'forward' | 'back'>('forward');
      const [showAbortDialog, setShowAbortDialog] = useState(false);
      const reducedMotion = useReducedMotion();

      // Start/resume on mount
      useEffect(() => {
        startMut.mutate(undefined);
      }, [character.id]);

      // Once session loads, init the reducer
      // (executor wires up: pull steps from session response + initWizardState)

      // PATCH on choice change — debounced 500ms
      // (executor wires up: useEffect on state.choices, debounced patchLevelUp)

      const commitMut = useCommitLevelUpMutation(character.id, state?.sessionId ?? '');

      const handleClose = () => {
        if (state && Object.keys(state.choices).length > 0) {
          setShowAbortDialog(true);
        } else {
          onClose();
        }
      };

      const handleCommit = async () => {
        await commitMut.mutateAsync(state.acknowledgedNonEvaluablePrereqs.length > 0);
        onCommitted();
        onClose();
      };

      if (startMut.isPending) {
        return (
          <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
            <Spinner />
          </div>
        );
      }

      if (!state) return null;

      const currentStep = state.steps[state.currentIdx];
      const heading = STEP_HEADINGS[currentStep].heading.replace('{N}', String(state.targetLevel));
      const isLastStep = state.currentIdx === state.steps.length - 1;
      const isFirstStep = state.currentIdx === 0;

      return (
        <div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center" role="dialog" aria-modal="true">
          <div className="absolute inset-0 bg-black/60" onClick={handleClose} />
          <div className="relative w-full sm:max-w-2xl max-h-[90vh] bg-bg-secondary rounded-t-2xl sm:rounded-2xl flex flex-col overflow-hidden">

            {/* Header */}
            <div className="flex items-center justify-between p-4 border-b border-border">
              <div>
                <h2 className="text-lg font-semibold text-text-primary">Stufenaufstieg — Stufe {state.targetLevel}</h2>
                <p className="text-xs text-text-secondary">{character.name}</p>
              </div>
              <Button variant="ghost" size="icon" onClick={handleClose} aria-label="Schließen">
                <X className="h-5 w-5" />
              </Button>
            </div>

            {/* Stepper */}
            <nav role="navigation" aria-label="Wizard-Schritte" className="px-4 py-3 border-b border-border">
              <div className="text-xs text-text-secondary pb-2">
                Schritt {state.currentIdx + 1} von {state.steps.length} — {STEP_LABELS[currentStep]}
              </div>
              <ol className="flex items-center gap-2 overflow-x-auto">
                {state.steps.map((stepKind, idx) => (
                  // ... per UI-SPEC stepper lines 263-285
                  <li key={`${stepKind}-${idx}`}>
                    <button
                      type="button"
                      onClick={() => { setDirection(idx < state.currentIdx ? 'back' : 'forward'); dispatch({ type: 'GO_TO_STEP', idx }); }}
                      disabled={idx > state.currentIdx}
                      aria-current={idx === state.currentIdx ? 'step' : undefined}
                      className="h-11 w-11 -m-1 flex items-center justify-center disabled:cursor-not-allowed"
                    >
                      <span className={cn(
                        'rounded-full transition-colors duration-150',
                        idx === state.currentIdx ? 'h-2.5 w-2.5 bg-primary-500' :
                        idx < state.currentIdx ? 'h-2 w-2 bg-primary-500/40' :
                        'h-2 w-2 bg-bg-tertiary',
                      )} />
                    </button>
                    {idx < state.steps.length - 1 && <span className={cn('h-px w-4', idx < state.currentIdx ? 'bg-primary-500/40' : 'bg-bg-tertiary')} />}
                  </li>
                ))}
              </ol>
            </nav>

            {/* Body */}
            <div className="flex-1 overflow-y-auto p-4 sm:p-6">
              <h3 className="text-lg font-semibold text-text-primary mb-1">{heading}</h3>
              <p className="text-sm text-text-secondary mb-4">{STEP_HEADINGS[currentStep].sub}</p>
              <AnimatePresence mode="wait">
                <motion.div
                  key={currentStep}
                  initial={reducedMotion ? { opacity: 0 } : { opacity: 0, x: direction === 'forward' ? 24 : -24 }}
                  animate={reducedMotion ? { opacity: 1 } : { opacity: 1, x: 0 }}
                  exit={reducedMotion ? { opacity: 0 } : { opacity: 0, x: direction === 'forward' ? -24 : 24 }}
                  transition={{ duration: reducedMotion ? 0.1 : 0.2, ease: 'easeOut' }}
                >
                  {renderStep(currentStep, state, dispatch, character)}
                </motion.div>
              </AnimatePresence>
            </div>

            {/* Footer */}
            <div className="flex items-center justify-between gap-3 p-4 border-t border-border bg-bg-secondary">
              <span className="text-xs text-text-secondary">Schritt {state.currentIdx + 1}/{state.steps.length}</span>
              <div className="flex items-center gap-2">
                <Button
                  variant="outline"
                  onClick={() => { setDirection('back'); dispatch({ type: 'GO_PREV' }); }}
                  disabled={isFirstStep}
                >
                  <ChevronLeft className="h-4 w-4" /> Zurück
                </Button>
                {state.revisionMode ? (
                  <Button variant="default" onClick={() => dispatch({ type: 'RETURN_TO_REVIEW' })}>
                    <ArrowLeft className="h-4 w-4" /> Zurück zur Übersicht
                  </Button>
                ) : isLastStep ? (
                  <Button variant="default" onClick={handleCommit} isLoading={commitMut.isPending}>
                    <Check className="h-4 w-4" /> Bestätigen
                  </Button>
                ) : (
                  <Button variant="default" onClick={() => { setDirection('forward'); dispatch({ type: 'GO_NEXT' }); }} disabled={!isStepValid(currentStep, state)}>
                    Weiter <ChevronRight className="h-4 w-4" />
                  </Button>
                )}
              </div>
            </div>
          </div>

          {/* Abort confirm dialog */}
          {showAbortDialog && (
            // Reuse PrereqConfirmDialog shape with different copy, OR inline a similar dialog.
            // Per UI-SPEC §Destructive confirmations:
            //   Heading: 'Wizard abbrechen?'
            //   Body: 'Deine bisherigen Wahlen werden als Entwurf gespeichert und du kannst später fortsetzen.'
            //   Buttons: 'Weiter bearbeiten' (default) + 'Als Entwurf speichern und schließen' (outline)
            null
          )}
        </div>
      );
    }

    function renderStep(kind: StepKind, state: WizardState, dispatch: (e: WizardEvent) => void, character: Character) {
      switch (kind) {
        case 'class-features': return <LevelUpStepClassFeatures state={state} character={character} />;
        case 'class-feature-choice': return <LevelUpStepClassFeatureChoice state={state} dispatch={dispatch} character={character} />;
        case 'boost': return <LevelUpStepBoost state={state} dispatch={dispatch} character={character} />;
        // ... all 11
      }
    }

    function isStepValid(kind: StepKind, state: WizardState): boolean {
      switch (kind) {
        case 'class-features': return true;
        case 'boost': return state.choices.boostTargets?.length === 4;
        case 'skill-increase': return !!state.choices.skillIncrease;
        case 'feat-class': return !!state.choices.featClassId;
        // ... per step
        default: return true;
      }
    }
    ```

    **Constraints:**
    - All copy German.
    - Modal chrome exactly matches UI-SPEC line 222-238.
    - Stepper has 44×44 hit-zones with -m-1 negative margin (UI-SPEC line 271-275).
    - Motion respects `prefers-reduced-motion` (UI-SPEC line 660).
  </action>
  <verify>
    <automated>cd client &amp;&amp; npx tsc --noEmit -p tsconfig.app.json 2&gt;&amp;1 | grep -E "level-up-wizard" || echo "tsc clean"</automated>
  </verify>
  <acceptance_criteria>
    - File `level-up-wizard.tsx` exists with `LevelUpWizard` named export
    - File contains `useReducer(wizardReducer` (uses Task 2 reducer)
    - File contains `useStartLevelUpMutation` AND `useCommitLevelUpMutation` (uses Task 2 hooks)
    - File contains `<AnimatePresence mode="wait">` (motion per UI-SPEC §Motion)
    - File contains `useReducedMotion` (accessibility per UI-SPEC line 660)
    - File contains `Stufenaufstieg — Stufe` (header German copy)
    - File contains `Schritt {state.currentIdx + 1} von {state.steps.length}` or close (always-visible progress label per UI-SPEC line 257)
    - File contains `h-11 w-11` (stepper hit-zone) AND `-m-1` (negative margin to compensate)
    - File contains `aria-modal="true"` and `role="dialog"` (a11y)
    - File contains `Bestätigen`, `Zurück`, `Weiter`, `Zurück zur Übersicht` German strings
    - File contains NO emoji
    - File contains NO `: any` outside comments
    - `cd client && npx tsc --noEmit -p tsconfig.app.json` exits 0
  </acceptance_criteria>
  <done>
    Wizard container compiles, mounts, manages reducer, drives steps via switch, runs motion, exposes Bestätigen wired to commit mutation, abort flow handled.
  </done>
</task>

<task type="auto" tdd="false">
  <name>Task 6: character-sheet-page.tsx — Stufe-steigen button + 2 banner mounts + wizard mount</name>
  <files>client/src/features/characters/components/character-sheet-page.tsx</files>
  <read_first>
    - client/src/features/characters/components/character-sheet-page.tsx (entire file — header cluster lines 1607-1626; modal mount cluster lines 1652-1666; state pattern lines 122-132)
    - .planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md (lines 208-217 — Stufe-steigen button states; lines 588 — banner positions)
    - .planning/phases/01-level-up-pf2e-regelkonform/01-PATTERNS.md (lines 728-765 — exact insert positions)
  </read_first>
  <action>
    Make four additions to `character-sheet-page.tsx`:

    **Add 1 — State variables** (around lines 122-132 with the other useState's):
    ```tsx
    const [showLevelUpWizard, setShowLevelUpWizard] = useState(false);
    const [hasOpenDraft, setHasOpenDraft] = useState(false);
    const [openDraftMeta, setOpenDraftMeta] = useState<{ targetLevel: number; updatedAt: string } | null>(null);
    ```

    **Add 2 — Effect to detect open DRAFT on character load** (after the character query):
    ```tsx
    useEffect(() => {
      if (!character) return;
      // GET /characters/:id/level-up should return the open DRAFT or 404
      // Implementation: call api.startLevelUp() with no targetLevel — if a DRAFT exists, it's returned;
      // if not, a new DRAFT is created. To avoid spurious DRAFT creation on a fresh load, instead
      // add a separate read-only endpoint. Planner: extend Plan 04 with GET /characters/:id/level-up
      // (returns DRAFT or 404). For now: lazily check by leaving hasOpenDraft = false until the user
      // clicks 'Stufe steigen' and the start endpoint resolves it. The banner then hydrates on commit/discard mutations.
    }, [character]);
    ```

    Note for executor: DRAFT detection uses the `GET /characters/:characterId/level-up` endpoint already provided by Plan 04 (LevelingController.getOpenDraft -- 404 when none, 200 with the DRAFT row when present). Wire it via `api.getOpenLevelUpDraft(characterId)` (added in Task 1) and use `useQuery({ queryKey: ['levelUpDraft', characterId], queryFn: () => api.getOpenLevelUpDraft(characterId), retry: false })`. The banner reads `data` and `setHasOpenDraft(!!data)`. No server-side changes in this plan.

    **Add 3 — Header button** (in the header cluster around lines 1607-1626 — INSERT AS FIRST in the cluster, left of Download):

    ```tsx
    {(isOwner || isGM) && character.level < 20 && (
      <Button
        variant="default"
        size="sm"
        onClick={() => setShowLevelUpWizard(true)}
      >
        {hasOpenDraft ? <RotateCcw className="h-4 w-4" /> : <Sparkles className="h-4 w-4" />}
        <span className="ml-1">{hasOpenDraft ? 'Stufe fortsetzen' : 'Stufe steigen'}</span>
      </Button>
    )}
    ```

    Add the icon imports: `import { Sparkles, RotateCcw } from 'lucide-react';` (or extend existing import).

    **Add 4 — Banner mounts** (above the avatar header AND below the avatar header for the violations banner):

    ```tsx
    {/* Resume banner — above avatar */}
    {hasOpenDraft && openDraftMeta && (
      <LevelUpResumeBanner
        targetLevel={openDraftMeta.targetLevel}
        lastEditedRelative={formatRelativeDate(openDraftMeta.updatedAt)}
        onResume={() => setShowLevelUpWizard(true)}
        onDiscard={async () => {
          // Use useDiscardLevelUpMutation
          // ... wire it
        }}
      />
    )}

    {/* Avatar header (existing) */}

    {/* Violations banner — below avatar, above tabs */}
    {character.prereqViolations?.violations && character.prereqViolations.violations.length > 0 && (
      <LevelUpViolationsBanner violations={character.prereqViolations.violations} />
    )}

    {/* Tab navigation (existing) */}
    ```

    Add a tiny `formatRelativeDate(iso: string): string` helper using `Intl.RelativeTimeFormat`:
    ```tsx
    function formatRelativeDate(iso: string): string {
      const diffMs = Date.now() - new Date(iso).getTime();
      const days = Math.floor(diffMs / 86400000);
      const rtf = new Intl.RelativeTimeFormat('de-DE', { numeric: 'auto' });
      if (days === 0) return rtf.format(0, 'day');     // 'heute'
      return rtf.format(-days, 'day');                  // 'vor 3 Tagen'
    }
    ```

    **Add 5 — Wizard mount** (in the modal mounts area lines 1652-1666):

    ```tsx
    {showLevelUpWizard && (
      <LevelUpWizard
        character={character}
        onClose={() => setShowLevelUpWizard(false)}
        onCommitted={() => {
          setShowLevelUpWizard(false);
          // refetchCharacter or rely on react-query invalidate from the commit mutation
        }}
      />
    )}
    ```

    **Add 6 — Wire WebSocket callback** to invalidate character query when level_up_committed arrives:

    Find where `useCharacterSocket` is called (probably elsewhere in the page or a parent). Add the new callback:
    ```tsx
    useCharacterSocket({
      characterId: character.id,
      // ... existing callbacks ...
      onLevelUpCommitted: () => {
        queryClient.invalidateQueries({ queryKey: ['character', character.id] });
      },
    });
    ```

    **Constraints:**
    - Do NOT change any existing rendering logic for HP, conditions, inventory, etc. The additions are strictly additive.
    - All visible new copy German.
    - Use existing icon imports where possible; extend the `lucide-react` import line.
  </action>
  <verify>
    <automated>cd client &amp;&amp; npx tsc --noEmit -p tsconfig.app.json 2&gt;&amp;1 | grep -E "character-sheet-page" || echo "tsc clean"</automated>
  </verify>
  <acceptance_criteria>
    - File `character-sheet-page.tsx` contains the literal string `Stufe steigen` (or `Stufe fortsetzen`)
    - File contains the literal string `<LevelUpWizard` (mount JSX)
    - File contains the literal string `<LevelUpResumeBanner` (banner mount)
    - File contains the literal string `<LevelUpViolationsBanner` (banner mount)
    - File contains import for `Sparkles` and `RotateCcw` from `lucide-react`
    - File contains import for `LevelUpWizard`, `LevelUpResumeBanner`, `LevelUpViolationsBanner` from `./level-up/...`
    - File contains the literal string `onLevelUpCommitted` (WebSocket callback wired)
    - `cd client && npx tsc --noEmit -p tsconfig.app.json` exits 0
    - `cd client && npm run build` exits 0 (full Vite production build)
  </acceptance_criteria>
  <done>
    Character sheet now has the entry point + the resume banner + the violations banner + the wizard mount + the WebSocket callback wired.
  </done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 7: Human verification — walk through the wizard on mobile viewport</name>
  <what-built>
    The full Level-Up wizard (Plans 01-05). Player can click "Stufe steigen" on a character sheet, walk through 4-11 steps depending on level/class/FA/caster, see Vorher/Nachher in Review, click Bestätigen, and have the character mutated atomically with a single WebSocket broadcast.

    Specifically:
    - Server: 5 REST endpoints + 1 WebSocket event
    - Client: 1 modal wizard + 11 step components + 2 banners + character-sheet integration
    - DB: 4 new tables, partial unique index, ClassProgression seeded with 320+ rows
    - Tests: ≥60 unit + integration tests passing

    Manual verification is required for the UI surface because:
    1. There is no client-side test framework yet (per VALIDATION.md decision — Phase 1 doesn't introduce vitest).
    2. UI-SPEC compliance is visual.
    3. The "feel" of the wizard on mobile (375×667) is a product judgment.
  </what-built>
  <how-to-verify>
    1. Start the dev environment:
       ```bash
       # Terminal 1
       cd server && npm run start:dev
       # Terminal 2
       cd client && npm run dev
       ```
    2. Open Chrome DevTools, set viewport to **iPhone SE (375×667)** (Mobile-First per CLAUDE.md).
    3. Log in as a user who owns at least one character below level 20.
    4. Open that character's character sheet.
    5. **Verify the "Stufe steigen" button appears** at the FIRST position in the header button cluster (left of Download), with `Sparkles` icon and German label.
    6. Click "Stufe steigen". **Verify the wizard opens as a bottom-sheet** (mobile) with:
       - Header: `Stufenaufstieg — Stufe N`, character name beneath, X close button on right.
       - Stepper: dot row at top, with always-visible `Schritt 1 von M — Merkmale` progress label.
       - Body: first step (Klassenmerkmale auf Stufe N) heading + sub-line.
       - Footer: `Zurück` (disabled on first step) + `Weiter`.
    7. **Walk through each applicable step.** For a Fighter L4→L5 (boost level), the steps are: Klassenmerkmale → Boost → Skill-Increase → Klassentalent → Fertigkeitstalent → Abstammungstalent → Übersicht. For each step, verify:
       - The step heading + sub-line match `01-UI-SPEC.md` §Wizard-step screen headings (line 156).
       - Touch targets are ≥44px (use DevTools "Inspect" → check `Computed → height/width`).
       - Choice-cards have the correct source-color badges (Klasse=red, Abstammung=blue, etc.) per UI-SPEC §Color §Inherited Exceptions.
       - For Boost step: 6 attribute rows with +/- buttons; tapping `+` increments the boost count, shows "wird {newScore}" preview, "+1 (Cap bei 18)" chip appears for STR if STR is at 18. Footer hint "X von 4 Boosts gewählt" updates.
       - The Weiter button is disabled when the step is invalid (e.g. fewer than 4 boosts).
    8. **Verify the prereq-confirm dialog (D-03):** if any feat in the Klassentalent / Fertigkeitstalent step has a yellow `AlertTriangle`, click it. A z-60 layered dialog appears with the raw prereq quote. `Trotzdem wählen` selects it; `Abbrechen` cancels.
    9. **Reach the Review step.** Verify:
       - Section A: Wahlen-Zusammenfassung lists every choice made, each with `Ändern` link.
       - Section B: Vorher / Nachher cards with HP-Max, RK, Klassen-DC, Wahrnehmung, Saves. Numbers are in `font-mono`. `Sparkles` icon next to "Nachher". Positive deltas show as green chips.
       - Section C: only appears for caster characters; shows slot/cantrip increment.
    10. **Test the Ändern revision contract:** click `Ändern` on the Boost row. Wizard navigates back. Footer button changes from `Weiter` to `Zurück zur Übersicht` (with `ArrowLeft` icon). Change a boost. Click `Zurück zur Übersicht`. Wizard returns to Review.
    11. **Click Bestätigen.** Verify:
        - Spinner appears on the button.
        - Wizard closes.
        - Toast appears (planner discretion on toast lib): `Stufenaufstieg bestätigt — Stufe N.`
        - Character header now shows the new level.
        - HP-Max updated. AC, Saves, Klassen-DC updated.
        - `hpCurrent` UNCHANGED (Pitfall #9 — verify by checking HP shows e.g. 12/63 NOT 63/63).
    12. **Open a second browser tab** on the same character (different user with access — e.g. GM viewing a player's sheet). Confirm the new level + stat block appears within ~1s without a page reload (WebSocket sync).
    13. **Test DRAFT-Resume:**
        - Start a level-up, make some choices, close the wizard via X (the abort dialog appears: confirm with `Als Entwurf speichern und schließen`).
        - Verify the DRAFT-Resume Banner now appears at the top of the character sheet with `Du hast eine offene Stufenaufstiegs-Session — Stufe N.`, `Zuletzt bearbeitet: vor wenigen Minuten.`
        - Click `Verwerfen`. Confirm the discard dialog. The DRAFT row disappears.
    14. **Test Pathbuilder-Import-Violations Banner (D-06):**
        - Re-import a Pathbuilder JSON whose feats violate prereqs. Open the character sheet. The yellow violations banner appears below the avatar header. Click `Liste anzeigen` to expand the list of violating feats.
    15. **Verify accessibility basics:**
        - Tab through the wizard with the keyboard. All controls reachable.
        - The stepper-dot wrappers are keyboard-focusable.
        - The choice-cards announce their selected/locked/warning state via aria-label.
        - In DevTools "Rendering" panel, enable `Emulate CSS prefers-reduced-motion: reduce`. Re-walk the wizard. Step transitions should fade only (no horizontal slide).
    16. **Verify TypeScript + build green** (executor confirms before this checkpoint):
        ```bash
        cd server && npm run build && npm test -- --testPathPattern=leveling
        cd client && npx tsc --noEmit -p tsconfig.app.json && npm run build
        ```
    17. If everything checks out, type `approved` to mark the checkpoint passed.
    18. If any item fails, describe the issue and the executor revises.
  </how-to-verify>
  <resume-signal>Type "approved" or describe issues found</resume-signal>
  <files>(no file writes — manual UI verification only)</files>
  <action>Pause execution and present the &lt;what-built&gt; and &lt;how-to-verify&gt; sections to the developer. Wait for the &lt;resume-signal&gt;. Do not modify any files during this checkpoint.</action>
  <verify>Developer types "approved" — or describes issues found that block approval.</verify>
  <done>Developer has walked through the wizard on a 375×667 mobile viewport per the &lt;how-to-verify&gt; steps and replied "approved".</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| browser → REST | All wizard interactions cross HTTP via Plan 04 endpoints; client trusts server's authoritative responses. |
| browser → WebSocket | level_up_committed event arrives from server; client invalidates query — no inbound user-tampering surface. |
| translated text → React render | German prereq strings + class-feature descriptions from TranslationsService render in the prereq-confirm dialog and ChoiceCard. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-1-W4-01 | Tampering | Player crafts a custom POST commit bypassing the wizard's validation | mitigate | Plan 04 server-side validation (DTO + commit guards) is the source of truth; the wizard is convenience UI. Any client tampering is rejected by the server. |
| T-1-W4-02 | Injection | Stored XSS via German feat-prereq translation text rendered in PrereqConfirmDialog or ChoiceCard | mitigate | All German strings rendered via React text-binding (`{prereqText}` inside `<p>`), never `dangerouslySetInnerHTML`. Verified by code review — no usage of dangerouslySetInnerHTML in any of the new client files. |
| T-1-W4-03 | Injection | Stored XSS via class-feature description rendered in ChoiceCard.description | mitigate | Same pattern — text-binding only. Description is `line-clamp-2` truncated CSS-style; full text in the FeatDetailModal also uses text-binding. |
| T-1-W4-04 | Information Disclosure | Wizard JSON state PATCHed to server contains user-typed text (e.g. notes) that surfaces to other character viewers | accept | Phase 1 wizard state contains only IDs (featId, optionKey, etc.) and structured choices — no user-typed text. Future steps that add notes need to reconsider. |
| T-1-W4-05 | DoS | Rapid-fire PATCHes from the wizard overload the server | mitigate | The patchLevelUp call is debounced 500ms client-side (Task 5 implementation). Server endpoint is light (single update). |
</threat_model>

<verification>
After Task 6 (before checkpoint):

```bash
# Client TS clean
cd client && npx tsc --noEmit -p tsconfig.app.json

# Client production build clean
cd client && npm run build

# Server still builds (no server changes in this plan -- sanity check only)
cd server && npm run build

# Full server test suite still green
cd server && npm test
```

All four commands must exit 0 before the human checkpoint.
</verification>

<success_criteria>
- 18 new client files exist in `client/src/features/characters/components/level-up/`
- 4 extended files (character-sheet-page, api, shared types, use-character-socket) extended additively
- All UI text in German; no emojis; only Lucide icons
- Touch targets ≥44px throughout
- Wizard chrome matches UI-SPEC §Component Contract — Wizard Chrome exactly (header, stepper, body, footer)
- Choice-Card primitive matches UI-SPEC §Component Contract — Choice-Card (states, source badges, prereq warning)
- Boost step uses h-11 w-11 +/- buttons + live "wird {newScore}" + cap-bei-18 chip
- Review step uses font-mono for stat numbers, two-column Vorher/Nachher cards
- Ändern revision contract implemented (revision-mode flag + Zurück-zur-Übersicht button — chain re-validation deferred to v2 per must_haves.gotchas)
- DRAFT-Resume banner implemented per UI-SPEC §Component Contract — DRAFT-Resume Banner
- Pathbuilder-Import-Violations banner implemented per UI-SPEC §Component Contract — Pathbuilder-Import-Violations Banner
- WebSocket level_up_committed callback wired to invalidate character query
- Motion respects prefers-reduced-motion
- TypeScript strict — no `: any`
- Client production build clean (`cd client && npm run build` exits 0)
- Server still builds and tests green (this plan adds no server changes)
- Human verification checkpoint approved
</success_criteria>

<output>
After completion, create `.planning/phases/01-level-up-pf2e-regelkonform/01-05-SUMMARY.md` documenting:
- Final file list (all 18 new client files + 4 extended)
- Confirmation that the wizard consumes the Plan 04-owned endpoints (GET open-draft, GET feats, GET class-feature-options) without any server-side edits in this plan
- Toast library used (if any introduced) or pattern chosen (browser alert is NOT acceptable; planner discretion: react-hot-toast, sonner, or hand-rolled context — record the decision)
- Any deviations from UI-SPEC noted with rationale
- Result of the human-verification checkpoint
</output>
