diff --git a/docs/superpowers/specs/2026-07-06-persistence-editors-design.md b/docs/superpowers/specs/2026-07-06-persistence-editors-design.md
new file mode 100644
index 0000000..4eab54e
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-06-persistence-editors-design.md
@@ -0,0 +1,266 @@
+# Persistence, Secure Image Handling, and Scenario Editors Design
+
+## Purpose
+
+Plan 3 of the four-plan BattleForge build. Closes the gap between the pure-PHP `Domain` layer (Plans 1 and 2) and the in-browser experience by adding:
+
+- Anonymous-browser persistence of scenarios and in-progress matches (no user accounts, no cross-device, no server-side storage of game state).
+- Server-rendered editor pages for teams and battlefields, with the domain's `Scenario` and `ScenarioValidator` enforcing every rule.
+- A secure image-upload pipeline (content-sniffed, size-limited, dimension-limited, session-scoped).
+- A minimal plain-PHP web stack with the cross-cutting security concerns the spec demands (CSRF, output escaping, content-validated uploads).
+
+## Success Criteria
+
+Plan 3 succeeds when a new user, without developer assistance and with no global state, can:
+
+1. Visit `http://localhost:8000/`, see a home page that lists their recent scenarios, and start a new one.
+2. Use the team editor to build two teams of 3-6 units each, choosing archetypes, stats, abilities, names, and optional custom images.
+3. Use the battlefield editor to choose dimensions, paint terrain, place deployment zones, and place an objective (when the victory condition requires it).
+4. Validate the assembled scenario server-side; see clear errors when something is invalid.
+5. Upload a custom image and have it appear in the team editor.
+6. "Start a match" from a saved scenario and have the initial `MatchState` round-trip through the browser.
+7. Refresh the page, lose the connection, or come back tomorrow and find their saved scenarios still in their browser (no data loss from the user's perspective).
+8. Reject forged cross-site requests, accept only image types the spec allows, and never let a user access another user's uploads.
+
+## Out of Scope (deferred to Plan 4 or later)
+
+- Hot-seat battle interface: Plan 3 ships a placeholder page that reads the assembled `MatchState` from `localStorage` and renders a "Battle interface coming in Plan 4" stub.
+- Bundled scenarios (the three ready-made scenarios in the spec): Plan 4.
+- Real-time interaction, drag-and-drop, or any other JS surface beyond the single fetch-based grid editor and the small `localStorage` helper.
+- Server-side storage of scenarios, matches, or any game state.
+- User accounts, sessions beyond the CSRF cookie, or any per-user server-side state.
+- Mobile-native, PWA, offline service workers, or any non-server-rendered path.
+- Antivirus scanning or upload rate limiting.
+
+## In Scope
+
+### Persistence
+
+- Scenarios are JSON-serialized in the browser's `localStorage`, keyed as `scenario:{id}`.
+- In-progress matches are JSON-serialized in `localStorage`, keyed as `match:current`.
+- The server never sees a `Scenario` or `MatchState` after the editor POST completes. Each form submission is validated and acknowledged; the browser then writes to `localStorage` on its own.
+- The home page lists scenarios by enumerating `localStorage` keys; no server call.
+- "Resume in-progress match" reads `match:current` and shows a banner on the home page.
+
+### Team Editor
+
+- One form per scenario, with sections for: meta (id, name), team A, team B, victory condition.
+- Each team section has 3-6 unit rows; rows are added and removed client-side (DOM manipulation, no fetch).
+- Each unit row has: archetype dropdown, display name, image upload button + hidden URL field, four stat inputs (with `min`/`max` from the chosen archetype's template), and an abilities multi-select (limited to the chosen archetype's allowlist).
+- The archetype JSON is served from `/assets/archetypes.json` and includes the four `ArchetypeTemplate` definitions.
+- A new "Upload" form submits a single image to `POST /assets/upload` and writes the returned URL into the unit row's hidden `image` field. The unit-editor form is not submitted as part of the upload.
+- Victory condition: a radio pair (`EliminateAll` / `HoldObjective`). When `HoldObjective` is selected, an input for `holdRoundsRequired` (1-10) appears.
+- Submitting the form POSTs the full draft to `POST /scenarios/{id}/edit/team`. The server parses, builds a `ScenarioDraft`, runs `ScenarioValidator::validate()`, and on failure re-renders the form with errors inline.
+
+### Battlefield Editor
+
+- One page per scenario. Width/height inputs (8-16 each) at the top.
+- Terrain palette: one button per `Terrain` enum value. The selected button is the "current paint."
+- Grid: an HTML `