(fn, delay)` generic utility
- - `debouncedSave = debounce(() => { collect all node knowledge, call saveState }, 500)`
-
-5. **4-state knowledge cycling** (from research Pattern 2):
- - Replace Phase 10's existing 3-state cycle with the new 4-state array
- - Find the existing `cy.on('tap', 'node', ...)` handler and update it:
- - Get current knowledge from `node.data('knowledge')`
- - Find index in KNOWLEDGE_STATES, advance by 1 (modulo length)
- - Set via `node.data('knowledge', next)` -- Cytoscape auto-refreshes styles
- - Call `debouncedSave()`, `updateSidebar()`, `updatePrompt()`, `updateStats()`
- - Update Cytoscape stylesheet to use 4 data-driven selectors instead of 3:
- - `node[knowledge = "unknown"]` -> border-color: #f85149
- - `node[knowledge = "familiar"]` -> border-color: #d29922
- - `node[knowledge = "confident"]` -> border-color: #58a6ff
- - `node[knowledge = "mastered"]` -> border-color: #3fb950
- - Also add backward compatibility selectors for legacy data:
- - `node[knowledge = "know"]` -> border-color: #3fb950 (same as mastered)
- - `node[knowledge = "fuzzy"]` -> border-color: #d29922 (same as familiar)
-
-6. **Bulk state operations** (for preset buttons like "Set All Unknown"):
- - Wrap bulk `node.data()` calls in `cy.batch(() => { ... })` to limit to 1 redraw
- - Add "Set All" buttons for each knowledge state or an "All Unknown"/"All Mastered" pair
-
-7. **State restore on load:**
- - After Cytoscape init + layout complete, call `loadState(TOPIC_DATA.metadata.slug)`
- - If state exists: apply knowledge to nodes in `cy.batch()`, set learning mode
- - If null: keep defaults (nodes start as whatever their JSON default is)
- - CRITICAL: Render graph first, apply persisted state after (research Pitfall 6)
-
-8. **Stats display for 4 states:**
- - Update `updateStats()` to count and display: N unknown, N familiar, N confident, N mastered
- - Use the KNOWLEDGE_COLORS for stat badges
-
-9. **JSON export fallback** (from research Pattern 7, EXPL-05):
- - `exportStateAsJSON()` function: collect knowledge map, create Blob, trigger download as `{slug}-progress.json`
- - Uses Blob + URL.createObjectURL + anchor click pattern
- - Always available as secondary action; more prominent when `!storageAvailable`
-
-**In styles.css, add:**
-
-10. **4 knowledge state CSS utility classes:**
- - `.knowledge-unknown`, `.knowledge-familiar`, `.knowledge-confident`, `.knowledge-mastered`
- - Each sets `--state-color` CSS variable to the corresponding color
- - Badge styling for sidebar concept list items
-
-**CRITICAL: Do NOT break Phase 10 functionality.** This is additive. The graph, CoSE layout, sidebar, presets, tooltips, and basic prompt generation must all continue working. Do NOT remove any existing event handlers or DOM manipulation -- extend them.
-
-**Anti-patterns from research to AVOID:**
-- Do NOT save on every pointer move -- only on knowledge change + debounce
-- Do NOT block initial render on localStorage load
-- Do NOT use topic-specific JavaScript -- all code works generically via TOPIC_DATA
-- Do NOT add custom pointer event listeners on #cy -- Cytoscape handles its own events
+ **Step 1: Update template.html**
+
+ Modify `src/explorer/template/template.html` with the following additions. Preserve ALL existing structure -- only ADD new elements and modify the stats section.
+
+ 1. **Viewport meta tag** -- Ensure this exists in ``:
+ ```html
+
+ ```
+
+ 2. **Learning mode selector** -- Add AFTER the stats div inside `.header-controls`:
+ ```html
+
+
+
+
+
+ ```
+ Note: Default active mode is "guided" in HTML; the explorer.ts init will read from localStorage and may switch to a different mode.
+
+ 3. **Progress bar** -- Add BEFORE `.main` div (between header and main content):
+ ```html
+
+
+
0/0 concepts progressed
+
+
+ 0 unknown
+ 0 familiar
+ 0 confident
+ 0 mastered
+
+
+ ```
+
+ 4. **Update stats section** -- Replace the 3-state stats with 4-state:
+ ```html
+
+ 0 unknown
+ 0 familiar
+ 0 confident
+ 0 mastered
+
+ ```
+
+ 5. **Update sidebar instructions** -- Change the cycling description:
+ ```html
+ Click a node to cycle knowledge: Unknown → Familiar → Confident → Mastered.
+ Drag nodes to arrange. Use presets to focus. Choose a learning mode above.
+ The prompt below updates live based on your gaps and selected mode.
+ ```
+
+ 6. **Update action buttons** -- Replace 3-state set-all buttons with 4-state + Reset All:
+ ```html
+
+
+
+
+
+
+
+ ```
+
+ 7. **Update prompt panel copy button** -- Add an id if it doesn't have one. Also add the `onclick` will be wired in explorer.ts. Keep existing structure.
+
+ 8. **Last studied timestamp** -- Add below the prompt panel header:
+ ```html
+
+ ```
+
+ **Step 2: Update styles.css**
+
+ Add the following CSS rules to `src/explorer/template/styles.css`. Preserve ALL existing styles -- only ADD new rules.
+
+ 1. **4-state knowledge badge colors** (update or add these):
+ ```css
+ .stat-unknown { color: #f85149; }
+ .stat-familiar { color: #d29922; }
+ .stat-confident { color: #58a6ff; }
+ .stat-mastered { color: #3fb950; }
+
+ .unknown-bg { background: rgba(248,81,73,0.15); color: #f85149; }
+ .familiar-bg { background: rgba(210,153,34,0.15); color: #d29922; }
+ .confident-bg { background: rgba(88,166,255,0.15); color: #58a6ff; }
+ .mastered-bg { background: rgba(63,185,80,0.15); color: #3fb950; }
+ ```
+
+ 2. **Touch-action on Cytoscape container**:
+ ```css
+ #cy {
+ touch-action: none;
+ }
+ ```
+ Add to the existing `#cy` rule (do not create a duplicate selector -- merge into the existing one).
+
+ 3. **Learning mode selector**:
+ ```css
+ .mode-selector {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+ }
+ .mode-btn {
+ background: var(--surface);
+ color: var(--text-secondary);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 4px 12px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ }
+ .mode-btn:hover {
+ border-color: var(--text-secondary);
+ }
+ .mode-btn.active {
+ background: var(--accent);
+ color: #fff;
+ border-color: var(--accent);
+ }
+ ```
+
+ 4. **Progress bar**:
+ ```css
+ .progress-container {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 16px;
+ border-bottom: 1px solid var(--border);
+ flex-wrap: wrap;
+ }
+ .progress-bar {
+ flex: 1;
+ min-width: 100px;
+ max-width: 300px;
+ height: 8px;
+ background: var(--surface);
+ border-radius: 4px;
+ overflow: hidden;
+ }
+ .progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #58a6ff, #3fb950);
+ border-radius: 4px;
+ transition: width 0.3s ease;
+ width: 0%;
+ }
+ .progress-text {
+ font-size: 12px;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ }
+ .progress-expand-btn {
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-size: 10px;
+ padding: 2px 4px;
+ }
+ .progress-breakdown {
+ width: 100%;
+ display: flex;
+ gap: 8px;
+ padding-top: 4px;
+ }
+ .breakdown-item {
+ font-size: 11px;
+ padding: 2px 8px;
+ border-radius: 4px;
+ }
+ ```
+
+ 5. **Last studied timestamp**:
+ ```css
+ .last-studied {
+ font-size: 11px;
+ color: var(--text-secondary);
+ margin-left: auto;
+ }
+ ```
+
+ 6. **Responsive layout (mobile)**:
+ ```css
+ @media (max-width: 768px) {
+ .main {
+ flex-direction: column;
+ }
+ .sidebar {
+ width: 100%;
+ max-height: 40vh;
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ order: 1;
+ }
+ .graph-container {
+ order: 0;
+ min-height: 50vh;
+ }
+ .prompt-panel {
+ order: 2;
+ }
+ .header-controls {
+ flex-wrap: wrap;
+ gap: 4px;
+ }
+ .mode-selector {
+ width: 100%;
+ justify-content: center;
+ }
+ .progress-container {
+ flex-wrap: wrap;
+ }
+ .progress-bar {
+ min-width: 80px;
+ }
+ }
+ ```