---
phase: 1
slug: level-up-pf2e-regelkonform
status: approved
shadcn_initialized: false
preset: none
created: 2026-04-27
revised: 2026-04-27
reviewed_at: 2026-04-27
---

# Phase 1 — UI Design Contract

> Visual and interaction contract for the Level-Up Wizard. Generated by gsd-ui-researcher,
> verified by gsd-ui-checker.
>
> Most decisions are inherited from CONTEXT.md, the project's existing design tokens
> (`client/src/index.css`), and the established modal/component patterns in
> `client/src/features/characters/components/`. Where CONTEXT.md locks a behavior,
> the relevant `D-XX` decision ID is referenced inline.

---

## Design System

| Property | Value |
|----------|-------|
| Tool | none (hand-built primitives) |
| Preset | not applicable |
| Component library | hand-built shadcn-style primitives in `client/src/shared/components/ui/` (`Button`, `Input`, `Card`, `Spinner`, `ActionIcon`); reused for the wizard. |
| Icon library | `lucide-react` 0.562.0 (`ChevronLeft`, `ChevronRight`, `X`, `AlertTriangle`, `Sparkles`, `Swords`, `Star`, `BookOpen`, `Shield`, `Heart`, `Eye`, `Footprints`, `Wand2`, `Check`, `Lock`, `Info`, `RotateCcw`, `Minus`, `Plus`, `Trash2`, `ArrowLeft`) — never emojis (CLAUDE.md). |
| Font | Inter (sans), JetBrains Mono (mono) — declared in `--font-sans`, `--font-mono` (`client/src/index.css:66-67`). |
| Theme | Tailwind v4 `@theme` block in `client/src/index.css`. All tokens already exist; UI-SPEC adds **no new tokens**. |
| Animation lib | Framer Motion 12.26.2 is installed but unused today. Phase 1 introduces it for the wizard step transition (see "Motion" section). |

---

## Spacing Scale

Declared values (all multiples of 4, all already present in Tailwind v4 default scale used by the codebase):

| Token | Value | Usage in Phase 1 |
|-------|-------|-------------------|
| xs | 4px (`p-1`, `gap-1`) | Icon gap inside choice-card chips, badge padding |
| sm | 8px (`p-2`, `gap-2`) | Stepper-dot gap, footer button gap, badge stacks |
| md | 16px (`p-4`, `gap-4`) | Wizard body padding (mobile), step-section padding, choice-card inner padding |
| lg | 24px (`p-6`, `gap-6`) | Wizard body padding (`sm:` desktop), Review-step section padding |
| xl | 32px (`p-8`) | Reserved for review-step major section breaks on desktop |
| 2xl | 48px | Not used in Phase 1 |
| 3xl | 64px | Not used in Phase 1 |

Touch-target minimums (mobile-first, CLAUDE.md):

- All interactive buttons: 44px height minimum — `h-11` (`Button` `default`), `h-11 w-11` (`Button` `icon`).
- Choice-cards: minimum 64px tap height (so an entire card is a single touch target including its title row + meta row — exceeds 44px).
- Boost-step `+/-` controls: `h-11 w-11` (44×44px) — see Boost-Step contract.
- Stepper-dot buttons: visible dot is `h-2 w-2` / `h-2.5 w-2.5` but the wrapping `<button>` is `h-11 w-11` with `-m-1` to keep the visual gap at 8–12px while the hit-zone is a genuine 44×44 — see Stepper subsection for the honest math.

**Exceptions (micro-utilities for inline chip / badge / icon-stack patterns only — NOT promoted to the layout grid):**

- Chip / badge padding: `px-1.5 py-0.5` (6×2px) — used inside source-tag badges, source-cap chips, level chips, delta chips. These are inline glyph-sized elements, never standalone surfaces.
- Inline icon offset: `mt-0.5` (2px) — used to optically align an icon's baseline with its sibling text on chip/banner heads (e.g. `RotateCcw` in DRAFT-resume banner, `AlertTriangle` in violations banner).
- Footer button gap: `gap-3` (12px) — used in the dialog footer row only when two buttons sit at the right edge with a clear visual separation; layout-grid gaps remain on the 4/8/16/24 ladder.

These micro-values stay as Tailwind utilities and never appear on layout-level surfaces (modals, cards, sections, body padding).

---

## Typography

The codebase has no custom font-size tokens — sizes come from Tailwind defaults. Phase 1 declares **4 sizes** (12 / 14 / 18 / 24) and **2 weights** to satisfy the 3-4-size, 2-weight contract.

| Role | Size | Tailwind class | Weight | Line Height | Usage |
|------|------|----------------|--------|-------------|-------|
| Auxiliary / Chrome | 12px | `text-xs` | 400 (`font-normal`); 600 (`font-semibold`) inside chips | 1.4 | Stepper labels (current/completed), meta-row chips on choice-cards (level, source, rarity, cap-erreicht, action-cost), banner sub-lines (`Zuletzt bearbeitet …`), card sub-lines (English-name under German-name), positive/negative delta chips in Review, footer help text in Boost-step. Two weights coexist within this size: chip *labels* (e.g. badge text) are 600; banner sub-lines and English-name fallbacks are 400. |
| Body | 14px | `text-sm` | 400 (`font-normal`) | 1.5 (`leading-normal`) | Choice-card descriptions, prereq text, banner body, dialog body, tooltip body, Spellcaster info-strip body, Skill-row text. |
| Label | 14px | `text-sm` | 600 (`font-semibold`) | 1.4 | Choice-card titles, step-section labels, button labels, banner headings, dialog headings (when ≤ `text-base`). |
| Heading | 18px | `text-lg` | 600 (`font-semibold`) | 1.3 | Wizard header title (`Stufenaufstieg — Stufe X`), step-screen heading (`Boost setzen`, `Klassentalent wählen`, …), modal H2. |
| Display | 24px | `text-2xl` | 600 (`font-semibold`) | 1.2 | Review-step "before/after" stat numbers (HP-Max, AC, DC, Wahrnehmung, Saves) in `font-mono`. |

Total: **4 sizes** (12 / 14 / 18 / 24) — within the 3-4-size limit. **2 weights**: 400 (normal) and 600 (semibold). No `font-medium` (500), no `font-bold` (700).

This matches the existing `add-condition-modal.tsx`, `rest-modal.tsx`, `add-feat-modal.tsx` patterns (`text-lg font-semibold` for headers, `text-sm` for body, `text-xs` for chips).

Mono font (JetBrains Mono) is reserved for stat numbers and dice notation in the Review step (`font-mono`).

---

## Color

The 60/30/10 split maps directly to existing tokens in `client/src/index.css`:

| Role | Value | Token | Usage |
|------|-------|-------|-------|
| Dominant (60%) | `#0f0f12` | `bg-bg-primary` | Page background behind the wizard backdrop |
| Secondary (30%) | `#1a1a1f` / `#242429` | `bg-bg-secondary` (modal panel), `bg-bg-tertiary` (choice-cards, info-strips, stepper rail) | Wizard surface, choice-card surfaces, step-section containers, stepper inactive state |
| Accent (10%) | `#c26dbc` | `primary-500` (`text-primary-500`, `bg-primary-500`, `border-primary-500`) | **EXACTLY** these elements only (see reserved-for list below) |
| Destructive | `#ef4444` | `error-500` (`text-error-500`, `bg-error-500`) | "Verwerfen" (DRAFT discard) confirm-dialog button only — **not** generic cancel surfaces. |

Accent reserved-for list (the only places `primary-500` may be applied in this phase):

1. **Primary CTA buttons in the wizard footer**: `Weiter`, `Bestätigen`, `Zurück zur Übersicht` (after `Ändern` in Review) — `Button variant="default"` (which is `bg-primary-500`).
2. **Active stepper dot**: the dot for the current step uses `bg-primary-500`; completed dots use `bg-primary-500/40`; future dots use `bg-bg-tertiary`.
3. **Selected choice-card border + checkmark**: the chosen card (boost target, talent, skill, doctrine, …) gets `border-primary-500 ring-1 ring-primary-500/40`, and a `Check` icon in `text-primary-500` appears in the top-right corner.
4. **DRAFT-resume banner accent stripe and the "Fortsetzen" button**: the resume banner has a left-border `border-l-4 border-primary-500`, body text `text-text-primary`, and the "Fortsetzen" CTA is `Button variant="default"`.
5. **"Stufe steigen"-Button on the character-sheet header** (when enabled — i.e. character is below cap and no foreign DRAFT exists): `Button variant="default"` with `Sparkles` icon.
6. **`Ändern` link in Review-step Section A**: `text-xs text-primary-500 hover:underline` — the only inline-link use of accent in the wizard.

`primary-500` is **never** used for: choice-card backgrounds (those stay `bg-bg-tertiary`), step-headings (those use `text-text-primary`), tab/section dividers (`border-border`), tooltip backgrounds, banner backgrounds.

Semantic color usage (already established by existing modals — `rest-modal.tsx` is the canonical reference):

| Semantic | Token | Reserved For |
|----------|-------|--------------|
| Warning (`#f59e0b`, `warning-500` / `yellow-400`) | `text-warning-500` `bg-warning-500/10` `border-warning-500/20` | (a) `AlertTriangle` icon at the top-right of any **choice-card whose prereq is non-evaluable** (D-03). (b) Pathbuilder-import-violation banner background (D-06). (c) "Cap erreicht" indicator on a skill that cannot be increased further. (d) "+1 (Cap bei 18)" chip in Boost-step. |
| Error (`#ef4444`, `error-500` / `red-400`) | `text-error-500` `bg-error-500/10` `border-error-500/20` | (a) "Verwerfen" button in the DRAFT-resume banner and the destructive-confirm dialog. (b) Inline form-validation errors (e.g. boost target invalid). |
| Success (`#22c55e`, `success-500` / `green-400`) | `text-success-500` | Checkmarks in completed stepper dots; "+X HP", "+1 Save" deltas in the Review step (positive change). |
| Info (`#3b82f6`, `info-500` / `blue-400`) | `text-info-500` `bg-info-500/10` | Read-only info strips in the wizard (e.g. "Free Archetype: aktiv", "Spellcaster-Slot +1 (automatisch)"). |

### Inherited Exceptions — Source-tag chroma palette

The 6 raw-hue source badges below are an **inherited convention** from `feat-detail-modal.tsx:22-29` (existing `featSourceColors` map) and are intentionally exempt from the single-accent rule. They are used **only for talent-source identification badges** in the meta row of choice-cards — never for layout, surfaces, banner backgrounds, CTAs, or interactive accents. The selection accent (`primary-500`) remains the only interactive accent across the wizard.

| Source | Token classes |
|--------|---------------|
| Klassentalent | `bg-red-500/20 text-red-400` |
| Abstammungstalent | `bg-blue-500/20 text-blue-400` |
| Allgemeines Talent | `bg-yellow-500/20 text-yellow-400` |
| Fertigkeitstalent | `bg-green-500/20 text-green-400` |
| Archetypentalent | `bg-purple-500/20 text-purple-400` |
| Bonustalent | `bg-cyan-500/20 text-cyan-400` |

---

## Copywriting Contract

All copy is **German** (CLAUDE.md / PROJECT.md). German imperative tone, du-Form, no emojis, no exclamation marks except in genuine warnings.

### Primary CTAs

| Element | Copy |
|---------|------|
| Character-sheet button to start the wizard | `Stufe steigen` (icon: `Sparkles`) |
| Wizard footer — go to next step | `Weiter` (icon: `ChevronRight` on right) |
| Wizard footer — go to previous step | `Zurück` (icon: `ChevronLeft` on left, `Button variant="outline"`) |
| Wizard footer — final commit on Review step | `Bestätigen` (replaces `Weiter`; icon: `Check` on right) |
| Wizard footer — return-to-Review after `Ändern` revision | `Zurück zur Übersicht` (icon: `ArrowLeft` on left, `Button variant="default"`, replaces `Weiter` while user is revising a step entered via `Ändern` from Review — see Review-step contract for re-validation behavior) |
| Choice-card click result label | `Wählen` (only used inside the prereq-confirm dialog as "Trotzdem wählen") |
| Stepper progress label (always visible — mobile and desktop) | `Schritt {n} von {m} — {currentStepLabel}` (e.g. `Schritt 3 von 7 — Klassentalent`) |
| Confirm-dialog secondary action (cancel within a destructive-confirm dialog only) | `Abbrechen` (`Button variant="ghost"`) — used **only inside** the destructive/prereq confirm dialogs. The wizard footer itself has no `Abbrechen` button; mid-flow exit happens via the header `X` close button or backdrop click. |

### Wizard-step screen headings (one per step, `text-lg font-semibold`)

| Step | Heading | Sub-line (`text-sm text-text-secondary`) |
|------|---------|--------------------------------------------|
| 0 — Klassenmerkmale (auto) | `Klassenmerkmale auf Stufe {N}` | `Diese Merkmale werden automatisch übernommen.` |
| 0a — Klassenmerkmal-Wahl (`choiceType`) | `{Merkmalname} wählen` (z.B. `Lehre wählen`, `Schule wählen`, `Waffenmeisterschaft wählen`) | `Wähle eine Option — diese Wahl ist endgültig nach Bestätigung.` |
| 1 — Boost-Set | `Attributs-Boosts setzen` | `Wähle vier verschiedene Attribute. Werte über 18 erhalten +1, sonst +2.` |
| 2 — Skill-Increase | `Fertigkeits-Erhöhung` | `Wähle eine Fertigkeit, um sie um eine Stufe zu erhöhen.` |
| 3 — Klassentalent | `Klassentalent wählen` | `Talente, deren Voraussetzungen du erfüllst.` |
| 4 — Fertigkeitstalent | `Fertigkeitstalent wählen` | `Talente, deren Voraussetzungen du erfüllst.` |
| 5 — Allgemein-Talent | `Allgemeines Talent wählen` | `Allgemein- oder Fertigkeitstalente sind erlaubt.` |
| 6 — Ancestry-Talent | `Abstammungstalent wählen` | `Talente deiner Abstammung und deines Erbes.` |
| 7 — Free-Archetype-Slot | `Archetypentalent (Free Archetype)` | (vor Dedication) `Wähle eine Dedication.` / (nach Dedication) `Talente jedes Archetyps sind erlaubt (Pathbuilder-Verhalten).` |
| 8 — Spellcaster | `Zauberprogression` | (spontan) `Repertoire um {N} Zauber erweitern.` / (vorbereitet) `Slot-Erhöhung wird automatisch angewendet.` |
| 9 — Review | `Übersicht & Bestätigen` | `Vergleiche Vorher/Nachher. Erst nach „Bestätigen" wird der Charakter geändert.` |

### Empty / Loading / Error states

| State | Copy |
|-------|------|
| Loading talent list (Spinner + label) | `Talente werden geladen …` |
| Loading any list (boost options, skills, formulas) | `Wird geladen …` |
| Empty: no talents meet prerequisites at this slot | Heading: `Keine erfüllbaren Talente gefunden` — Body: `Alle Talente dieser Kategorie haben Voraussetzungen, die du derzeit nicht erfüllst. Du kannst trotzdem ein Talent mit Warnung wählen — aktiviere "Auch nicht erfüllbare anzeigen".` (toggle button text: `Auch nicht erfüllbare anzeigen` / `Nur erfüllbare anzeigen`). |
| Empty: no skills can be increased (cap at all) | Heading: `Keine Fertigkeit erhöhbar` — Body: `Alle Fertigkeiten haben das für deine Stufe erlaubte Maximum erreicht. Diesen Schritt überspringen?` (button: `Schritt überspringen`). |
| Empty: no spells available for repertoire (impossible at L2+, but defensive) | `Keine Zauber verfügbar.` |
| Error: API failure during step (e.g. talent search) | Heading: `Talente konnten nicht geladen werden` — Body: `Versuche es erneut oder wähle ein anderes Filter.` — Action: `Erneut versuchen` (`Button variant="outline"`). |
| Error: commit transaction failed (rollback) | Heading: `Stufenaufstieg konnte nicht gespeichert werden` — Body: `Deine Wahlen sind sicher als Entwurf gespeichert. Bitte versuche es erneut. Wenn der Fehler bleibt, lade die Seite neu.` — Action: `Erneut versuchen` (`Button variant="default"`) + `Schließen` (`Button variant="ghost"`). DRAFT bleibt erhalten. |
| Error: prereq evaluator returns "non-evaluable" | (Inline am Talent — kein eigener State; siehe Prereq-Confirm-Dialog) |
| Review revalidation after `Ändern` cleared a later choice | Inline note in the affected Review row: `Diese Wahl wurde durch eine frühere Änderung zurückgesetzt — bitte erneut treffen.` (`text-xs text-warning-500`) — the row's `Ändern` link is replaced by `Wählen` (`text-xs text-primary-500`) which jumps directly to that step. |

### Destructive confirmations

| Action | Trigger | Confirm-Dialog |
|--------|---------|----------------|
| **DRAFT verwerfen** | Click `Verwerfen` in DRAFT-resume banner (D-14) | Heading: `Entwurf verwerfen?` — Body: `Deine bisherigen Wahlen für Stufe {N} werden gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.` — Buttons: `Abbrechen` (ghost) + `Verwerfen` (`destructive`, `Trash2` icon). |
| **Wizard mid-flow abbrechen** mit ungesicherten Änderungen | Click X (close) in header or backdrop click after at least one choice was made | 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). **Note:** discard is NOT offered here — only commit-or-save. Discard happens only via the resume-banner "Verwerfen". |
| **Talent mit nicht erfüllter Voraussetzung wählen** (D-03) | Click `Wählen` on a choice-card that carries the yellow `AlertTriangle` (non-evaluable prereq) | Heading: `Voraussetzung nicht prüfbar` — Body: `Voraussetzung: „{prereqText}". Dimension47 kann diese Bedingung nicht automatisch prüfen. Erfüllst du sie?` — Buttons: `Abbrechen` (ghost) + `Trotzdem wählen` (`Button variant="default"`, no destructive coloring — this is informed user choice, not a destructive op). |
| **Bestätigen (Final Commit)** in Review step | Click `Bestätigen` | No additional dialog — the Review step IS the confirmation surface. The button is `Button variant="default"` with `isLoading` spinner during commit. After success: wizard closes with a 2-second toast `Stufenaufstieg bestätigt — Stufe {N}.` (info-tone, `bg-success-500/10`). |

### Banner copy (D-06: Pathbuilder-Import-Violations)

Position: directly below the character-sheet header, above the tab navigation. Background `bg-warning-500/10`, border `border-l-4 border-warning-500`, padding `p-4`.

- Heading (`text-sm font-semibold text-warning-500`): `{N} Talente mit nicht erfüllter Voraussetzung`
- Body (`text-sm text-text-secondary`): `Beim Import wurde festgestellt, dass folgende Talente Voraussetzungen haben, die der Charakter nicht erfüllt. Liste prüfen und ggf. nachträglich anpassen.`
- Action: `Liste anzeigen` (`Button variant="ghost"` size sm, expand-collapse — when expanded, shows a `<ul>` of `{TalentName} — Voraussetzung: {prereqText}` items).
- Dismiss: not dismissible (the banner stays until the violations are resolved by GM/Player editing — Phase 1 ships read-only listing only; resolution UI is out of scope, deferred to v2 retrain).

### Banner copy (D-14: DRAFT-Resume-Banner)

Position: at the top of the character-sheet page (above the avatar header) AND inside the wizard if user clicks "Stufe steigen" while a DRAFT exists. Background `bg-bg-tertiary`, border `border-l-4 border-primary-500`, padding `p-4`.

- Heading (`text-sm font-semibold text-text-primary`): `Du hast eine offene Stufenaufstiegs-Session — Stufe {N}.`
- Body (`text-sm text-text-secondary`): `Zuletzt bearbeitet: {relativeDate}.` (z.B. `vor 3 Tagen`).
- Actions: `Fortsetzen` (`Button variant="default"`, icon `RotateCcw`) + `Verwerfen` (`Button variant="ghost"`, `text-error-500`, icon `Trash2`).

### "Stufe steigen"-Button on character-sheet header

Position: in the right-side header button cluster of `character-sheet-page.tsx` (lines 1607-1626 — alongside Download, Edit, Delete). Order: **first** in the cluster (left of Download).

| Character state | Button rendering |
|-----------------|------------------|
| Eligible, no DRAFT | `Button variant="default"` size `sm` — label `Stufe steigen`, icon `Sparkles`, fully enabled. |
| Eligible, DRAFT exists | `Button variant="default"` size `sm` — label `Stufe fortsetzen`, icon `RotateCcw`, fully enabled. (Consistent with D-14 banner; both routes open the same wizard, the banner is just an upper-page reminder.) |
| Character at level cap (level 20) | Hidden entirely. (No disabled state to avoid confusion — the button just isn't there.) |
| User is neither owner nor GM | Hidden entirely. |

---

## Component Contract — Wizard Chrome

### Container

The wizard reuses the project's modal pattern (canonical: `add-feat-modal.tsx:246-260`):

```
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
  <div className="absolute inset-0 bg-black/60" onClick={handleBackdropClick} />
  <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 */}
    {/* Stepper */}
    {/* Body (current step) */}
    {/* Footer */}
  </div>
</div>
```

- Mobile: full-width bottom-sheet (`items-end`, `rounded-t-2xl`).
- Desktop (`sm:` ≥ 640px): centered modal, max-width `2xl` (672px), `rounded-2xl`.
- Backdrop click: opens the "Wizard mid-flow abbrechen" confirm if any choice has been made; closes silently if no choice yet.

### Header (always visible)

- Container: `flex items-center justify-between p-4 border-b border-border`.
- Left: `<h2 className="text-lg font-semibold text-text-primary">Stufenaufstieg — Stufe {N}</h2>` + sub-line `<p className="text-xs text-text-secondary">{characterName}</p>`.
- Right: close button `<Button variant="ghost" size="icon" aria-label="Schließen"><X className="h-5 w-5" /></Button>` triggering the same backdrop-confirm logic as the backdrop click. **This is the only cancel surface in the wizard chrome** (the footer carries no Abbrechen button).

### Stepper (always visible, between Header and Body)

Mobile-first compact dot-stepper with **always-visible progress label**:

- Outer container: `<nav role="navigation" aria-label="Wizard-Schritte" className="px-4 py-3 border-b border-border">`.
- **Progress label (always visible, mobile and desktop)**: rendered as the first child of the stepper, above the dot row:
  ```
  <div className="text-xs text-text-secondary pb-2">
    Schritt {currentIdx+1} von {totalSteps} — {currentStepLabel}
  </div>
  ```
  This guarantees that on mobile (where dot-row labels are hidden) the user always knows which step they are on. With up to 11 conditional steps this label is essential, not optional.
- Dot row: `<ol className="flex items-center gap-2 overflow-x-auto">`.
- Each dot is wrapped in a real button so the touch target is genuine 44×44:
  ```
  <button
    type="button"
    onClick={() => goToStep(idx)}
    disabled={idx > currentIdx}
    aria-label={`${stepLabel}${idx === currentIdx ? ', aktuell' : idx < currentIdx ? ', abgeschlossen' : ', noch nicht verfügbar'}`}
    aria-current={idx === currentIdx ? 'step' : undefined}
    className="h-11 w-11 -m-1 flex items-center justify-center disabled:cursor-not-allowed"
  >
    {/* visible dot */}
    <span className={cn('rounded-full transition-colors duration-150', dotState)} />
  </button>
  ```
  - The `-m-1` negative margin on the wrapper compensates for the 44px hit-zone so the perceived gap between dots stays at 8–12px while the tappable area is 44×44 — honest math, not a 6px-padding-claims-44px trick.
- Visible-dot states (rendered inside the wrapping button):
  - **Future**: `h-2 w-2 rounded-full bg-bg-tertiary`.
  - **Current**: `h-2.5 w-2.5 rounded-full bg-primary-500`.
  - **Completed**: `h-2 w-2 rounded-full bg-primary-500/40`.
- Step labels next to dots (visible **`sm:` and up only** — desktop bonus): each completed/current dot shows its step label `text-xs font-semibold text-text-primary` to its right; completed dots additionally render a `Check` icon at `h-3 w-3 text-success-500` overlaid on the dot. Mobile users rely on the always-visible progress label above the row instead.
- Step-label vocabulary (one of, depending on which steps apply this level): `Merkmale`, `Wahl`, `Boost`, `Skill`, `Klasse`, `Fertigkeit`, `Allgemein`, `Abstammung`, `Archetyp`, `Zauber`, `Übersicht`. Conditional steps not shown.
- Connector between dots: `h-px w-4 bg-bg-tertiary` (`bg-primary-500/40` between completed pairs). The connector sits inside its own non-interactive `<span>` between the dot buttons.
- Future steps: dot button is `disabled` — no jumping forward to incomplete future steps. Completed and current steps are interactive (back-navigation only).

### Body (per-step content area)

- Container: `flex-1 overflow-y-auto p-4 sm:p-6`.
- Top of body shows the step heading + sub-line (see Copywriting table above).
- Below: the step's content (Choice-Cards, Boost-Picker, Skill-Picker, Review-Diff, …).
- Mobile: one wahlpunkt per scroll-screen — choice-cards stack vertically, full-width.
- Desktop: choice-cards may form a 2-column grid (`grid grid-cols-1 sm:grid-cols-2 gap-3`) for talent picks; boost picker stays single-column for clarity.

### Footer (always visible)

- Container: `flex items-center justify-between gap-3 p-4 border-t border-border bg-bg-secondary`.
- **Left side** (contextual hint, optional): a `<span className="text-xs text-text-secondary">` may render `Schritt {n}/{m}` as a redundant footer hint, or be empty. The footer carries **no Abbrechen button** — mid-flow exit happens exclusively via the header `X` or backdrop click.
- **Right cluster** (`flex items-center gap-2`):
  - `<Button variant="outline" onClick={prev} disabled={isFirstStep}><ChevronLeft className="h-4 w-4" /> Zurück</Button>`
  - On non-final steps in the normal forward flow: `<Button variant="default" onClick={next} disabled={!stepValid}>Weiter <ChevronRight className="h-4 w-4" /></Button>`
  - On the Review step: `<Button variant="default" onClick={commit} isLoading={isCommitting}><Check className="h-4 w-4" /> Bestätigen</Button>`
  - When the user is on a step they re-entered via Review's `Ändern` link, the primary right button changes to `<Button variant="default" onClick={returnToReview}><ArrowLeft className="h-4 w-4" /> Zurück zur Übersicht</Button>` (replaces `Weiter` until the user returns to Review). See Review-step contract for the re-validation contract.
- Footer is sticky at the bottom of the modal (the body scrolls, the footer does not).

---

## Component Contract — Choice-Card

The choice-card is the primary interactive element across most steps (talent picks, boost-target picks, skill-increase picks, doctrine/school picks). Single canonical layout, used everywhere.

### Layout (per card)

Container:
```
<button
  onClick={() => onSelect(option)}
  disabled={isLocked}
  className={cn(
    'w-full text-left p-4 rounded-xl border transition-colors',
    'min-h-[64px]',                                     // touch-target floor
    !isSelected && !isLocked && 'bg-bg-tertiary border-border hover:border-border-hover',
    isSelected && 'bg-bg-tertiary border-primary-500 ring-1 ring-primary-500/40',
    isLocked && 'bg-bg-tertiary/60 border-border opacity-60 cursor-not-allowed',
  )}
>
```

Inner structure (top row → meta row → optional description):

1. **Top row** (`flex items-start justify-between gap-2`):
   - Left: `<h4 className="text-sm font-semibold text-text-primary">{germanName ?? englishName}</h4>` + sub-line `text-xs text-text-muted` showing the English name if a German name exists.
   - Right (icons stack): in this order, only if applicable:
     - `Lock` icon (`h-4 w-4 text-text-muted`) if locked (already chosen / cap reached / not eligible at this slot).
     - `AlertTriangle` icon (`h-4 w-4 text-warning-500`) if prereq is **non-evaluable** (D-03) — wrapped in a tooltip showing the raw prereq string.
     - `Check` icon (`h-4 w-4 text-primary-500`) if currently selected.

2. **Meta row** (`mt-2 flex flex-wrap gap-2 items-center text-xs`):
   - Source badge (existing color map — Klassen-/Abstammungs-/etc., see Color section).
   - Level chip: `text-text-secondary` `Stufe {n}+`.
   - Action-cost badge if applicable (uses existing `ActionIcon` component from `client/src/shared/components/ui/action-icon.tsx`).
   - Rarity chip if not `Common` (existing convention from `add-feat-modal.tsx:294-302`).
   - **Cap-erreicht** chip on Skill-Increase cards: `bg-warning-500/10 text-warning-500 px-1.5 py-0.5 rounded` text `Cap erreicht`.

3. **Description (truncated)**: `mt-2 text-sm text-text-secondary line-clamp-2`. A `Mehr` link beneath the truncation opens the existing `FeatDetailModal` (or a similar detail modal for non-talent options) **as a layered modal** at `z-60`.

### Locked / unavailable states

- **Already chosen this Level-Up** (e.g. cannot pick the same skill twice): `disabled` + `Lock` icon + opacity 60%. No click effect.
- **Prereq fails (evaluable)**: hidden by default. A toggle in the step body (`Auch nicht erfüllbare anzeigen`) shows them grayed (`opacity-60`) with a `bg-error-500/10 text-error-500 text-xs` chip `Voraussetzung nicht erfüllt: {parsed reason}`. Click still enabled but routes through the prereq-confirm dialog.
- **Prereq non-evaluable (D-03)**: shown by default (it's only a warning, not a block). Yellow `AlertTriangle` icon. Click triggers the prereq-confirm dialog with the raw `prerequisites` string.
- **Cap reached** (skill-increase): `disabled` + warning chip `Cap erreicht` + tooltip `Diese Fertigkeit ist auf deiner Stufe nicht weiter erhöhbar.`

### Selected state

- Border `border-primary-500` + halo `ring-1 ring-primary-500/40`.
- Top-right `Check` icon `text-primary-500`.
- Background remains `bg-bg-tertiary` — the accent is **only** the border + checkmark, not the surface fill (60/30/10 discipline).

---

## Component Contract — Boost-Set Step

Special layout: 6 attribute rows (STR, DEX, CON, INT, WIS, CHA) with a count of how many of the 4 boosts are spent on each. Mobile: vertical stack of 6 attribute rows. Desktop: 2-column grid.

**The row container is a `<div>`, not a `<button>`.** Tapping the row body itself has no behavior. Only the inner `+/-` controls are interactive — they are real `<button>` elements with proper `aria-label`s. This avoids the invalid HTML pattern of nesting `<button>` inside `<button>` and keeps a11y/event propagation clean. The row's hover state (`hover:border-border-hover`) is purely a decorative styling cue, not a clickable affordance.

Per attribute row:
```tsx
<li className="w-full p-4 rounded-xl border border-border bg-bg-tertiary
               flex items-center justify-between min-h-[64px]
               hover:border-border-hover">
  <div className="flex-1 min-w-0">
    <h4 className="text-sm font-semibold text-text-primary">
      {abbreviation} — {germanName}
    </h4>
    <p className="text-xs text-text-secondary">
      Aktuell {currentScore}{capReached ? ' (Cap)' : ''} → wird {newScore}
    </p>
    {currentScore >= 18 && count >= 1 && (
      <span className="inline-block mt-1 text-xs text-warning-500
                       bg-warning-500/10 px-1.5 py-0.5 rounded">
        +1 (Cap bei 18)
      </span>
    )}
  </div>
  <div className="flex items-center gap-2 flex-shrink-0">
    <button
      type="button"
      onClick={dec}
      disabled={count === 0}
      aria-label={`${abbreviation} Boost verringern`}
      className="h-11 w-11 rounded-lg bg-bg-elevated flex items-center justify-center
                 disabled:opacity-40 disabled:cursor-not-allowed
                 hover:bg-bg-elevated/80 transition-colors"
    >
      <Minus className="h-5 w-5" />
    </button>
    <span className="text-lg font-semibold text-text-primary tabular-nums w-8 text-center">
      {count}
    </span>
    <button
      type="button"
      onClick={inc}
      disabled={!canIncrement}
      aria-label={`${abbreviation} Boost erhöhen`}
      className="h-11 w-11 rounded-lg bg-bg-elevated flex items-center justify-center
                 disabled:opacity-40 disabled:cursor-not-allowed
                 hover:bg-bg-elevated/80 transition-colors"
    >
      <Plus className="h-5 w-5" />
    </button>
  </div>
</li>
```

- The `+/-` controls are `h-11 w-11` (44×44px) — meeting the 44px touch-target floor declared in the Spacing Scale. Lucide `Minus` / `Plus` icons render at `h-5 w-5` to keep visual weight balanced inside the larger button.
- **Responsive note for narrow viewports (`<360px`)**: the row layout falls back from `flex-row` to `flex-col` so the `+/-` cluster sits below the attribute label rather than to its right. Implementation: wrap the row in a container with `flex-col @[360px]:flex-row @[360px]:items-center @[360px]:justify-between` (Tailwind container queries) — or, if container queries are unavailable, accept the cramped layout and ensure the row has `flex-wrap gap-3` so the cluster wraps under the label rather than overlapping it.
- "wird {newScore}" reflects the +2 / +1 cap-bei-18 rule live (not the full character recompute — that's review-only per D-12, but this single-attribute preview stays correct because the math is local).
- **Cap visualisation**: when `currentScore >= 18` AND `count >= 1`, the inline chip `+1 (Cap bei 18)` renders below the score line in `text-warning-500`. Otherwise the row implies +2.
- **Footer-helper** below the 6 rows: `<p className="text-xs text-text-secondary mt-4">{boostsSpent} von 4 Boosts gewählt. Du musst genau 4 verschiedene Attribute boosten.</p>` — when `boostsSpent === 4`, the `Weiter` button enables.
- A boost cannot be doubled-up: max `count` per attribute is 1 (PF2e rule on level-up boosts). The +/- controls enforce this.

---

## Component Contract — Skill-Increase Step

A scrollable list of 16+ skills (or all the character's trained-or-higher skills, plus untrained ones to allow new training).

Per skill row (compact):
```
<button className="w-full p-3 rounded-lg border border-border bg-bg-tertiary
                   flex items-center justify-between min-h-[56px]
                   hover:border-border-hover disabled:opacity-60">
  <div className="flex items-center gap-3">
    <span className="text-sm font-semibold text-text-primary">{germanSkillName}</span>
    <span className="text-xs text-text-secondary">{abilityAbbreviation}</span>
  </div>
  <div className="flex items-center gap-2">
    <span className="text-xs text-text-secondary">{germanCurrentProf}</span>
    <ChevronRight className="h-3 w-3 text-text-muted" />
    <span className={cn('text-xs font-semibold', proficiencyColors[newProf])}>{germanNewProf}</span>
  </div>
</button>
```

- Reuses the existing proficiency color map (`feat-detail-modal.tsx` → `PROFICIENCY_COLORS` in `character-sheet-page.tsx`): `TRAINED text-blue-500`, `EXPERT text-purple-500`, `MASTER text-orange-500`, `LEGENDARY text-red-500`.
- **Cap-erreicht skills** (e.g. trying to push to MASTER before L7) are rendered with `disabled` + warning chip `Cap erreicht`.
- **Already trained skills (UNTRAINED case is rare, only for first-time training)** are shown normally.
- Mobile: one column. Desktop: still one column for clarity (16 rows × 56px ≈ 900px which fits in the modal scroll body).

---

## Component Contract — Free-Archetype-Slot Step (D-08)

Top of the step body shows the read-only toggle status:

- If FA is **enabled** for this character: info strip `bg-info-500/10 border border-info-500/20 text-info-500 p-3 rounded-lg flex items-center gap-2` with `<Sparkles className="h-4 w-4" />` icon and copy `Free Archetype: aktiv` + sub-line `text-xs text-text-secondary` `Du erhältst zusätzlich einen Archetypen-Talent-Slot.`
- If FA is **disabled**: the entire step is skipped (not rendered in the stepper).
- Below the strip: choice-cards filtered by archetype rules:
  - Vor erster Dedication: cards filtered to `featType === 'Archetype'` AND trait includes `Dedication`.
  - Nach erster Dedication (D-07 — Pathbuilder behavior): all `featType === 'Archetype'` cards (any archetype).
- A small text below the filter-toggle row reads either `Wähle eine Dedication.` or `Talente jedes Archetyps sind erlaubt (Pathbuilder-Verhalten).` (matches sub-line copy table above).

---

## Component Contract — Spellcaster Step

Top of the step body shows a read-only info strip describing what's automatic:

- Vorbereiteter Caster: `<info strip> Slot-Erhöhung: +{N} Slot auf Grad {M} (automatisch)` — and the body has **no choice-cards** beyond this strip; the user can press `Weiter` immediately.
- Spontaner Caster (Bard, Sorcerer, Oracle, Witch with Patron-Variant, Summoner spontaneously cast — D-18): info strip + a **repertoire-pick** sub-section. The repertoire-pick is a list of choice-cards (one per spell), filtered by tradition and by the user's known spell-levels. Cap message: `Wähle {N} Zauber für dein Repertoire.` `{spellsPicked} von {N} gewählt.` `Weiter` enables when `spellsPicked === N`.

Spell choice-cards reuse the canonical Choice-Card layout, with these adaptations:
- Top row title: `{germanSpellName}` + sub-line englishName.
- Meta row: tradition chip (`bg-primary-500/10 text-primary-400 px-1.5 py-0.5 rounded text-xs`), spell-level chip `Grad {n}`, traits.

---

## Component Contract — Choice-Klassenmerkmal Sub-Step (D-19)

Triggered when the `ClassProgression` row for `(class, targetLevel)` carries `choiceType` (e.g. Cleric Doctrine, Wizard School, Fighter Weapon Mastery, Champion Cause, Sorcerer Bloodline if not at L1, …).

Layout: a vertical stack of mutually-exclusive choice-cards (canonical Choice-Card layout), single-column on mobile and desktop. Selecting one card auto-deselects the previous (radio-group semantics, but using the same visual Choice-Card).

- Header sub-line is dynamic based on the choice key (see Copywriting table — `Lehre wählen`, `Schule wählen`, …).
- The `choiceOptionsRef` from `ClassProgression` resolves to a list of options. Each option's description is rendered in the truncated description area; `Mehr` opens a layered detail-modal at `z-60`.
- After picking, the meta row shows the badge `Klassenmerkmal-Wahl` (`bg-secondary-500/20 text-secondary-300 text-xs px-1.5 py-0.5 rounded`).

---

## Component Contract — Review Step (D-12)

Two-section layout, mobile-first vertically stacked, desktop two-column where helpful.

### Section A — Wahlen-Zusammenfassung

A `Card`-wrapped list of every choice the user made, grouped by step. Each row:

```
<div className="flex items-start justify-between gap-3 py-2 border-b border-border last:border-0">
  <div className="flex items-center gap-2">
    {stepIcon}     {/* e.g. Star for talent, Sparkles for skill, Wand2 for repertoire */}
    <span className="text-xs text-text-secondary">{stepLabel}</span>
    <span className="text-sm font-semibold text-text-primary">{choiceLabel}</span>
  </div>
  <button
    onClick={() => goToStep(step, { revisionMode: true })}
    className="text-xs text-primary-500 hover:underline"
  >
    Ändern
  </button>
</div>
```

#### `Ändern` revision contract

When the user clicks `Ändern` on a Review row:

1. Wizard navigates to that step (back-navigation).
2. The wizard footer's primary right button changes from `Weiter` to `<Button variant="default"><ArrowLeft className="h-4 w-4" /> Zurück zur Übersicht</Button>` (icon `ArrowLeft`). `Zurück` (left button) still works as normal back-nav, as does direct stepper-dot tapping.
3. The user revises their choice on that step.
4. When the user clicks `Zurück zur Übersicht`, the wizard runs **chain re-validation**:
   - Walk forward through every step that comes after the revised one.
   - For each later step, check whether the previously-saved choice is still valid given the new earlier choice (e.g. a Skill-Increase that depended on a now-removed boost; a Talent whose evaluable prereq now fails because of a swapped earlier feat).
   - Any step whose saved choice is now invalid has its choice **cleared** (set back to "not yet picked").
5. Wizard returns to the Review step.
6. Cleared choices appear in Review Section A with the inline note `Diese Wahl wurde durch eine frühere Änderung zurückgesetzt — bitte erneut treffen.` (`text-xs text-warning-500`), and the `Ändern` link on that row is replaced by a `Wählen` link (`text-xs text-primary-500`) that jumps to the cleared step.
7. The `Bestätigen` button is disabled until every Review row has a valid choice.

This contract avoids the "click Weiter through 6 steps again" trap: users always get a single explicit return path (`Zurück zur Übersicht`) and only re-enter steps that the chain actually invalidated.

Direct stepper-dot taps continue to work as conventional back-nav (no re-validation triggered) so users can browse without committing to a revision.

### Section B — Stat-Diff (Vorher / Nachher)

Two-column "Vorher / Nachher" layout. Mobile: stacked (Vorher card on top, Nachher card below). Desktop: side-by-side `grid grid-cols-1 sm:grid-cols-2 gap-4`.

Each card uses `Card` + `CardHeader` + `CardContent`:

- Card title (`CardTitle`): `Vorher (Stufe {N-1})` / `Nachher (Stufe {N})` — for "Nachher", a `Sparkles` icon `text-primary-500` is rendered next to the title.
- Card content: a 5-row `<dl>`:

| Stat | Vorher | Nachher |
|------|--------|---------|
| HP-Max | `{old}` | `{new}` |
| RK | `{old}` | `{new}` |
| Klassen-DC | `{old}` | `{new}` |
| Wahrnehmung | `{old}` | `{new}` |
| Rettungswürfe | `Fort {old} / Ref {old} / Wille {old}` | `Fort {new} / Ref {new} / Wille {new}` |

Numbers in the Nachher card use `font-mono text-2xl font-semibold text-text-primary`. Deltas are rendered inline as small chips:
- Positive change: `text-xs px-1.5 py-0.5 rounded bg-success-500/10 text-success-500` (`+{delta}`).
- No change: omitted (no chip).
- Negative change (rare — only if a class transition causes it; defensive): `bg-error-500/10 text-error-500` (`{delta}`).

### Section C — Spellcaster-Diff (only if applicable)

Shown below B when the character is a caster: a small table of spell-slot increments and (for spontaneous) repertoire deltas.

```
<dl className="grid grid-cols-2 gap-2 text-sm">
  <dt className="text-text-secondary">Slots Grad 3</dt><dd className="text-text-primary">3 → 4 (<span className="text-success-500">+1</span>)</dd>
  <dt className="text-text-secondary">Repertoire</dt><dd className="text-text-primary">+{N} Zauber gewählt</dd>
</dl>
```

---

## Component Contract — Prereq-Confirm Dialog (D-03)

A **secondary modal layered over the wizard** at `z-60` (wizard sits at `z-50`, dialog at `z-60` — both share the same fixed-positioned root, dialog gets its own backdrop `bg-black/40`).

- Container: `relative w-full sm:max-w-md p-6 bg-bg-secondary rounded-2xl border border-warning-500/30`.
- Header: `<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>`
- Body: `<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">„{prereqText}"</p> <p className="text-sm text-text-secondary">Dimension47 kann diese Bedingung nicht automatisch prüfen. Erfüllst du sie?</p>`
- Footer: `<div className="flex gap-3 mt-4 justify-end">` with `Abbrechen` (`Button variant="ghost"`) + `Trotzdem wählen` (`Button variant="default"`).

The same dialog shape is reused for "Wizard mid-flow abbrechen", "DRAFT verwerfen" — each with their own copy + heading + appropriate button variant. Note: the `Abbrechen` ghost button **only** appears inside these confirm dialogs. The wizard chrome itself never shows a top-level Abbrechen button.

---

## Component Contract — DRAFT-Resume Banner (D-14)

Two surfaces share this banner:

1. **Character-sheet page** (when a DRAFT exists for this character) — at the very top of the page, above the avatar header. Persistent until DRAFT is committed or discarded.
2. **Wizard entry** — when the user clicks `Stufe steigen` and a DRAFT exists, the wizard opens directly to the last step the user was on, with a small inline strip at the top of the body `Entwurf wiederhergestellt — letzte Bearbeitung: {relativeDate}.` (`text-xs text-info-500 bg-info-500/10 p-2 rounded mb-4`).

Banner layout (surface 1):
```
<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 {N}.
      </h3>
      <p className="text-xs text-text-secondary mt-0.5">
        Zuletzt bearbeitet: {relativeDate}.
      </p>
    </div>
  </div>
  <div className="flex items-center gap-2">
    <Button variant="ghost" size="sm" onClick={discard} className="text-error-500">
      <Trash2 className="h-4 w-4" /> Verwerfen
    </Button>
    <Button variant="default" size="sm" onClick={resume}>
      <RotateCcw className="h-4 w-4" /> Fortsetzen
    </Button>
  </div>
</div>
```

---

## Component Contract — Pathbuilder-Import-Violations Banner (D-06)

Surface: directly below the avatar header, above the tab-navigation row. Persistent — Phase 1 ships listing only; user-driven resolution UI is v2.

```
<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">
        {N} 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={toggleList} className="mt-2 -ml-2">
        {expanded ? <ChevronDown /> : <ChevronRight />} Liste {expanded ? 'verbergen' : 'anzeigen'}
      </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>
```

---

## Motion

Phase 1 is the first feature in the codebase to use `framer-motion`. Two motion contracts:

1. **Step transitions** (slide horizontal — direction = next/prev):
   - Wrapper: `<AnimatePresence mode="wait">` around the step body.
   - Each step `<motion.div key={stepId} initial={{ opacity: 0, x: direction === 'forward' ? 24 : -24 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: direction === 'forward' ? -24 : 24 }} transition={{ duration: 0.2, ease: 'easeOut' }}>`.
   - Direction is derived from the previous step index vs. current step index.
   - Reduced-motion: respects `prefers-reduced-motion: reduce` — when set, the wrapper falls back to no `x` translation and a 0.1s opacity-only fade. (Implemented via `useReducedMotion()` hook from framer-motion.)

2. **Modal entry/exit**: existing CSS animations from `index.css` (`@keyframes slide-up` / `fade-in`) are sufficient for the wizard container itself. No framer-motion on the outer modal.

**Stepper dot transitions**: pure CSS `transition-colors duration-150 ease-out` on `bg-primary-500` / `bg-bg-tertiary` swaps. No framer-motion needed.

**Confirm-dialog (prereq, abort, discard)**: existing `slide-up 0.3s ease-out` from `index.css` applied to the dialog panel. No framer-motion.

---

## States Inventory

For each interactive surface, the executor must implement:

| Surface | States required |
|---------|-----------------|
| Choice-card | default, hover (`border-border-hover`), focus (`ring-2 ring-primary-500/40`), selected, locked (already chosen / cap), prereq-warning (yellow icon), prereq-fail (hidden by default) |
| Boost-attribute row | default, hover (decorative only — row is a `<div>`), `+/-` disabled when count=0 / cap reached / 4 boosts spent |
| Skill row | default, hover, cap-reached (disabled + warning chip) |
| Talent list | loading (`Spinner` + label), empty (heading + body + show-unavailable toggle), error (heading + body + retry button), populated |
| Wizard footer | first-step (Zurück disabled), middle-step (both enabled when `stepValid`), final-step (Bestätigen replaces Weiter), revision-mode (Zurück zur Übersicht replaces Weiter), committing (`isLoading` on Bestätigen) |
| Stepper dot button | future (disabled), current (`aria-current="step"`), completed (interactive, back-nav), all wrapped at 44×44 hit-zone |
| DRAFT-resume banner | character-sheet surface (always visible until resolved), wizard inline (one-shot info strip on entry) |
| Pathbuilder-violations banner | collapsed (default), expanded (list visible) |
| `Stufe steigen` button on character header | enabled-no-draft (label `Stufe steigen`), enabled-with-draft (label `Stufe fortsetzen`, icon `RotateCcw`), hidden (cap or no permission) |
| Prereq-confirm dialog | default, accepting click closes dialog and selects card |
| Review row | normal (with `Ändern` link), revalidation-cleared (with warning note + `Wählen` link), all-valid (Bestätigen enabled) |

---

## Accessibility

- All interactive elements have a 44×44px tap-target floor (CLAUDE.md mobile-first). The boost `+/-` controls and the stepper-dot wrappers each meet this floor explicitly (`h-11 w-11`).
- All buttons have `aria-label` for icon-only variants:
  - Header close button: `aria-label="Schließen"`.
  - Boost `+`: `aria-label="{abbreviation} Boost erhöhen"` (e.g. `STR Boost erhöhen`).
  - Boost `-`: `aria-label="{abbreviation} Boost verringern"`.
  - Stepper dot buttons: `aria-label="{stepLabel}, {state}"` where state ∈ `aktuell` / `abgeschlossen` / `noch nicht verfügbar`.
- Modals use `role="dialog"` + `aria-modal="true"` + `aria-labelledby` pointing at the header H2.
- The stepper uses `role="navigation"` + each completed/current dot has `aria-current="step"` for current.
- Choice-cards are native `<button>` — keyboard focus + Enter/Space activates `onSelect`.
- The Boost-step row is a `<li>` within a `<ul>`, never a `<button>` — the only interactive children are the `+/-` buttons. No nested-button HTML.
- Prereq-confirm dialog traps focus (focus-lock) — first focusable element is `Abbrechen`, Esc closes the dialog.
- All text passes WCAG AA contrast on `bg-bg-secondary` (`text-text-primary` `#f5f5f7` on `#1a1a1f` is 14:1; `text-text-secondary` `#a1a1a6` is 7.4:1; `text-warning-500` on `bg-warning-500/10` is 4.5:1).
- `prefers-reduced-motion` honored (see Motion section).

---

## Design Tokens — Inventory (for executor reference)

The wizard introduces **zero new design tokens**. All colors, fonts, radii, shadows are read from the existing `@theme` block in `client/src/index.css`:

| Token name | Used by Phase 1 |
|------------|-----------------|
| `--color-primary-500` | wizard accent (CTA, selected card, stepper, banner stripe, button, `Ändern` link, `Zurück zur Übersicht`) |
| `--color-primary-500/40`, `--color-primary-500/20`, `--color-primary-500/10` | halos, badges, info strips |
| `--color-bg-primary` | page background |
| `--color-bg-secondary` | wizard modal surface |
| `--color-bg-tertiary` | choice-card surface, banner background, info-strip background |
| `--color-bg-elevated` | +/- step buttons in boost picker |
| `--color-text-primary`, `--color-text-secondary`, `--color-text-muted` | text hierarchy |
| `--color-border`, `--color-border-hover`, `--color-border-focus` | card outlines, hover, focus |
| `--color-warning-500`, `--color-warning-500/10`, `--color-warning-500/20` | non-evaluable prereq icon, violations banner, "+1 (Cap bei 18)" chip, revalidation-cleared note |
| `--color-error-500`, `--color-error-500/10` | DRAFT-discard button, prereq-fail chip |
| `--color-success-500`, `--color-success-500/10` | completed-step check, positive Δ chips |
| `--color-info-500`, `--color-info-500/10` | "Free Archetype: aktiv" strip, "automatisch"-info strips |
| `--radius-lg` (8px), `--radius-xl` (12px), `--radius-2xl` (16px) | rounded corners on cards / modals |

---

## Registry Safety

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| (none — hand-built primitives in `client/src/shared/components/ui/`) | `Button`, `Input`, `Card`, `Spinner`, `ActionIcon` (existing) | not applicable — code is owned in-repo, not pulled from a registry |
| (no third-party shadcn registry declared) | — | not applicable |

No `npx shadcn` add operations occur in Phase 1. All wizard components are written from scratch under `client/src/features/characters/components/level-up/` (planner discretion on exact filenames; UI-SPEC dictates the component contracts above).

---

## Component File Plan (advisory — final filenames at planner's discretion)

The following kebab-case files implement the contracts above. The planner may merge or split, but each contract section above must be implemented somewhere:

- `level-up-wizard.tsx` — outer container, stepper, header, footer, motion orchestration
- `level-up-step-class-features.tsx` — step 0 (auto-class-features summary)
- `level-up-step-class-feature-choice.tsx` — step 0a (Cleric Doctrine, Wizard School, …)
- `level-up-step-boost.tsx` — step 1 (4 attribute boosts)
- `level-up-step-skill-increase.tsx` — step 2
- `level-up-step-feat-class.tsx` — step 3
- `level-up-step-feat-skill.tsx` — step 4
- `level-up-step-feat-general.tsx` — step 5
- `level-up-step-feat-ancestry.tsx` — step 6
- `level-up-step-feat-archetype.tsx` — step 7 (Free Archetype slot)
- `level-up-step-spellcaster.tsx` — step 8
- `level-up-step-review.tsx` — step 9
- `level-up-choice-card.tsx` — shared choice-card primitive
- `level-up-prereq-confirm-dialog.tsx` — secondary z-60 dialog for D-03 prereq + abort + discard
- `level-up-resume-banner.tsx` — D-14 banner on character-sheet
- `level-up-violations-banner.tsx` — D-06 banner on character-sheet

---

## Checker Sign-Off

- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS

**Approval:** pending
