diff --git a/fixtures/courses/sample/MENTOR.md b/fixtures/courses/sample/MENTOR.md
new file mode 100644
index 0000000..696e9f0
--- /dev/null
+++ b/fixtures/courses/sample/MENTOR.md
@@ -0,0 +1 @@
+You are the mentor for the sample course. Give hints, never full solutions.
diff --git a/fixtures/courses/sample/code/S01 - First steps/hello.sh b/fixtures/courses/sample/code/S01 - First steps/hello.sh
new file mode 100644
index 0000000..6c02b52
--- /dev/null
+++ b/fixtures/courses/sample/code/S01 - First steps/hello.sh
@@ -0,0 +1 @@
+echo "hello from the sample course"
diff --git a/fixtures/courses/sample/exercises/add/solution.js b/fixtures/courses/sample/exercises/add/solution.js
new file mode 100644
index 0000000..7d65831
--- /dev/null
+++ b/fixtures/courses/sample/exercises/add/solution.js
@@ -0,0 +1,3 @@
+export function add(a, b) {
+ return a + b;
+}
diff --git a/fixtures/courses/sample/exercises/add/starter.js b/fixtures/courses/sample/exercises/add/starter.js
new file mode 100644
index 0000000..29b3851
--- /dev/null
+++ b/fixtures/courses/sample/exercises/add/starter.js
@@ -0,0 +1,3 @@
+export function add(a, b) {
+ return a - b;
+}
diff --git a/fixtures/courses/sample/exercises/add/tests.js b/fixtures/courses/sample/exercises/add/tests.js
new file mode 100644
index 0000000..4d5843b
--- /dev/null
+++ b/fixtures/courses/sample/exercises/add/tests.js
@@ -0,0 +1,4 @@
+export const tests = [
+ { name: "adds 2 and 3", run: (mod, assert) => assert.equal(mod.add(2, 3), 5) },
+ { name: "adds -1 and 1", run: (mod, assert) => assert.equal(mod.add(-1, 1), 0) },
+];
diff --git a/fixtures/courses/sample/lessons/01-intro.md b/fixtures/courses/sample/lessons/01-intro.md
new file mode 100644
index 0000000..4e844ff
--- /dev/null
+++ b/fixtures/courses/sample/lessons/01-intro.md
@@ -0,0 +1,3 @@
+# Introduction
+
+Welcome to the sample course. It exists so tests have something small to load.
diff --git a/fixtures/courses/sample/lessons/02-video.md b/fixtures/courses/sample/lessons/02-video.md
new file mode 100644
index 0000000..52c1d4b
--- /dev/null
+++ b/fixtures/courses/sample/lessons/02-video.md
@@ -0,0 +1,3 @@
+# Transcript
+
+A two-second test pattern plays. Nothing is said.
diff --git a/fixtures/courses/sample/lessons/03-code.md b/fixtures/courses/sample/lessons/03-code.md
new file mode 100644
index 0000000..0a29890
--- /dev/null
+++ b/fixtures/courses/sample/lessons/03-code.md
@@ -0,0 +1,7 @@
+# Code along
+
+Run the script in the section files:
+
+```sh
+sh hello.sh
+```
diff --git a/fixtures/courses/sample/lessons/04-exercise.md b/fixtures/courses/sample/lessons/04-exercise.md
new file mode 100644
index 0000000..4e78add
--- /dev/null
+++ b/fixtures/courses/sample/lessons/04-exercise.md
@@ -0,0 +1,23 @@
+# Adding numbers
+
+Functions take values and return values.
+
+## Exercises
+
+### Exercise 2.1 — `add`
+
+Implement `add(a, b)` so that it returns the sum of its arguments.
+
+## Hints
+
+Exercise 2.1 (add) — nudge
+
+Which operator combines two numbers?
+
+
+
+Exercise 2.1 (add) — near-solution
+
+Return `a + b`.
+
+
diff --git a/fixtures/courses/sample/media/clip.mp4 b/fixtures/courses/sample/media/clip.mp4
new file mode 100644
index 0000000..2e316e0
Binary files /dev/null and b/fixtures/courses/sample/media/clip.mp4 differ
diff --git a/fixtures/courses/sample/media/deck.pdf b/fixtures/courses/sample/media/deck.pdf
new file mode 100644
index 0000000..5f8fde2
Binary files /dev/null and b/fixtures/courses/sample/media/deck.pdf differ
diff --git a/fixtures/courses/sample/stcourse.yaml b/fixtures/courses/sample/stcourse.yaml
new file mode 100644
index 0000000..7d25ac2
--- /dev/null
+++ b/fixtures/courses/sample/stcourse.yaml
@@ -0,0 +1,35 @@
+schema: 1
+id: sample
+title: Sample course
+pitch: A tiny self-contained course used by the test suite.
+visibility: public
+tracks:
+ - { id: js, label: JavaScript }
+mentor: MENTOR.md
+source: { kind: authored }
+decks:
+ - { id: d1, file: media/deck.pdf, attachTo: m1-video }
+modules:
+ - id: m1
+ title: First steps
+ code: { dir: "code/S01 - First steps", command: "sh hello.sh" }
+ lessons:
+ - { id: m1-intro, title: Introduction, type: text, file: lessons/01-intro.md }
+ - { id: m1-video, title: Watch the clip, type: video, file: media/clip.mp4, duration: 2, transcript: lessons/02-video.md }
+ - { id: m1-code, title: Code along, type: text, file: lessons/03-code.md }
+ - id: m2
+ title: Exercises
+ lessons:
+ - { id: m2-text, title: Adding numbers, type: text, file: lessons/04-exercise.md }
+ - id: m2-ex-add
+ title: "Exercise 2.1: add"
+ type: exercise
+ file: lessons/04-exercise.md
+ anchor: exercise-21--add
+ hints: { file: lessons/04-exercise.md, match: "Exercise 2.1 (add)" }
+ browser:
+ language: js
+ starter: exercises/add/starter.js
+ tests: exercises/add/tests.js
+ solution: exercises/add/solution.js
+ localCommand: "node --test"
diff --git a/scripts/make-fixtures.sh b/scripts/make-fixtures.sh
new file mode 100755
index 0000000..a9112ef
--- /dev/null
+++ b/scripts/make-fixtures.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+# Regenerates the binary fixtures under fixtures/courses/sample/media.
+set -eu
+cd "$(dirname "$0")/../fixtures/courses/sample/media"
+/usr/bin/ffmpeg -y -loglevel error -f lavfi -i "testsrc=duration=2:size=160x90:rate=10" \
+ -pix_fmt yuv420p -c:v libx264 -crf 35 -preset veryslow -movflags +faststart clip.mp4
+/home/user/.venvs/pdf/bin/python -I - <<'PY'
+import pymupdf
+doc = pymupdf.open()
+for i, text in enumerate(["Sample deck: title", "Sample deck: middle", "Sample deck: end"], 1):
+ page = doc.new_page(width=480, height=270)
+ page.insert_text((40, 135), text, fontsize=24)
+doc.save("deck.pdf", garbage=4, deflate=True)
+PY