diff --git a/GODOT_PORTING_GUIDE.md b/GODOT_PORTING_GUIDE.md
new file mode 100644
index 0000000..1ac5858
--- /dev/null
+++ b/GODOT_PORTING_GUIDE.md
@@ -0,0 +1,726 @@
+# Theriapolis → Godot 4.6 Porting Guide
+
+A companion to `README.md`. This document maps the React/HTML prototype onto a Godot 4.6 project, file by file, with code sketches for the parts that are not obvious. It assumes you've read the existing README and have a working Godot 4.6 install.
+
+> **Scope.** This guide covers the character-creation wizard only — the same surface the prototype covers. It does not address how the resulting character is consumed by the rest of the game; that handoff is described under "The handoff contract" at the end.
+
+---
+
+## 0. The honest summary
+
+The port is **a UI rebuild, not a translation.** What carries over verbatim:
+
+- The four content JSON files in `data/`.
+- The `state` shape used as the character record.
+- All math (`abilityMod`, modifier summing, validation predicates).
+- Step-gating rules and the wizard flow.
+- Drag/drop *semantics* (pool→slot, slot→slot, slot→pool).
+
+What gets rebuilt in Godot's idiom:
+
+- All views (HTML/CSS → Control scenes + Theme).
+- The hover-popover system (`TraitName`) → a custom `PopupPanel` Control.
+- HTML5 drag-and-drop → Godot's `_get_drag_data` / `_can_drop_data` / `_drop_data`.
+- The Tweaks panel — almost certainly delete it; bake one theme.
+
+Plan on roughly: 1–2 days for scaffolding, ~1 day per step scene, 1–2 days for the popover and drag system, 2–3 days for theming the parchment look. Plus polish.
+
+---
+
+## 1. Suggested Godot project layout
+
+```
+res://
+├── project.godot
+├── data/ # Copy unchanged from prototype/data/
+│ ├── clades.json
+│ ├── species.json
+│ ├── classes.json
+│ └── backgrounds.json
+├── fonts/ # Bundle these — do NOT rely on Google Fonts at runtime
+│ ├── CormorantGaramond-Medium.ttf
+│ ├── CormorantGaramond-MediumItalic.ttf
+│ ├── CrimsonPro-Regular.ttf
+│ ├── CrimsonPro-Italic.ttf
+│ ├── CrimsonPro-SemiBold.ttf
+│ └── JetBrainsMono-Regular.ttf
+├── theme/
+│ ├── parchment.tres # Main Theme resource
+│ ├── parchment_styles.gd # Programmatic StyleBox factory (optional)
+│ └── paper_grain.png # Baked grain background
+├── autoload/
+│ ├── content.gd # Loads the four JSON files; replaces data.jsx
+│ ├── character.gd # Resource defining the character record
+│ └── rules.gd # ABILITIES, SKILL_*, abilityMod(), validate()
+├── scenes/
+│ ├── wizard.tscn # Root; replaces app.jsx
+│ ├── wizard.gd
+│ ├── stepper.tscn # 7-tab header; replaces .stepper
+│ ├── aside.tscn # Right-rail summary; replaces
+│ ├── nav_bar.tscn # Bottom Back / Next / Confirm bar
+│ ├── steps/
+│ │ ├── step_clade.tscn / .gd
+│ │ ├── step_species.tscn / .gd
+│ │ ├── step_class.tscn / .gd
+│ │ ├── step_background.tscn / .gd
+│ │ ├── step_stats.tscn / .gd
+│ │ ├── step_skills.tscn / .gd
+│ │ └── step_review.tscn / .gd
+│ └── widgets/
+│ ├── codex_card.tscn # Reusable selectable card
+│ ├── trait_chip.tscn # Replaces TraitName
+│ ├── trait_popover.tscn # The hover hint window
+│ ├── ability_token.tscn # Replaces .die / draggable score
+│ ├── ability_slot.tscn # Replaces .slot
+│ └── stat_strip.tscn # The 6-cell summary row
+└── icon.svg
+```
+
+### Autoloads (Project Settings → Autoload)
+
+| Name | Path | Singleton | Why |
+|-------------|-------------------------|-----------|---------------------------------------|
+| `Content` | `res://autoload/content.gd` | Yes | One-shot JSON loader. |
+| `Rules` | `res://autoload/rules.gd` | Yes | Static tables and pure functions. |
+| (no autoload for the character) | -- | -- | The character is a `Resource` instance owned by `wizard.tscn`. |
+
+---
+
+## 2. The data layer — concrete GDScript
+
+### 2.1 `Character` resource
+
+Mirrors the JS `state` shape exactly. Using `Resource` makes it serializable (`ResourceSaver.save`) and trivially debuggable in the inspector.
+
+```gdscript
+# autoload/character.gd
+class_name Character
+extends Resource
+
+@export var clade_id: StringName = &"canidae"
+@export var species_id: StringName = &""
+@export var class_id: StringName = &"fangsworn"
+@export var background_id: StringName = &"pack_raised"
+
+@export var stat_method: StringName = &"array" # "array" | "roll"
+@export var stat_pool: Array[int] = [15, 14, 13, 12, 10, 8]
+@export var stat_assign: Dictionary = {} # { "STR": 15, ... }
+@export var stat_history: Array[Dictionary] = [] # [{ vals: [int], ts: int }]
+
+@export var chosen_skills: Array[StringName] = []
+@export var name_text: String = ""
+@export var portrait_style: StringName = &"silhouette"
+
+signal changed # emit on any mutation
+
+func patch(d: Dictionary) -> void:
+ for k in d:
+ set(k, d[k])
+ changed.emit()
+```
+
+Wizard steps connect to `changed` and re-render.
+
+### 2.2 `Content` autoload — replaces `loadData()`
+
+```gdscript
+# autoload/content.gd
+extends Node
+
+var clades: Array = []
+var species: Array = []
+var classes: Array = []
+var backgrounds: Array = []
+
+func _ready() -> void:
+ clades = _load_json("res://data/clades.json")
+ species = _load_json("res://data/species.json")
+ classes = _load_json("res://data/classes.json")
+ backgrounds = _load_json("res://data/backgrounds.json")
+
+func _load_json(path: String) -> Array:
+ var f := FileAccess.open(path, FileAccess.READ)
+ assert(f, "Missing %s" % path)
+ var parsed = JSON.parse_string(f.get_as_text())
+ return parsed if parsed is Array else []
+
+func clade(id: StringName): return _find(clades, id)
+func species_of(id: StringName): return _find(species, id)
+func cls(id: StringName): return _find(classes, id)
+func background(id: StringName): return _find(backgrounds, id)
+
+func _find(arr: Array, id: StringName):
+ for o in arr:
+ if StringName(o.id) == id: return o
+ return null
+```
+
+### 2.3 `Rules` autoload — replaces `data.jsx` constants and helpers
+
+```gdscript
+# autoload/rules.gd
+extends Node
+
+const ABILITIES := ["STR", "DEX", "CON", "INT", "WIS", "CHA"]
+const STANDARD_ARRAY := [15, 14, 13, 12, 10, 8]
+
+const ABILITY_LABELS := {
+ "STR": "Strength", "DEX": "Dexterity", "CON": "Constitution",
+ "INT": "Intellect", "WIS": "Wisdom", "CHA": "Charisma",
+}
+
+const SKILL_ABILITY := {
+ "acrobatics": "DEX", "athletics": "STR",
+ # ... copy verbatim from src/data.jsx
+}
+
+const SKILL_LABEL := { ... } # copy from src/data.jsx
+const SKILL_DESC := { ... } # copy from src/data.jsx
+const LANGUAGES := { ... } # copy from src/data.jsx
+
+const CLASS_CLADE_REC := {
+ "fangsworn": ["canidae", "felidae", "ursidae"],
+ # ... copy verbatim
+}
+
+func ability_mod(score: int) -> int:
+ return int(floor((score - 10) / 2.0))
+
+func signed_str(n: int) -> String:
+ return "+%d" % n if n >= 0 else str(n)
+
+# Validation per step. Mirrors the JS validate() in app.jsx.
+# Returns "" if valid, otherwise an error message.
+func validate_step(step: int, ch: Character) -> String:
+ match step:
+ 0: return "" if ch.clade_id else "Pick a clade."
+ 1: return "" if ch.species_id else "Pick a species."
+ 2: return "" if ch.class_id else "Pick a calling."
+ 3: return "" if ch.background_id else "Pick a background."
+ 4:
+ var n := ch.stat_assign.size()
+ return "" if n == 6 else "Assign all six abilities (%d/6)." % n
+ 5:
+ var c = Content.cls(ch.class_id)
+ var need: int = c.skills_choose if c else 0
+ var got := ch.chosen_skills.size()
+ return "" if got == need else "Pick exactly %d (%d/%d)." % [need, got, need]
+ 6: return "" if ch.name_text.strip_edges() else "Enter a name."
+ return ""
+```
+
+> Tip: copy the full `SKILL_DESC`, `LANGUAGES`, and `TRAIT_READING` dictionaries straight from `src/data.jsx` — the JS object literal is valid as a GDScript dictionary with two find/replace passes (`:` is fine; you mostly need to swap `\n` strings and remove backticks).
+
+---
+
+## 3. Mapping React concepts to Godot
+
+| React / web concept | Godot 4.6 equivalent |
+|------------------------------------|--------------------------------------------------------------|
+| Component (`StepClade`) | `Control`-derived scene + script |
+| Props (`{ state, set }`) | Scene exports + a shared `Character` resource reference |
+| `useState` | Plain `var` on the script + `signal` for change notifications |
+| `useEffect` on dep change | Connect to `Character.changed`; gate work with diffs |
+| `useMemo` | Cache in `_ready()` or recompute in `_process` only on dirty |
+| Conditional render | `node.visible = ...` (avoid `queue_free` for steps; reuse) |
+| `