diff --git a/islands/StudyDeck.tsx b/islands/StudyDeck.tsx
deleted file mode 100644
index c3068f9..0000000
--- a/islands/StudyDeck.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-// reveal.js touches window/document at init — render on the client only.
-export const client = "only";
-
-import { Deck, Markdown } from "@revealjs/react";
-import RevealHighlight from "reveal.js/plugin/highlight";
-import "reveal.js/reveal.css";
-import "reveal.js/theme/black.css";
-import "reveal.js/plugin/highlight/monokai.css";
-
-import { decks, type DeckName } from "./decks";
-
-/**
- * An embedded reveal.js slide deck for drilling a DS/algorithms topic.
- * Usage in any MDX page: ``.
- */
-export default function StudyDeck({ deck }: { deck: DeckName }) {
- const markdown = decks[deck];
- if (!markdown) {
- return (
-
- Unknown study deck "{deck}". Available: {Object.keys(decks).join(", ")}.
-
- );
- }
- return (
-
-
- {markdown}
-
-
- );
-}
diff --git a/islands/decks/arrays-and-strings.md b/islands/decks/arrays-and-strings.md
deleted file mode 100644
index a4184a7..0000000
--- a/islands/decks/arrays-and-strings.md
+++ /dev/null
@@ -1,56 +0,0 @@
-## Arrays & Strings
-
-Ordered collections. Arrays are **mutable**, strings are **immutable** — every "edit" to a string builds a new one.
-
----
-
-## Cost model
-
-| Operation | Array | String |
-| --- | --- | --- |
-| Append / pop at end | O(1)* | O(n) |
-| Insert / delete in middle | O(n) | O(n) |
-| Random access | O(1) | O(1) |
-| Membership check | O(n) | O(n) |
-
-*amortized — occasional resize, constant on average.
-
----
-
-## Recognition
-
-- Contiguous data + index arithmetic → array patterns apply
-- Building a string piece by piece → collect parts, join once
-- "In-place" requirement → think swap / overwrite, not rebuild
-
----
-
-## String building
-
-```js
-// O(n^2): each += copies the whole string
-let s = "";
-for (const ch of parts) s += ch;
-
-// O(n): buffer then join
-const buf = [];
-for (const ch of parts) buf.push(ch);
-const out = buf.join("");
-```
-
----
-
-## Recall
-
-- Why is append amortized O(1) and not plain O(1)?
-- What does immutability cost when you "modify" one character?
-- Which patterns sit on top of arrays? (two pointers, sliding window, prefix sum)
-
----
-
-## Drill
-
-- 1480 Running Sum of 1d Array
-- 217 Contains Duplicate
-- 189 Rotate Array
-- 977 Squares of a Sorted Array
diff --git a/islands/decks/binary-search.md b/islands/decks/binary-search.md
deleted file mode 100644
index 22d0d81..0000000
--- a/islands/decks/binary-search.md
+++ /dev/null
@@ -1,63 +0,0 @@
-## Binary Search
-
-Halve a **monotonic** search space each step → O(log n).
-
----
-
-## Recognition
-
-- Sorted input + "find / insert position"
-- "Minimize the maximum" / "first value where condition flips"
-- Answer itself is numeric and checkable → binary search **on the answer**
-
----
-
-## Template
-
-```js
-function search(nums, target) {
- let lo = 0;
- let hi = nums.length - 1;
- while (lo <= hi) {
- const mid = lo + ((hi - lo) >> 1);
- if (nums[mid] === target) return mid;
- if (nums[mid] < target) lo = mid + 1;
- else hi = mid - 1;
- }
- return -1; // lo is the insert position
-}
-```
-
----
-
-## First-true boundary
-
-```js
-// smallest index where ok(i) is true; ok is false...false true...true
-let lo = 0;
-let hi = n; // exclusive; n means "never true"
-while (lo < hi) {
- const mid = lo + ((hi - lo) >> 1);
- if (ok(mid)) hi = mid;
- else lo = mid + 1;
-}
-return lo;
-```
-
-Most "hard" binary searches are this shape in disguise.
-
----
-
-## Pitfalls
-
-- Off-by-one: pick `lo <= hi` **or** `lo < hi` and stay consistent
-- `mid = lo + ((hi - lo) >> 1)` avoids overflow and floats
-- No progress → infinite loop: every branch must shrink the range
-
----
-
-## Recall
-
-- What property must the search space have? (not "sorted" — *monotonic*)
-- Where does `lo` land when the target is absent?
-- When do you search the answer space instead of the array?
diff --git a/islands/decks/hashing.md b/islands/decks/hashing.md
deleted file mode 100644
index 92f4c68..0000000
--- a/islands/decks/hashing.md
+++ /dev/null
@@ -1,72 +0,0 @@
-## Hashing
-
-Trade O(n) memory for **O(1) average lookup**. The workhorse behind "have I seen this before?".
-
----
-
-## Recognition
-
-- "Does X exist?" / "How many times?" → `Set` / `Map`
-- Pair matching (complement lookup) → map value → index
-- Grouping by a derived key → map key → bucket
-- Counting characters → frequency map
-
----
-
-## Template — complement lookup (LC 1)
-
-```js
-function twoSum(nums, target) {
- const seen = new Map(); // value -> index
- for (let i = 0; i < nums.length; i++) {
- const need = target - nums[i];
- if (seen.has(need)) return [seen.get(need), i];
- seen.set(nums[i], i);
- }
-}
-```
-
-One pass: check for the partner **before** inserting yourself.
-
----
-
-## Template — grouping (LC 49)
-
-```js
-function groupAnagrams(strs) {
- const groups = new Map();
- for (const s of strs) {
- const key = [...s].sort().join("");
- if (!groups.has(key)) groups.set(key, []);
- groups.get(key).push(s);
- }
- return [...groups.values()];
-}
-```
-
-The whole trick is **choosing the canonical key**.
-
----
-
-## Pitfalls
-
-- Frequency compare needs both directions (surplus *and* deficit)
-- Object keys coerce to strings — use `Map` for numbers
-- Hashing is O(1) *average*; keys must be cheap to compute
-
----
-
-## Recall
-
-- Two Sum: why insert after the lookup, not before?
-- Group Anagrams: name two valid canonical keys
-- LC 128 Longest Consecutive: why check `!set.has(x - 1)` first?
-
----
-
-## Drill
-
-- 217 Contains Duplicate · 242 Valid Anagram
-- 1 Two Sum · 49 Group Anagrams
-- 347 Top K Frequent · 383 Ransom Note
-- 205 Isomorphic Strings · 128 Longest Consecutive · 290 Word Pattern
diff --git a/islands/decks/index.ts b/islands/decks/index.ts
deleted file mode 100644
index fecbee6..0000000
--- a/islands/decks/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Markdown study decks rendered by the StudyDeck island (reveal.js).
- *
- * Each deck is a plain markdown file in this directory — edit the `.md`
- * file to change its slides. Slides split on `---` lines; lists animate
- * in one item at a time (see `animateLists` in StudyDeck.tsx). Problem
- * numbers reference the curriculum in README.md and the solutions under
- * `work/`.
- */
-import arraysAndStrings from "./arrays-and-strings.md?raw";
-import binarySearch from "./binary-search.md?raw";
-import hashing from "./hashing.md?raw";
-import prefixSum from "./prefix-sum.md?raw";
-import slidingWindow from "./sliding-window.md?raw";
-import twoPointers from "./two-pointers.md?raw";
-
-export const decks = {
- "arrays-and-strings": arraysAndStrings,
- "binary-search": binarySearch,
- hashing,
- "prefix-sum": prefixSum,
- "sliding-window": slidingWindow,
- "two-pointers": twoPointers,
-} as const;
-
-export type DeckName = keyof typeof decks;
diff --git a/islands/decks/prefix-sum.md b/islands/decks/prefix-sum.md
deleted file mode 100644
index e0ba462..0000000
--- a/islands/decks/prefix-sum.md
+++ /dev/null
@@ -1,71 +0,0 @@
-## Prefix Sum
-
-Precompute cumulative totals once → any range sum in **O(1)**.
-
----
-
-## Recognition
-
-- Many range-sum queries over a static array
-- "Count subarrays with sum k" (negatives allowed — window won't work)
-- "Product/sum of everything except me" → prefix from both directions
-
----
-
-## Template — range query (LC 303)
-
-```js
-// prefix[i] = sum of nums[0..i-1], prefix[0] = 0
-const prefix = [0];
-for (const x of nums) prefix.push(prefix[prefix.length - 1] + x);
-
-// sum of nums[l..r] inclusive:
-const rangeSum = (l, r) => prefix[r + 1] - prefix[l];
-```
-
-The leading 0 removes every edge case at `l = 0`.
-
----
-
-## Template — prefix + hashmap (LC 560)
-
-```js
-function subarraySum(nums, k) {
- const count = new Map([[0, 1]]); // empty prefix
- let sum = 0;
- let total = 0;
- for (const x of nums) {
- sum += x;
- total += count.get(sum - k) ?? 0; // earlier prefix to cut off
- count.set(sum, (count.get(sum) ?? 0) + 1);
- }
- return total;
-}
-```
-
-sum(l..r) = prefix[r] − prefix[l−1] — so look up `sum - k`.
-
----
-
-## Pitfalls
-
-- Forgetting the seed `(0, 1)` misses subarrays starting at index 0
-- Count **before** inserting the current prefix (a subarray has length ≥ 1)
-- Sliding window fails here when values can be negative
-
----
-
-## Recall
-
-- Why does LC 560 need a hashmap instead of a window?
-- LC 238 Product Except Self: how do prefix and suffix passes combine?
-- LC 525 Contiguous Array: what do you turn 0s into, and why?
-
----
-
-## Drill
-
-- 303 Range Sum Query · 1480 Running Sum
-- 238 Product of Array Except Self
-- 560 Subarray Sum Equals K
-- 525 Contiguous Array · 974 Subarray Sums Divisible by K
diff --git a/islands/decks/raw.d.ts b/islands/decks/raw.d.ts
deleted file mode 100644
index 1fa5881..0000000
--- a/islands/decks/raw.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// Vite `?raw` imports resolve to the file's text content.
-declare module "*.md?raw" {
- const content: string;
- export default content;
-}
diff --git a/islands/decks/sliding-window.md b/islands/decks/sliding-window.md
deleted file mode 100644
index 669748b..0000000
--- a/islands/decks/sliding-window.md
+++ /dev/null
@@ -1,68 +0,0 @@
-## Sliding Window
-
-Two pointers **plus incremental state** between them. Never recompute the window from scratch.
-
----
-
-## Recognition
-
-- "Longest / shortest **contiguous** subarray or substring such that…"
-- Constraint is monotone: growing the window can only make it worse
-- Fixed window size k → same idea, both ends move together
-
----
-
-## Template — variable window (LC 3)
-
-```js
-function lengthOfLongestSubstring(s) {
- const seen = new Set();
- let best = 0;
- let l = 0;
- for (let r = 0; r < s.length; r++) {
- while (seen.has(s[r])) { // shrink until valid
- seen.delete(s[l]);
- l++;
- }
- seen.add(s[r]);
- best = Math.max(best, r - l + 1);
- }
- return best;
-}
-```
-
-Grow with `r`, shrink with `l` only while invalid.
-
----
-
-## Why it's O(n)
-
-- `r` moves n times, `l` moves at most n times
-- Each element enters and leaves the window **once**
-- The inner `while` is amortized, not nested work
-
----
-
-## Pitfalls
-
-- State must update in O(1) on both add and remove
-- "Shortest window" flips the loop: shrink while **valid**, record before breaking
-- Doesn't apply when the constraint isn't monotone (mixed signs → prefix sum)
-
----
-
-## Recall
-
-- LC 121 Best Time to Buy/Sell: what is the "window state"? (min so far)
-- LC 76 Minimum Window: what makes a window "valid"?
-- When must you fall back to prefix sums instead?
-
----
-
-## Drill
-
-- 121 Best Time to Buy and Sell Stock
-- 3 Longest Substring Without Repeating Characters
-- 424 Longest Repeating Character Replacement
-- 567 Permutation in String · 76 Minimum Window Substring
-- 209 Minimum Size Subarray Sum
diff --git a/islands/decks/two-pointers.md b/islands/decks/two-pointers.md
deleted file mode 100644
index 7308a88..0000000
--- a/islands/decks/two-pointers.md
+++ /dev/null
@@ -1,77 +0,0 @@
-## Two Pointers
-
-Walk two indices instead of trying all pairs — **order lets you prune O(n²) → O(n)**.
-
----
-
-## Recognition
-
-- Sorted array + pair/target condition → converge from both ends
-- Palindrome / symmetric check → ends inward
-- In-place partition or dedupe → slow writer + fast reader
-- Two sequences compared → one pointer each
-
----
-
-## Template — converging (LC 167)
-
-```js
-function twoSumSorted(nums, target) {
- let l = 0;
- let r = nums.length - 1;
- while (l < r) {
- const sum = nums[l] + nums[r];
- if (sum === target) return [l + 1, r + 1];
- if (sum < target) l++; // need a bigger sum
- else r--; // need a smaller sum
- }
-}
-```
-
-Each step discards every pair using the abandoned index — that's the proof.
-
----
-
-## Template — fast/slow writer (LC 977 idea)
-
-```js
-function sortedSquares(nums) {
- const out = new Array(nums.length);
- let l = 0;
- let r = nums.length - 1;
- for (let i = nums.length - 1; i >= 0; i--) {
- const a = nums[l] * nums[l];
- const b = nums[r] * nums[r];
- if (a > b) { out[i] = a; l++; }
- else { out[i] = b; r--; }
- }
- return out;
-}
-```
-
-Largest squares live at the **edges** — fill the output backwards.
-
----
-
-## Pitfalls
-
-- Moving the wrong pointer breaks the pruning argument
-- Duplicates in 3Sum: skip repeats after each fixed element
-- `l < r` vs `l <= r`: do the pointers meet or cross?
-
----
-
-## Recall
-
-- Container With Most Water: why move the *shorter* wall?
-- 3Sum = sort + fix one + which pattern inside?
-- Valid Palindrome: what do you do with non-alphanumerics?
-
----
-
-## Drill
-
-- 125 Valid Palindrome · 167 Two Sum II
-- 15 3Sum · 11 Container With Most Water
-- 42 Trapping Rain Water · 392 Is Subsequence
-- 977 Squares of a Sorted Array · 80 Remove Duplicates II