diff --git a/islands/StudyDeck.tsx b/islands/StudyDeck.tsx
new file mode 100644
index 0000000..c3068f9
--- /dev/null
+++ b/islands/StudyDeck.tsx
@@ -0,0 +1,46 @@
+// 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
new file mode 100644
index 0000000..a4184a7
--- /dev/null
+++ b/islands/decks/arrays-and-strings.md
@@ -0,0 +1,56 @@
+## 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
new file mode 100644
index 0000000..22d0d81
--- /dev/null
+++ b/islands/decks/binary-search.md
@@ -0,0 +1,63 @@
+## 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
new file mode 100644
index 0000000..92f4c68
--- /dev/null
+++ b/islands/decks/hashing.md
@@ -0,0 +1,72 @@
+## 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
new file mode 100644
index 0000000..fecbee6
--- /dev/null
+++ b/islands/decks/index.ts
@@ -0,0 +1,26 @@
+/**
+ * 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
new file mode 100644
index 0000000..e0ba462
--- /dev/null
+++ b/islands/decks/prefix-sum.md
@@ -0,0 +1,71 @@
+## 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
new file mode 100644
index 0000000..1fa5881
--- /dev/null
+++ b/islands/decks/raw.d.ts
@@ -0,0 +1,5 @@
+// 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
new file mode 100644
index 0000000..669748b
--- /dev/null
+++ b/islands/decks/sliding-window.md
@@ -0,0 +1,68 @@
+## 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
new file mode 100644
index 0000000..7308a88
--- /dev/null
+++ b/islands/decks/two-pointers.md
@@ -0,0 +1,77 @@
+## 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