diff --git a/docs/UNDO_REDO_FEATURE.md b/docs/UNDO_REDO_FEATURE.md
index 1d09f01..affb385 100644
--- a/docs/UNDO_REDO_FEATURE.md
+++ b/docs/UNDO_REDO_FEATURE.md
@@ -1,89 +1,254 @@
-# Undo/Redo Feature Plan
+# Undo / Redo Feature Plan (v2)
-## Overview
-Add undo/redo functionality to allow users to fix mistakes and reverse unintended changes. State snapshots will be saved to browser localStorage.
+## Goals
+Give every editor surface a familiar undo/redo flow so users can fix mistakes without leaving the editor.
-## Complexity
-**Moderate** - Estimated 1-2 hours to implement
+### Product goals
+1. `Ctrl+Z` / `Ctrl+Shift+Z` (and the equivalent macOS keys) feel natural everywhere in the editor.
+2. Reverse any user-driven edit to **local design state** — adding / deleting layers, dragging, resizing, color changes, mode switches, artwork loads, metadata edits.
+3. Do NOT undo network actions (publish, block, follow, tip), identity changes, or community-modal state — those are out-of-scope and irreversible.
-## Key Components
+### Non-goals
+- Persisting the undo stack to localStorage (see "Storage strategy" — memory-only in v1).
+- Selective undo of a specific past action.
+- Time-travel / branched history.
+- Multi-tab synced undo.
+- Sub-action granularity inside text editing (text input already has the browser's native undo when focused).
-### 1. Undo/Redo Stack
-- Maintain two stacks: `undoStack` and `redoStack`
-- Each stack stores snapshots of the entire `state` object
-- Limit stack size to 20-50 snapshots to avoid filling localStorage (~2-5MB usage)
+---
-### 2. State Snapshot Capture
-Save snapshots at major action points (not on every keystroke):
-- Project import/load
-- Add/delete/modify layers
-- Design image URL changes
-- Design property edits (fit, zoom, x, y, rotate)
-- Metadata edits (title, category, imdbId)
-- Add/remove disc designs in template slots
+## What snapshot includes
-### 3. Storage Strategy
-- Serialize state snapshots using `JSON.stringify()`
-- Store in localStorage key like `undo-stack` and `redo-stack`
-- Include timestamp with each snapshot for debugging
-- Implement size checks to prevent localStorage overflow
+A snapshot is a deep-cloned subset of `state`. The fields are split into editor-state (snapshotted) and ambient/community state (not snapshotted).
-### 4. Core Functions Needed
-```javascript
-function saveUndoSnapshot(label)
- // Save current state to undo stack, clear redo stack
-
-function undo()
- // Restore previous state from undo stack, move current to redo stack
-
-function redo()
- // Restore state from redo stack, move current to undo stack
-
-function updateUndoRedoUI()
- // Enable/disable undo/redo buttons based on stack availability
+### Snapshotted (editor state)
+- `appMode` (`'template' | 'designer'`)
+- `templateMode` (`'cover' | 'disc' | 'jewel' | 'customart'`)
+- `savedMetaByMode` (the parked metadata for the other app mode)
+- `meta` (title, identifiers, customTag, category, language, omdbApiKey)
+- `preset`, `panelW`, `trimH`, `spineW`, `bleed`, `safe`, `artworkMode` (cover-mode dimensions + wrap mode)
+- `slots`, `combined` (cover artwork)
+- `discs`, `discMeta`, `discSpineLabels` (disc-sheet artwork + per-disc metadata + spine labels)
+- `jewelInserts`, `jewelSpineLabels` (jewel artwork + spine labels)
+- `customArt` (canvas width, height, background, preset)
+- `discDesign` (single-disc-designer state)
+- `layers` (the entire layer array, including per-layer fill / stroke / clip / shape data)
+- `selectedLayerId` (so undoing restores the active selection)
+- `editingDTag`, `editingKind`, `editingEventId`, `editingTitle` (edit-in-place context — yes, snapshot, so cancelling out of edit mode and undoing brings the context back)
+- `showGuides`, `showBleed`, `showCrops`, `printGuides`, `printCrops`, `zoom` (display toggles — debatable, but cheap to include so users can undo "I turned off crops")
+
+### NOT snapshotted (ambient / community / cache)
+- `community.*` (signed-in identity, follows, mute list, profile cache, relay list, etc.)
+- `_feedCache`, `_zapCache`, `_profilePages`, `_blockedAuthors`, `_brokenImages` set
+- `_layerClipboard` (clipboard is its own session ring buffer)
+- `_publishConfirmTarget`, `_shapePathTarget`, `_tipTarget`, `_closeProjectTarget` (transient modal state)
+- `_modalZTop`, `_aspectLocked` (UI preferences)
+- `_discImportTargetSlot`, `_prevTemplateMode` (transient bookkeeping)
+- `darkMode` (a user preference, not a design choice)
+
+### Cloning strategy
+Use `structuredClone(snapshot)` for both push and restore. Falls back to `JSON.parse(JSON.stringify(...))` if `structuredClone` is somehow unavailable. Both are fast for our state size (~30–500 KB per snapshot depending on layer count). No prototype chain to preserve, no Maps/Sets to lose.
+
+---
+
+## When to push a snapshot
+
+Pushes happen on **coarse, completed edits**, not on every keystroke or slider tick. The list:
+
+### Layer lifecycle
+- `addImageLayer` / `addTextLayer` / `addColorLayer` / `addShapeLayer` succeed → push.
+- Delete layer → push (before deletion so undo restores the layer).
+- Duplicate layer → push.
+- Paste / Paste all from the clipboard → push (after the paste lands).
+- Cut → push (before removing, so undo restores it).
+
+### Layer geometry / transforms
+- Drag end (`pointerup` on a layer wrapper after a drag) → push **once** per drag, not per pointer-move.
+- W / H / X / Y / Rotate / Opacity number inputs → debounced 400 ms after last `input` event.
+- Aspect-lock toggle does NOT trigger a snapshot (it's a UI preference).
+
+### Layer paint properties
+- Color picker `change` event (the commit, not the live `input` drag) → push.
+- Linear / radial gradient angle / center / radius sliders → debounced 400 ms.
+- Stroke type, width, dash → debounced 300 ms for width; immediate for type / dash dropdowns.
+- Shape kind-specific params (corner radius, sides, star points + inner ratio, line stroke width) → debounced 400 ms.
+
+### SVG layer (`type:'svg'`)
+- Per-element fill / stroke color picker `change` → push per element.
+- Per-element stroke-width input → debounced 400 ms.
+- Replace SVG (upload) → push immediately after the new SVG is parsed and applied.
+
+### Clip / mask
+- Clip kind dropdown change → push.
+- Inline-shape clip param inputs → debounced 400 ms.
+- Layer-ref clip target change → push.
+
+### Mode / structural
+- App mode flip (`switchAppMode('template' | 'designer')`) → push **before** the swap so undo returns to the previous mode + its parked metadata.
+- Template sub-mode flip (cover/disc/jewel/customart) → push before the change.
+- Close project / Clear images → push **before** the wipe so undo restores everything.
+- Load project from file → push before the load.
+- Fork (load row payload into editor) → push before the load.
+- Edit-in-place enter / Cancel edit → push (the editing context is part of state).
+
+### Artwork loads
+- Slot artwork URL committed (cover front/spine/back, disc top/bottom, jewel front/tray, combined wrap, disc-designer image) → push.
+
+### Metadata
+- Title / IMDb / UPC / TheTVDB / MusicBrainz / Custom tag inputs → debounced 600 ms.
+- Category / language `change` → push.
+- Spine label inputs → debounced 600 ms.
+
+### Custom Art canvas
+- Canvas size preset change → push.
+- Custom width / height inputs → debounced 400 ms.
+- Background color picker `change` → push.
+
+### NOT a snapshot trigger
+- Selection (`selectedLayerId` only) — selection is reflected in snapshots but selection-only changes don't push.
+- Layer reorder (Move forward / Move back) — debatable; v1 says **yes** push.
+- Modal opens / closes.
+- Community-feed tab switches, scrolling, filter changes.
+- Hover, zoom, pan.
+- Layer lock toggle (debatable — push to be safe).
+
+---
+
+## How undo / redo behave
+
+```
+pushHistory(label)
+ ├── deep-clone the snapshotted slice of state
+ ├── undoStack.push({ label, snapshot, at: Date.now() })
+ ├── trim undoStack to UNDO_MAX entries
+ └── clear redoStack
+
+undo()
+ └── if undoStack.length > 0:
+ current = current snapshotted slice
+ redoStack.push({ label: 'redo→' + …, snapshot: current })
+ restore from undoStack.pop()
+ rerender + sync inputs + paint banners
+
+redo()
+ └── mirror operation
```
-### 5. UI Changes
-- Add "Undo" button in toolbar/header
-- Add "Redo" button next to undo
-- Show disabled state when no undo/redo available
-- Optional: Show keyboard shortcuts (Ctrl+Z, Ctrl+Shift+Z)
-- Optional: Show snapshot label on hover
+`UNDO_MAX = 50` for v1. In-memory only — see Storage below.
-### 6. Integration Points
-Modify existing functions to call `saveUndoSnapshot()` after:
-- `importDiscDesignFromPayload()` - after design imported
-- `addLayer()` - after layer created
-- Layer deletion handlers
-- Design image URL updates
-- Metadata input change handlers
-- Template preset changes
+### Restoring a snapshot
+Restoration replaces the snapshotted fields on `state` and re-runs the sync/render pipeline:
+1. `Object.assign(state, snapshot)` (or per-field assignment for the snapshotted set).
+2. Re-derive any computed-after-restore state (we don't have much — `normalizeDiscState`, `normalizeJewelState`, `paintEditingBanner`, `applyTargetMode` on the active target, etc.).
+3. Call `syncInputs()` + `syncAllSlotInputs()` + `syncAllDiscInputs()` + `syncAllJewelInputs()` + `syncCombinedInputs()` + `syncDesignerInputs()` + `syncModeTabsUI()`.
+4. Call `render()`.
-## Performance Considerations
-- Serializing/deserializing state is fast (~1-5ms)
-- No performance impact on normal operations
-- localStorage access is synchronous and fast
-- Consider debouncing rapid metadata edits
+If the snapshot's `appMode` differs from the current `appMode`, the mode tabs UI flips along with the rest.
-## Limitations
-- Browser localStorage only (not synced across devices/browsers)
-- Limited to current session if user closes browser
-- Large image URLs could impact stack size
-- No undo for external Nostr publishes (data already on network)
+---
-## Future Enhancements
-- IndexedDB for larger storage capacity
-- Undo history UI showing snapshots
-- Selective undo (undo specific action)
-- Cloud sync of undo history
-- Keyboard shortcuts configuration
+## Keyboard shortcuts
-## Testing Checklist
-- [ ] Undo works after layer add
-- [ ] Undo works after image URL change
-- [ ] Undo works after metadata edit
-- [ ] Redo restores undone actions
-- [ ] Stack limits prevent localStorage overflow
-- [ ] Buttons disable when stacks empty
-- [ ] Page refresh preserves undo stack (if kept in localStorage)
-- [ ] Works across template/designer mode switches
+- `Ctrl+Z` / `Cmd+Z` → undo.
+- `Ctrl+Shift+Z` / `Cmd+Shift+Z` → redo. Also accept `Ctrl+Y` (Windows muscle memory).
+- **Bypass when an input is focused**: if `document.activeElement` is an ``, `