What to build: Undo + redo in the deck editor via a snapshot stack (not command pattern). Every mutation is one step. History is in-memory only (cleared on reload) and spans saves — the editor's explicit save button + dirty flag make this purely frontend work. Bindings: Ctrl+Z / Ctrl+Shift+Z, plus visible undo/redo buttons (these later move into the mobile sticky bar, ticket 15).
Blocked by: None — can start immediately.
Status: fixed
2026-08-01: Implemented as a snapshot stack in static/editor.js (snapshot/pushHistory/applySnapshot/undo/redo, cap 200 entries). A snapshot is identity + format + banlist version + a copy of the decklist; every mutation (qty change, identity change, format change, ban list change) pushes exactly one snapshot before it lands and clears the redo branch, and no-op mutations (clamped at 0 or at the deck limit, or re-picking the current select value) push nothing. History is module state only — a reload clears it — and nothing touches it on save, so undo spans saves and re-dirties the deck. Bindings are Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z (suppressed only while a *typing* field has focus — textarea, contenteditable, or a text-flavoured <input> — so the deck-name and search boxes keep their native undo while a focused checkbox or select does not swallow the shortcut) plus #undo-btn/#redo-btn in the editor header, wrapped in #history-controls for ticket 15 to relocate into the mobile sticky bar. Both buttons carry an aria-label, since their glyphs (↶/↷) are their only text; their disabled state is refreshed from updateHistoryButtons() on init and after every push/undo/redo, and button:disabled in style.css already dims them. The deck name is deliberately outside the snapshot (native input undo owns it). Drive-by: the identity select now calls markDirty(), which it previously skipped.
2026-08-01: Code reviewed. Found one medium-severity dirty-state regression, recorded in todo.md: after saving, undoing, then redoing to the saved deck state, applySnapshot() still marks the deck dirty and the unload warning appears. Python, Rust, and JavaScript syntax checks otherwise passed.