diff --git a/src/lib/napkin/MathMap.svelte b/src/lib/napkin/MathMap.svelte
new file mode 100644
index 0000000..2750924
--- /dev/null
+++ b/src/lib/napkin/MathMap.svelte
@@ -0,0 +1,1331 @@
+
+
+
+
+
+
+
+
+
+
+
(searchOpen = true)}
+ onblur={() => setTimeout(() => (searchOpen = false), 150)}
+ oninput={() => (active = 0)}
+ onkeydown={onsearchkey}
+ />
+ {#if searchOpen && results.length}
+
+ {#each results as c, i (c.key)}
+ {@const s = statusByKey.get(c.key) ?? 'not_started'}
+ -
+
+
+ {/each}
+
+ {/if}
+
+
+
+
+
+ {#each Object.entries(status) as [s, v] (s)}
+ {STATUS_LABEL[s as MathStatus]}
+ {/each}
+
+
+
+
+
+ {#if !mobile && cw}
+
+ {/if}
+
+ {#if sel}
+ {@const s = statusByKey.get(sel.key) ?? 'not_started'}
+ {@const Icon = status[s].icon}
+ {@const p = partByKey.get(sel.partKey)}
+
+
+ {/if}
+
+
+
diff --git a/src/lib/napkin/map.test.ts b/src/lib/napkin/map.test.ts
new file mode 100644
index 0000000..aae72ea
--- /dev/null
+++ b/src/lib/napkin/map.test.ts
@@ -0,0 +1,124 @@
+import { describe, expect, it } from 'vitest';
+import { buildMapLayout, MAP_GRID, PART_ACCENT, PART_LINKS } from './map';
+
+// Chapter counts per part, as in the seeded Napkin.
+const COUNTS: Record