diff --git a/BACKLOG.md b/BACKLOG.md
new file mode 100644
index 0000000..4d72dbc
--- /dev/null
+++ b/BACKLOG.md
@@ -0,0 +1,244 @@
+# BACKLOG.md — Improvement Tasks for Future Agents
+
+This document captures prioritized, actionable improvement tasks identified
+through a comprehensive code analysis. Each task includes the problem, affected
+files/lines, and a concrete implementation plan.
+
+**Ground rules for agents working on these tasks:**
+- Run `node .claude/hooks/run-tests.js` before and after every change
+- Add tests in `tests.html` for any new logic
+- Keep the zero-dependency, no-build-step constraint
+- Do not add npm packages or bundlers
+- All JS must work without transpilation (no JSX, no ES modules)
+
+---
+
+## Task 1 — Add React Error Boundary and Crash Recovery
+
+**Priority:** HIGH | **Effort:** Low | **Category:** Reliability
+
+**Problem:** No error boundary exists. If any React component throws, the entire
+app goes blank with no recovery path. Users must hard-refresh and may not
+understand what happened.
+
+**Files:**
+- `index.html` — wrap App component (around line 1295)
+
+**Implementation:**
+1. Add an `ErrorBoundary` class component before the App definition:
+ ```js
+ class ErrorBoundary extends React.Component {
+ constructor(props) { super(props); this.state = { hasError: false, error: null }; }
+ static getDerivedStateFromError(error) { return { hasError: true, error: error }; }
+ componentDidCatch(error, info) { console.error('App crash:', error, info); }
+ render() {
+ if (this.state.hasError) {
+ return React.createElement('div', { style: { padding: 40, textAlign: 'center' } },
+ React.createElement('h2', null, 'Something went wrong'),
+ React.createElement('p', null, this.state.error && this.state.error.message),
+ React.createElement('button', { onClick: function() { location.reload(); } }, 'Reload'),
+ React.createElement('button', { onClick: function() { localStorage.clear(); location.reload(); } }, 'Reset all data')
+ );
+ }
+ return this.props.children;
+ }
+ }
+ ```
+2. Wrap `` in `` in the `ReactDOM.createRoot` call
+3. Add a test: render ErrorBoundary with a child that throws, assert fallback UI appears
+
+---
+
+## Task 2 — Handle localStorage Quota/Disabled Errors
+
+**Priority:** HIGH | **Effort:** Low | **Category:** Data Safety
+
+**Problem:** `lsSave` (line ~309-312) and `srsSave` (line ~472-476) silently
+catch exceptions. If localStorage is full or disabled, users lose progress with
+no warning.
+
+**Files:**
+- `index.html` — `lsSave`, `srsSave`, and all `localStorage.setItem` calls
+
+**Implementation:**
+1. Create a `safeSave(key, value)` helper that wraps `localStorage.setItem` in
+ try/catch and returns `{ ok: boolean, error: string|null }`
+2. On failure, show a non-blocking notification bar at the top of the page:
+ "Unable to save progress — storage may be full or disabled"
+3. Add a `storageAvailable()` check at app startup; show a persistent warning
+ if localStorage is not available
+4. Replace all direct `localStorage.setItem` calls with `safeSave`
+5. Add test: mock `localStorage.setItem` to throw, verify `safeSave` returns error
+
+---
+
+## Task 3 — Add Accessibility Attributes
+
+**Priority:** HIGH | **Effort:** Medium | **Category:** Accessibility
+
+**Problem:** No `aria-label`, `role`, or `tabindex` attributes on interactive
+elements. Screen reader users cannot navigate the app.
+
+**Files:**
+- `index.html` — all React component `createElement` calls
+
+**Implementation (incremental — can be split across multiple PRs):**
+1. **Speak buttons:** Add `aria-label="Listen to pronunciation"` to all TTS buttons
+2. **Quiz buttons:** Add `role="button"` and `aria-label` with the answer text
+3. **Feedback messages:** Add `aria-live="polite"` to exercise result text
+4. **Navigation:** Add `role="navigation"` to header, `role="main"` to content area
+5. **Review cards:** Make card flippable via Enter/Space key (add `tabIndex={0}`
+ and `onKeyDown` handler)
+6. **Overview calendar:** Add `aria-label` to day cells with day number and status
+7. Verify with a screen reader or axe-core
+
+---
+
+## Task 4 — Sanitize SVG in dangerouslySetInnerHTML
+
+**Priority:** MEDIUM | **Effort:** Low | **Category:** Security
+
+**Problem:** Line ~579 uses `dangerouslySetInnerHTML` for stroke order SVGs
+fetched via `fetch()`. While currently fetched from a known source, this is a
+latent XSS vector.
+
+**Files:**
+- `index.html` — `CharCard` component (line ~579), `loadStrokeOrderSvg` (line ~343)
+
+**Implementation:**
+1. Add a `sanitizeSvg(raw)` function that:
+ - Parses with DOMParser
+ - Removes `