# Character Creation Wizard Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace the bare "New Character" inline form in CampaignView with a themed 4-step multi-step wizard that walks players through the full Shadowdark character creation process: name/class/ancestry → stat rolling → background/alignment/deity → review & create.

**Architecture:** A standalone `CharacterWizard` modal component manages step state internally. A `character-creation.ts` util handles all dice math (3d6, HP by class, starting gold). A `backgrounds.ts` data file holds all 20 Shadowdark backgrounds. The server `POST /campaigns/:id/characters` endpoint is extended to accept full character data so we can create a complete character in one round-trip.

**Tech Stack:** React + TypeScript + CSS Modules (matching existing patterns); existing `getModifier()` from `client/src/utils/modifiers.ts`; existing `getShadowdarkTitle()` from `client/src/utils/shadowdark-titles.ts`; existing `SelectDropdown` component; Express/MariaDB on server.

---

### Task 1: Extend server character creation endpoint

**Files:**
- Modify: `server/src/routes/characters.ts:93-151`

The current endpoint accepts only `name`, `class`, `ancestry`, `hp_max`. We need it to also accept `alignment`, `background`, `deity`, `title`, and stat values so we can create a fully-formed character without a second PATCH call.

- [ ] **Step 1: Write the failing test** (manual — run the server and test via curl after implementation)

```bash
# After implementation, verify this works:
curl -s -X POST http://localhost:3001/api/campaigns/1/characters \
  -H "Content-Type: application/json" \
  -b "..." \
  -d '{
    "name": "Tharyn",
    "class": "Fighter",
    "ancestry": "Human",
    "alignment": "Lawful",
    "background": "Soldier",
    "deity": "",
    "title": "Squire",
    "hp_max": 9,
    "gp": 30,
    "stats": {"STR":15,"DEX":10,"CON":12,"INT":8,"WIS":11,"CHA":9}
  }' | jq '.alignment, .background, .title, .gp'
# Expected: "Lawful" "Soldier" "Squire" 30
```

- [ ] **Step 2: Extend the POST handler to accept full character fields**

In `server/src/routes/characters.ts`, replace the destructure at line 97 and the INSERT at line 106:

```typescript
// Replace lines 97-120 with:
const {
  name,
  class: charClass,
  ancestry,
  hp_max,
  alignment,
  background,
  deity,
  title,
  gp,
  stats,
} = req.body;

if (!name?.trim()) {
  res.status(400).json({ error: "Character name is required" });
  return;
}

const userId = req.user?.userId ?? null;

const [result] = await db.execute<ResultSetHeader>(
  `INSERT INTO characters
     (campaign_id, user_id, name, class, ancestry, hp_current, hp_max,
      alignment, background, deity, title, gp, color)
   VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
  [
    campaignId,
    userId,
    name.trim(),
    charClass ?? "Fighter",
    ancestry ?? "Human",
    hp_max ?? 1,
    hp_max ?? 1,
    alignment ?? "Neutral",
    background ?? "",
    deity ?? "",
    title ?? "",
    gp ?? 0,
    generateCharacterColor(),
  ]
);
const characterId = result.insertId;
```

- [ ] **Step 3: Use provided stat values instead of default 10s**

Replace the `Promise.all` stat insert block (lines 123-130) with:

```typescript
const statNames = ["STR", "DEX", "CON", "INT", "WIS", "CHA"];
const providedStats: Record<string, number> = stats && typeof stats === "object" ? stats : {};

await Promise.all(
  statNames.map((stat) =>
    db.execute(
      "INSERT INTO character_stats (character_id, stat_name, value) VALUES (?, ?, ?)",
      [characterId, stat, providedStats[stat] ?? 10]
    )
  )
);
```

Then update the enriched object's stats line (currently `DEFAULT_STATS.map((s) => ({ stat_name: s, value: 10 }))`):

```typescript
const enriched = {
  ...charRows[0],
  overrides: {},
  stats: statNames.map((s) => ({ stat_name: s, value: providedStats[s] ?? 10 })),
  gear: [],
  talents: [],
};
```

- [ ] **Step 4: Run the server and test**

```bash
cd /Users/aaron.wood/workspace/shadowdark/server && npm run dev
```

Confirm no TypeScript errors and the curl test from Step 1 returns expected values.

- [ ] **Step 5: Commit**

```bash
git -C /Users/aaron.wood/workspace/shadowdark add server/src/routes/characters.ts
git -C /Users/aaron.wood/workspace/shadowdark commit -m "feat: extend character creation endpoint to accept full character data"
```

---

### Task 2: Backgrounds data file

**Files:**
- Create: `client/src/data/backgrounds.ts`

20 official Shadowdark backgrounds from the Player Quickstart, each with a name and the skill it grants.

- [ ] **Step 1: Create the file**

```typescript
// client/src/data/backgrounds.ts
export interface Background {
  name: string;
  skill: string;
}

export const BACKGROUNDS: Background[] = [
  { name: "Urchin",              skill: "You are never lost in a city and can always find food and shelter in urban areas." },
  { name: "Wanted",              skill: "You know how to move unseen and blend into crowds to avoid pursuit." },
  { name: "Cult Initiate",       skill: "You know the rituals, symbols, and secrets of a dark cult." },
  { name: "Thieves' Guild",      skill: "You can pick locks, pocket items, and recognize guild signs." },
  { name: "Banished",            skill: "You can survive in the wilderness indefinitely and sense approaching weather." },
  { name: "Orphaned",            skill: "You can beg, scrounge, and identify charity-givers in any settlement." },
  { name: "Wizard's Apprentice", skill: "You can read and identify magical writing and recognize common spell effects." },
  { name: "Jeweler",             skill: "You can appraise gems, metals, and jewelry with accuracy." },
  { name: "Herbalist",           skill: "You know which plants heal, harm, or alter the mind in any terrain." },
  { name: "Barbarian",           skill: "You can track creatures and navigate by the stars in any wilderness." },
  { name: "Mercenary",           skill: "You know the going rate for violence and can always find hired work." },
  { name: "Sailor",              skill: "You can navigate by stars and read weather patterns at sea or in the open." },
  { name: "Acolyte",             skill: "You know the prayers, calendar, and hierarchy of a major religion." },
  { name: "Soldier",             skill: "You can read a battlefield and know the tactics of common military units." },
  { name: "Ranger",              skill: "You can move silently through wilderness and identify animal tracks." },
  { name: "Scout",               skill: "You can estimate distances, draw rough maps, and spot ambushes." },
  { name: "Minstrel",            skill: "You can perform music, recite epics, and always find a crowd willing to listen." },
  { name: "Scholar",             skill: "You can read most languages and recall obscure historical and arcane lore." },
  { name: "Noble",               skill: "You know the etiquette, rumors, and politics of the upper class." },
  { name: "Chirurgeon",          skill: "You can stabilize the dying and treat wounds to prevent infection." },
];

export const BACKGROUND_NAMES = BACKGROUNDS.map((b) => b.name);
```

- [ ] **Step 2: Verify TypeScript compiles**

```bash
cd /Users/aaron.wood/workspace/shadowdark/client && npx tsc --noEmit
```

Expected: no errors.

- [ ] **Step 3: Commit**

```bash
git -C /Users/aaron.wood/workspace/shadowdark add client/src/data/backgrounds.ts
git -C /Users/aaron.wood/workspace/shadowdark commit -m "feat: add Shadowdark backgrounds data file"
```

---

### Task 3: Character creation utilities

**Files:**
- Create: `client/src/utils/character-creation.ts`

Pure math functions used by the wizard: roll 3d6, calculate HP by class, roll starting gold, calculate derived title.

- [ ] **Step 1: Create the util file**

```typescript
// client/src/utils/character-creation.ts
import { getModifier } from "./modifiers.js";
import { getShadowdarkTitle } from "./shadowdark-titles.js";

const CLASS_HIT_DIE: Record<string, number> = {
  Fighter: 8,
  Priest: 6,
  Thief: 4,
  Wizard: 4,
};

/** Roll 3d6 and return the sum (3–18). */
export function roll3d6(): number {
  return (
    Math.ceil(Math.random() * 6) +
    Math.ceil(Math.random() * 6) +
    Math.ceil(Math.random() * 6)
  );
}

/** Roll a full set of 6 stats (STR/DEX/CON/INT/WIS/CHA). */
export function rollAllStats(): Record<string, number> {
  return {
    STR: roll3d6(),
    DEX: roll3d6(),
    CON: roll3d6(),
    INT: roll3d6(),
    WIS: roll3d6(),
    CHA: roll3d6(),
  };
}

/**
 * Roll HP for level 1: roll the class hit die, add CON modifier.
 * Minimum result is 1.
 */
export function rollStartingHp(charClass: string, conScore: number): number {
  const die = CLASS_HIT_DIE[charClass] ?? 4;
  const rolled = Math.ceil(Math.random() * die);
  return Math.max(1, rolled + getModifier(conScore));
}

/**
 * Roll starting gold: 2d6 × 5 gp.
 */
export function rollStartingGold(): number {
  const d1 = Math.ceil(Math.random() * 6);
  const d2 = Math.ceil(Math.random() * 6);
  return (d1 + d2) * 5;
}

/**
 * Derive starting gear slots: 10 + STR modifier.
 */
export function startingGearSlots(strScore: number): number {
  return 10 + getModifier(strScore);
}

/** Convenience: get title for a level-1 character. */
export function getStartingTitle(charClass: string, alignment: string): string {
  return getShadowdarkTitle(charClass, alignment, 1) ?? "";
}
```

- [ ] **Step 2: Verify TypeScript**

```bash
cd /Users/aaron.wood/workspace/shadowdark/client && npx tsc --noEmit
```

Expected: no errors.

- [ ] **Step 3: Commit**

```bash
git -C /Users/aaron.wood/workspace/shadowdark add client/src/utils/character-creation.ts
git -C /Users/aaron.wood/workspace/shadowdark commit -m "feat: add character creation utility functions (roll3d6, rollHp, rollGold)"
```

---

### Task 4: Extend client API function

**Files:**
- Modify: `client/src/api.ts:69-76`

Extend `createCharacter` to accept the full set of fields the server now supports.

- [ ] **Step 1: Update the createCharacter function signature**

Replace the existing `createCharacter` export:

```typescript
// Before:
export const createCharacter = (
  campaignId: number,
  data: { name: string; class?: string; ancestry?: string; hp_max?: number },
) =>
  request<Character>(`/campaigns/${campaignId}/characters`, {
    method: "POST",
    body: JSON.stringify(data),
  });
```

```typescript
// After:
export interface CreateCharacterData {
  name: string;
  class?: string;
  ancestry?: string;
  alignment?: string;
  background?: string;
  deity?: string;
  title?: string;
  hp_max?: number;
  gp?: number;
  stats?: Record<string, number>;
}

export const createCharacter = (campaignId: number, data: CreateCharacterData) =>
  request<Character>(`/campaigns/${campaignId}/characters`, {
    method: "POST",
    body: JSON.stringify(data),
  });
```

- [ ] **Step 2: Verify TypeScript compiles**

```bash
cd /Users/aaron.wood/workspace/shadowdark/client && npx tsc --noEmit
```

Expected: no errors.

- [ ] **Step 3: Commit**

```bash
git -C /Users/aaron.wood/workspace/shadowdark add client/src/api.ts
git -C /Users/aaron.wood/workspace/shadowdark commit -m "feat: extend createCharacter API to accept full character fields"
```

---

### Task 5: CharacterWizard component — step state & step 1 (Name / Class / Ancestry)

**Files:**
- Create: `client/src/components/CharacterWizard.tsx`
- Create: `client/src/components/CharacterWizard.module.css`

The wizard manages all step state. Step 1 collects name, class, and ancestry. It validates that name is non-empty before Next is enabled.

- [ ] **Step 1: Create the CSS file**

```css
/* client/src/components/CharacterWizard.module.css */

/* ── Backdrop ─────────────────────────────────────────── */
.backdrop {
  position: fixed;
  inset: 0;
  background: var(--bg-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
  padding: 1rem;
}

/* ── Dialog ───────────────────────────────────────────── */
.dialog {
  background-color: var(--bg-modal);
  background-image: var(--texture-surface), var(--texture-speckle);
  background-size: 256px 256px, 128px 128px;
  background-repeat: repeat, repeat;
  border: 2px solid rgba(var(--gold-rgb), 0.3);
  border-radius: 4px;
  padding: 1.5rem;
  width: 100%;
  max-width: 480px;
  box-shadow:
    0 8px 40px rgba(var(--shadow-rgb), 0.7),
    0 2px 8px rgba(var(--shadow-rgb), 0.5),
    inset 0 1px 0 rgba(var(--gold-rgb), 0.1),
    inset 0 0 60px rgba(var(--shadow-rgb), 0.2);
}

/* ── Header ───────────────────────────────────────────── */
.title {
  font-family: "Cinzel", Georgia, serif;
  font-size: 1.2rem;
  font-weight: 700;
  color: var(--gold);
  letter-spacing: 0.05em;
  text-shadow: 0 1px 2px rgba(var(--shadow-rgb), 0.3);
  margin-bottom: 0.25rem;
}

.stepLabel {
  font-size: 0.75rem;
  color: var(--text-tertiary);
  font-family: "Cinzel", Georgia, serif;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 1.25rem;
}

/* ── Progress dots ────────────────────────────────────── */
.dots {
  display: flex;
  gap: 0.4rem;
  margin-bottom: 1.25rem;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(var(--gold-rgb), 0.2);
  border: 1px solid rgba(var(--gold-rgb), 0.3);
  transition: background 0.2s;
}

.dotActive {
  background: var(--gold);
  border-color: var(--gold);
}

.dotDone {
  background: rgba(var(--gold-rgb), 0.5);
  border-color: rgba(var(--gold-rgb), 0.5);
}

/* ── Form fields ──────────────────────────────────────── */
.field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin-bottom: 0.75rem;
}

.fieldLabel {
  font-family: "Cinzel", Georgia, serif;
  font-size: 0.75rem;
  color: var(--text-secondary);
  text-transform: uppercase;
  font-weight: 600;
  letter-spacing: 0.05em;
}

.input {
  padding: 0.5rem 0.75rem;
  background: var(--bg-inset);
  border: 1px solid rgba(var(--gold-rgb), 0.15);
  border-radius: 4px;
  color: var(--text-primary);
  font-size: 0.9rem;
  font-family: "Alegreya", Georgia, serif;
}

.input:focus {
  outline: none;
  border-color: var(--gold);
}

/* ── Stat rolling (step 2) ────────────────────────────── */
.statGrid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}

.statRow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.4rem 0.6rem;
  background: var(--bg-inset);
  border: 1px solid rgba(var(--gold-rgb), 0.12);
  border-radius: 4px;
}

.statName {
  font-family: "Cinzel", Georgia, serif;
  font-size: 0.7rem;
  color: var(--text-secondary);
  letter-spacing: 0.05em;
  font-weight: 600;
}

.statValue {
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--text-primary);
  min-width: 2ch;
  text-align: right;
}

.statMod {
  font-size: 0.75rem;
  color: var(--text-tertiary);
  min-width: 3ch;
  text-align: right;
}

.rerollBtn {
  width: 100%;
  padding: 0.5rem;
  background: none;
  border: 1px solid rgba(var(--gold-rgb), 0.25);
  border-radius: 4px;
  color: var(--text-secondary);
  font-family: "Cinzel", Georgia, serif;
  font-size: 0.8rem;
  cursor: pointer;
  letter-spacing: 0.05em;
  transition: border-color 0.15s, color 0.15s;
  margin-bottom: 0.75rem;
}

.rerollBtn:hover {
  border-color: rgba(var(--gold-rgb), 0.5);
  color: var(--gold);
}

/* ── Background list (step 3) ─────────────────────────── */
.bgList {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  max-height: 180px;
  overflow-y: auto;
  margin-bottom: 0.75rem;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--gold-rgb), 0.2) transparent;
}

.bgItem {
  padding: 0.4rem 0.6rem;
  border-radius: 4px;
  border: 1px solid transparent;
  cursor: pointer;
  font-size: 0.88rem;
  font-family: "Alegreya", Georgia, serif;
  transition: background 0.1s, border-color 0.1s;
}

.bgItem:hover {
  background: rgba(var(--gold-rgb), 0.06);
}

.bgItemSelected {
  background: rgba(var(--gold-rgb), 0.12);
  border-color: rgba(var(--gold-rgb), 0.3);
  color: var(--gold);
}

.bgSkill {
  font-size: 0.75rem;
  color: var(--text-tertiary);
  font-style: italic;
  margin-top: 0.2rem;
}

.randomBtn {
  width: 100%;
  padding: 0.35rem;
  background: none;
  border: 1px dashed rgba(var(--gold-rgb), 0.2);
  border-radius: 4px;
  color: var(--text-tertiary);
  font-size: 0.8rem;
  cursor: pointer;
  font-family: "Cinzel", Georgia, serif;
  letter-spacing: 0.05em;
  transition: border-color 0.15s, color 0.15s;
  margin-bottom: 0.75rem;
}

.randomBtn:hover {
  border-color: rgba(var(--gold-rgb), 0.4);
  color: var(--text-secondary);
}

/* ── Review (step 4) ──────────────────────────────────── */
.reviewGrid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.35rem 1rem;
  margin-bottom: 0.75rem;
}

.reviewRow {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  border-bottom: 1px solid rgba(var(--gold-rgb), 0.08);
  padding-bottom: 0.2rem;
}

.reviewLabel {
  font-family: "Cinzel", Georgia, serif;
  font-size: 0.7rem;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.reviewValue {
  font-size: 0.88rem;
  color: var(--text-primary);
  font-family: "Alegreya", Georgia, serif;
}

.reviewSectionTitle {
  font-family: "Cinzel", Georgia, serif;
  font-size: 0.75rem;
  color: var(--gold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 0.4rem;
  grid-column: 1 / -1;
  border-bottom: 1px solid rgba(var(--gold-rgb), 0.15);
  padding-bottom: 0.2rem;
}

.reviewStats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.35rem;
  margin-bottom: 0.75rem;
}

.reviewStat {
  text-align: center;
  padding: 0.35rem;
  background: var(--bg-inset);
  border-radius: 4px;
  border: 1px solid rgba(var(--gold-rgb), 0.1);
}

.reviewStatName {
  font-family: "Cinzel", Georgia, serif;
  font-size: 0.65rem;
  color: var(--text-tertiary);
  letter-spacing: 0.05em;
}

.reviewStatVal {
  font-size: 1rem;
  font-weight: 700;
  color: var(--text-primary);
}

.reviewStatMod {
  font-size: 0.7rem;
  color: var(--text-tertiary);
}

/* ── Navigation buttons ───────────────────────────────── */
.actions {
  display: flex;
  gap: 0.5rem;
  justify-content: space-between;
  margin-top: 1rem;
}

.btnSecondary {
  padding: 0.5rem 1rem;
  border-radius: 4px;
  font-weight: 600;
  cursor: pointer;
  font-size: 0.9rem;
  background: none;
  border: 1px solid rgba(var(--gold-rgb), 0.2);
  color: var(--text-secondary);
  font-family: "Cinzel", Georgia, serif;
  transition: border-color 0.15s, color 0.15s;
}

.btnSecondary:hover {
  border-color: rgba(var(--gold-rgb), 0.4);
  color: var(--text-primary);
}

.btnPrimary {
  padding: 0.5rem 1.25rem;
  background: var(--btn-gold-bg);
  color: var(--btn-active-text);
  border: none;
  border-radius: 4px;
  font-family: "Cinzel", Georgia, serif;
  font-weight: 600;
  cursor: pointer;
  font-size: 0.9rem;
  box-shadow:
    0 2px 4px rgba(var(--shadow-rgb), 0.3),
    inset 0 1px 0 rgba(255, 255, 255, 0.1);
  text-shadow: 0 1px 1px rgba(var(--shadow-rgb), 0.2);
}

.btnPrimary:hover {
  background: linear-gradient(180deg, var(--gold-bright), var(--gold-hover) 40%, var(--gold));
}

.btnPrimary:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
```

- [ ] **Step 2: Create CharacterWizard.tsx with step 1**

```tsx
// client/src/components/CharacterWizard.tsx
import { useState } from "react";
import type { CreateCharacterData } from "../api.js";
import SelectDropdown from "./SelectDropdown.js";
import { BACKGROUNDS, type Background } from "../data/backgrounds.js";
import {
  rollAllStats,
  rollStartingHp,
  rollStartingGold,
  startingGearSlots,
  getStartingTitle,
} from "../utils/character-creation.js";
import { getModifier, formatModifier } from "../utils/modifiers.js";
import styles from "./CharacterWizard.module.css";

const CLASSES = ["Fighter", "Priest", "Thief", "Wizard"];
const ANCESTRIES = ["Human", "Elf", "Dwarf", "Halfling", "Goblin", "Half-Orc"];
const ALIGNMENTS = ["Lawful", "Neutral", "Chaotic"];
const STAT_NAMES = ["STR", "DEX", "CON", "INT", "WIS", "CHA"];

const STEP_LABELS = [
  "Step 1 of 4 — Name & Origins",
  "Step 2 of 4 — Ability Scores",
  "Step 3 of 4 — Background & Beliefs",
  "Step 4 of 4 — Review & Create",
];

interface Props {
  campaignId: number;
  onSubmit: (data: CreateCharacterData) => Promise<void>;
  onClose: () => void;
}

export default function CharacterWizard({ onSubmit, onClose }: Props) {
  const [step, setStep] = useState(0);

  // Step 1 state
  const [name, setName] = useState("");
  const [charClass, setCharClass] = useState("Fighter");
  const [ancestry, setAncestry] = useState("Human");

  // Step 2 state
  const [stats, setStats] = useState<Record<string, number>>(rollAllStats);

  // Step 3 state
  const [selectedBg, setSelectedBg] = useState<Background | null>(null);
  const [alignment, setAlignment] = useState("Neutral");
  const [deity, setDeity] = useState("");

  // Derived for review / submit
  const conMod = getModifier(stats.CON);
  const hp = rollStartingHp(charClass, stats.CON);
  const gp = rollStartingGold();
  const gearSlots = startingGearSlots(stats.STR);
  const title = getStartingTitle(charClass, alignment);

  // Keep hp/gp stable across re-renders by computing once at step transition
  const [rolledHp, setRolledHp] = useState(0);
  const [rolledGp, setRolledGp] = useState(0);
  const [rolledGearSlots, setRolledGearSlots] = useState(10);

  function handleRerollStats() {
    setStats(rollAllStats());
  }

  function advanceToStep(nextStep: number) {
    if (nextStep === 3 && step === 2) {
      // Roll HP and gold when entering review so they're stable
      setRolledHp(rollStartingHp(charClass, stats.CON));
      setRolledGp(rollStartingGold());
      setRolledGearSlots(startingGearSlots(stats.STR));
    }
    setStep(nextStep);
  }

  async function handleCreate() {
    await onSubmit({
      name: name.trim(),
      class: charClass,
      ancestry,
      alignment,
      background: selectedBg?.name ?? "",
      deity: charClass === "Priest" ? deity : "",
      title,
      hp_max: rolledHp,
      gp: rolledGp,
      stats,
    });
  }

  return (
    <div className={styles.backdrop} onClick={onClose}>
      <div className={styles.dialog} onClick={(e) => e.stopPropagation()}>
        <div className={styles.title}>New Character</div>
        <div className={styles.stepLabel}>{STEP_LABELS[step]}</div>

        {/* Progress dots */}
        <div className={styles.dots}>
          {[0, 1, 2, 3].map((i) => (
            <div
              key={i}
              className={[
                styles.dot,
                i === step ? styles.dotActive : "",
                i < step ? styles.dotDone : "",
              ]
                .filter(Boolean)
                .join(" ")}
            />
          ))}
        </div>

        {/* ── Step 1: Name / Class / Ancestry ── */}
        {step === 0 && (
          <>
            <div className={styles.field}>
              <label className={styles.fieldLabel}>Name</label>
              <input
                className={styles.input}
                type="text"
                value={name}
                onChange={(e) => setName(e.target.value)}
                autoFocus
                placeholder="Tharyn the Bold..."
              />
            </div>
            <div className={styles.field}>
              <label className={styles.fieldLabel}>Class</label>
              <SelectDropdown
                value={charClass}
                options={CLASSES}
                onChange={setCharClass}
              />
            </div>
            <div className={styles.field}>
              <label className={styles.fieldLabel}>Ancestry</label>
              <SelectDropdown
                value={ancestry}
                options={ANCESTRIES}
                onChange={setAncestry}
              />
            </div>
          </>
        )}

        {/* ── Step 2: Stat Rolling ── */}
        {step === 1 && (
          <>
            <div className={styles.statGrid}>
              {STAT_NAMES.map((s) => (
                <div key={s} className={styles.statRow}>
                  <span className={styles.statName}>{s}</span>
                  <span className={styles.statValue}>{stats[s]}</span>
                  <span className={styles.statMod}>
                    {formatModifier(getModifier(stats[s]))}
                  </span>
                </div>
              ))}
            </div>
            <button className={styles.rerollBtn} onClick={handleRerollStats}>
              Reroll All
            </button>
          </>
        )}

        {/* ── Step 3: Background / Alignment / Deity ── */}
        {step === 2 && (
          <>
            <div className={styles.field}>
              <label className={styles.fieldLabel}>Background</label>
            </div>
            <div className={styles.bgList}>
              {BACKGROUNDS.map((bg) => (
                <div
                  key={bg.name}
                  className={[
                    styles.bgItem,
                    selectedBg?.name === bg.name ? styles.bgItemSelected : "",
                  ]
                    .filter(Boolean)
                    .join(" ")}
                  onClick={() => setSelectedBg(bg)}
                >
                  <div>{bg.name}</div>
                  {selectedBg?.name === bg.name && (
                    <div className={styles.bgSkill}>{bg.skill}</div>
                  )}
                </div>
              ))}
            </div>
            <button
              className={styles.randomBtn}
              onClick={() =>
                setSelectedBg(
                  BACKGROUNDS[Math.floor(Math.random() * BACKGROUNDS.length)]
                )
              }
            >
              Random Background
            </button>
            <div className={styles.field}>
              <label className={styles.fieldLabel}>Alignment</label>
              <SelectDropdown
                value={alignment}
                options={ALIGNMENTS}
                onChange={setAlignment}
              />
            </div>
            {charClass === "Priest" && (
              <div className={styles.field}>
                <label className={styles.fieldLabel}>Deity</label>
                <input
                  className={styles.input}
                  type="text"
                  value={deity}
                  onChange={(e) => setDeity(e.target.value)}
                  placeholder="Saint Terragnis..."
                />
              </div>
            )}
          </>
        )}

        {/* ── Step 4: Review ── */}
        {step === 3 && (
          <>
            <div className={styles.reviewGrid}>
              <div className={styles.reviewSectionTitle}>Character</div>
              {[
                ["Name", name],
                ["Class", charClass],
                ["Ancestry", ancestry],
                ["Alignment", alignment],
                ["Title", title],
                ["Background", selectedBg?.name ?? "—"],
                ["HP", String(rolledHp)],
                ["Starting Gold", `${rolledGp} gp`],
                ["Gear Slots", String(rolledGearSlots)],
                ...(charClass === "Priest" && deity ? [["Deity", deity]] : []),
              ].map(([label, value]) => (
                <div key={label} className={styles.reviewRow}>
                  <span className={styles.reviewLabel}>{label}</span>
                  <span className={styles.reviewValue}>{value}</span>
                </div>
              ))}
            </div>

            <div
              className={styles.reviewSectionTitle}
              style={{ fontFamily: "Cinzel, Georgia, serif", fontSize: "0.75rem", color: "var(--gold)", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: "0.4rem", borderBottom: "1px solid rgba(var(--gold-rgb), 0.15)", paddingBottom: "0.2rem" }}
            >
              Ability Scores
            </div>
            <div className={styles.reviewStats}>
              {STAT_NAMES.map((s) => (
                <div key={s} className={styles.reviewStat}>
                  <div className={styles.reviewStatName}>{s}</div>
                  <div className={styles.reviewStatVal}>{stats[s]}</div>
                  <div className={styles.reviewStatMod}>
                    {formatModifier(getModifier(stats[s]))}
                  </div>
                </div>
              ))}
            </div>
          </>
        )}

        {/* ── Navigation ── */}
        <div className={styles.actions}>
          <button
            className={styles.btnSecondary}
            onClick={step === 0 ? onClose : () => setStep(step - 1)}
          >
            {step === 0 ? "Cancel" : "Back"}
          </button>
          {step < 3 ? (
            <button
              className={styles.btnPrimary}
              disabled={step === 0 && !name.trim()}
              onClick={() => advanceToStep(step + 1)}
            >
              Next
            </button>
          ) : (
            <button className={styles.btnPrimary} onClick={handleCreate}>
              Create Character
            </button>
          )}
        </div>
      </div>
    </div>
  );
}
```

- [ ] **Step 3: Verify TypeScript compiles**

```bash
cd /Users/aaron.wood/workspace/shadowdark/client && npx tsc --noEmit
```

Expected: no errors.

- [ ] **Step 4: Commit**

```bash
git -C /Users/aaron.wood/workspace/shadowdark add client/src/components/CharacterWizard.tsx client/src/components/CharacterWizard.module.css
git -C /Users/aaron.wood/workspace/shadowdark commit -m "feat: add CharacterWizard multi-step character creation modal"
```

---

### Task 6: Wire CharacterWizard into CampaignView

**Files:**
- Modify: `client/src/pages/CampaignView.tsx`

Replace the inline `showCreate` / `newChar` form with `CharacterWizard`. Remove the `newChar` state, the inline form JSX block, and the `CLASSES`/`ANCESTRIES` constants from this file (they now live in the wizard).

- [ ] **Step 1: Add the import**

At the top of `CampaignView.tsx`, after the existing component imports, add:

```typescript
import CharacterWizard from "../components/CharacterWizard.js";
```

- [ ] **Step 2: Remove newChar state**

Remove this block (around lines 45-50):

```typescript
const [newChar, setNewChar] = useState({
  name: "",
  class: "Fighter",
  ancestry: "Human",
  hp_max: 1,
});
```

- [ ] **Step 3: Update handleCreate**

Replace the existing `handleCreate` function (around lines 290-299):

```typescript
// Before:
async function handleCreate(e: React.FormEvent) {
  e.preventDefault();
  if (!newChar.name.trim()) return;
  try {
    await createCharacter(campaignId, newChar);
    setNewChar({ name: "", class: "Fighter", ancestry: "Human", hp_max: 1 });
    setShowCreate(false);
  } catch (err) {
    console.error("Failed to create character:", err);
  }
}
```

```typescript
// After:
async function handleCreate(data: import("../api.js").CreateCharacterData) {
  try {
    await createCharacter(campaignId, data);
    setShowCreate(false);
  } catch (err) {
    console.error("Failed to create character:", err);
  }
}
```

- [ ] **Step 4: Replace inline form JSX with CharacterWizard**

Find the inline form block in the JSX (around lines 440-508) — the entire `{showCreate && (<div className={styles.createModal}>...</div>)}` block. Replace it with:

```tsx
{showCreate && (
  <CharacterWizard
    campaignId={campaignId}
    onSubmit={handleCreate}
    onClose={() => setShowCreate(false)}
  />
)}
```

- [ ] **Step 5: Remove unused imports and constants**

Remove `CLASSES` and `ANCESTRIES` from the top of `CampaignView.tsx` (they're now only used internally in `CharacterWizard`). Also remove the `SelectDropdown` import if it's no longer used elsewhere in this file (check — it may still be used in the create form, which is now gone).

```typescript
// Remove these lines if no longer used:
const CLASSES = ["Fighter", "Priest", "Thief", "Wizard"];
const ANCESTRIES = ["Human", "Elf", "Dwarf", "Halfling", "Goblin", "Half-Orc"];
import SelectDropdown from "../components/SelectDropdown.js";
```

- [ ] **Step 6: Verify TypeScript compiles**

```bash
cd /Users/aaron.wood/workspace/shadowdark/client && npx tsc --noEmit
```

Expected: no errors.

- [ ] **Step 7: Commit**

```bash
git -C /Users/aaron.wood/workspace/shadowdark add client/src/pages/CampaignView.tsx
git -C /Users/aaron.wood/workspace/shadowdark commit -m "feat: replace inline create form with CharacterWizard multi-step modal"
```

---

### Task 7: Visual QA and CSS polish

**Files:**
- Modify: `client/src/components/CharacterWizard.tsx` (minor fixes as needed)
- Modify: `client/src/components/CharacterWizard.module.css` (polish as needed)

Open the app, click "+ Add Character", and walk through all 4 steps. Verify each step works as expected and looks good.

- [ ] **Step 1: Start the dev server**

```bash
cd /Users/aaron.wood/workspace/shadowdark/client && npm run dev
```

- [ ] **Step 2: Walk through each step manually**

Check:
- Step 1: Name field auto-focuses. "Next" disabled with empty name. Class/ancestry dropdowns work.
- Step 2: Stats show correct values. Reroll All gives new numbers. Modifiers display correctly.
- Step 3: Background list scrolls smoothly. Clicking a background shows skill text. Random Background works. Alignment dropdown works. Deity field only shows for Priest.
- Step 4: All review values are correct. Title matches class/alignment. HP and gold are reasonable values. Stats block shows all 6 stats.
- "Create Character" closes modal and shows new character card in grid.

- [ ] **Step 3: Fix any visual issues found**

Common things to check:
- Background list scroll height feels right (adjust `max-height` in `.bgList`)
- Review section not cut off on smaller screens (adjust `max-width` on `.dialog` if needed)
- Dark/light mode both look correct

- [ ] **Step 4: Commit any CSS fixes**

```bash
git -C /Users/aaron.wood/workspace/shadowdark add client/src/components/CharacterWizard.tsx client/src/components/CharacterWizard.module.css
git -C /Users/aaron.wood/workspace/shadowdark commit -m "fix: CharacterWizard visual polish"
```

---

## Implementation Notes

**HP is rolled at step transition:** `rolledHp` is set when advancing from step 3 → 4, so the value shown in review is what gets sent to the server. It won't re-roll if the user navigates back and forward.

**Gold is also rolled at step transition:** Same pattern as HP — rolled once entering review, stable through multiple visits.

**Gear slots are derived from STR:** `startingGearSlots()` returns `10 + STR modifier`. This is shown in review and sent as part of the create payload. The server currently uses `gear_slots_max` — ensure the PATCH call in the server endpoint saves this value. If the server's create endpoint doesn't yet support `gear_slots_max`, add it alongside the other extended fields in Task 1.

**Deity field only shows for Priests:** Step 3 conditionally renders the deity input. For all other classes it's skipped (empty string sent to server).

**Background is optional:** A player can advance from step 3 without selecting a background — `selectedBg` will be `null` and an empty string is sent to the server.

**The `conMod` / `hp` / `gp` / `gearSlots` computed values at the top of the component are not used directly** — they're only used as a seed for the rolled values stored in state. This avoids re-rolling on every render.
