The original hardening got the structural selectors right, but the
labs/kits/book sidebar still had two divergences from the instructors
reference: (a) section headers and leaf links rendered with the same
muted color (no hierarchy) because the section-header rule only matched
the chevron `<a class="sidebar-item-toggle">` and missed the section
text `<a class="sidebar-link text-start">`, and (b) the active-state
rule colored the text in the accent color rather than white on a subtle
accent tint, so active links read as loud accent fills instead of the
clean instructors look.
Changes per file:
* labs/, kits/: extend section-header selector list to include
`a.sidebar-link.text-start`; drop `font-weight: 400 !important` from
the leaf rule so the section-header weight (600) wins on elements
matching both; move active-state color/background/weight into the
high-specificity `#quarto-sidebar` guard block to beat Bootstrap's
default `.sidebar-link.active { color: var(--bs-primary) }`.
* book/quarto/: same treatment, plus include `.part-divider` in the
section-header selector list to keep the part-divider hierarchy.
* tinytorch/quarto/: same active-state hardening, plus rename
`$accent` → `$accent-dark` throughout (the theme partial
shared/styles/themes/_theme-tinytorch.scss declares both variables;
since Quarto auto-`!default`s every scss:defaults declaration, the
reassignment of `$accent` here was silently ignored and rgba calls
resolved with the light #D4740C value). Drop the unused
`$link-color-dark` definition that referenced `$accent-dark` from
scss:defaults — Quarto hoists user defaults above shared partial
defaults, so `$accent-dark` isn't yet defined when that line runs.
Rules in scss:rules resolve `$accent-dark` correctly because rules
are processed after all defaults are merged.
Verified with Playwright probes on /vol1.html (slides), /index.html
(labs, kits), /big-picture.html (tinytorch), /api-stability.html
(mlsysim), /foundations-syllabus.html (instructors): all six now show
white active text on rgba(accent, 0.15) tint, weight 500, with bright
section headers (#e6e6e6) at weight 600 and muted leaf links (#888888
or #cbd5e1).
Extends PR #1633's instructors fix to the rest of the Quarto ecosystem.
Quarto renders floating sidebars with `<a class="sidebar-link">` and
`<a class="sidebar-item-toggle">` directly (not nested inside an
`.sidebar-item a` wrapper), so the legacy selector missed all real DOM
nodes.
Sites updated:
* slides, interviews, tinytorch/quarto, mlsysim/docs: full hardening
(toggle, leaf-link, active-state, search input, TOC rail).
* labs, kits, book/quarto: added missing search-input theming
(core sidebar and TOC fix already present from earlier work).
Each site's accent color is preserved: indigo for instructors and
interviews, pink for slides, amber for tinytorch, cyan for mlsysim,
teal for kits and labs, crimson for book.
The answer option said "~15 seconds (loading 140 GB over PCIe Gen5)"
but the calculation uses NVMe (7 GB/s) as the bottleneck, not PCIe
Gen5 (64 GB/s):
Disk read: 130 GB / 7 GB/s = ~19 s
Deserialization: 130 GB / 20 GB/s = ~7 s
CUDA + warmup: = 1.3 s
Total: = ~26 s
Update option C text to "~26 seconds (NVMe read bottleneck +
deserialization)", change the answer key from "15s" to "26s", and
update the correct callout to show the breakdown.
Doubling hidden width from 128 to 256 in a 3-layer MLP (784->W->W->10)
increases total FLOPs by ~2.3x, not ~4x. The 4x scaling only applies
to the hidden-to-hidden layer (2*W^2); the input (2*784*W) and output
(2*W*10) layers scale linearly, pulling the total well below 4x.
- Change correct answer from "4x" to "2x"
- Update "2x" callout to show "Correct" with explanation
- Update "4x" callout to explain why it's wrong
- Fix MathPeek note from "(not 2x!)" to "(not 4x!)"
With the default settings (2% ingestion error, 5 pipeline stages),
the amplification factor of 1.4 produced ~10.8% output error, but
the answer key marks "~15%" as correct and the MathPeek explains
that 2% error can degrade accuracy by ~15%.
Change factor from 1.4 to 1.5: 2% * 1.5^5 = 15.2%, which matches
the expected answer and the textbook explanation.
`ESP32.memory.capacity` returns 8 MB flash (7812 KiB). Switch to
`memory.sram_capacity` (512 KiB) so the MathPeek ratio and dynamic
display labels correctly show 512 KB and ~3,600x, matching the
hardcoded explanation text throughout the lab.
Same root cause as PRs #1625-#1627 (labs 01-03).
`Hardware.Tiny.ESP32_S3.memory.capacity` returns 8 MB flash (7812 KiB),
not the 512 KiB SRAM. Switch to `memory.sram_capacity` so the computed
ratio (~{size}x over) renders correctly as ~98x instead of ~6x.
Update the hardcoded badge text from "200x OOM" to "~100x OOM" to match
the corrected value (~98x, consistent with the lab 01 fix in PR #1625).
Same root cause as PR #1625 (lab 01) and PR #1626 (lab 02).
`Hardware.Tiny.ESP32_S3.memory.capacity` returns 8 MB flash storage
(7812 KiB), not the 512 KiB SRAM. Switch to `memory.sram_capacity`
so the variable correctly reflects the available SRAM.
Same root cause as the lab 01 fix in PR #1625.
ESP32.memory.capacity returns 8 MB flash storage, not SRAM.
The correct field is ESP32.memory.sram_capacity = 512 KiB.
This was causing ESP32_RAM_KB to be 7812 (flash) instead of 512 (SRAM),
making the OOM ratio show ~6x instead of the correct ~98x.
Also update hardcoded '200x' text to '~100x' throughout the lab
to match the actual ratio (~98x with corrected SRAM value).
Adds the Tier B (lite) versioning pattern to all three Quarto-only
sites in one commit — they have identical shape, so fragmenting would
add review burden without isolation benefit.
For each project:
- workflow_dispatch inputs: release_type/description/site_only/
explicit_version/confirm. Same shape as Tier A workflows so
operators see one consistent UI across the monorepo.
- New `prepare:` job calls _release-prepare.yml with tier=B and the
project's previous_tag_pattern.
- New manifest emit step before deploy: hashes the project's content
directory (excluding _build, .quarto, etc.) and writes
release-manifest.json into the build output. URL convention
matches the deploy path: /kits/, /labs/, /instructors/.
- New create-tag job: tags <project>-v<release_id> on success.
Tier B intentionally skips AI-generated release notes — the
description plus auto-generated commit list (where available) is
sufficient given the rapid iteration cadence and lower citation
stakes than book/staffml/tinytorch/mlsysim.
- Quarto _quarto.yml: <meta name="release-manifest"> in
include-in-header and shared/release/release-pill.html in
include-after-body.
No changes to Marimo notebook structure (labs), Quarto build commands,
PDF builds, or any project's content files.
read_source() used open() without an encoding argument. On Windows,
the default codec is cp1252, which cannot decode the UTF-8 byte 0x90
(used in em-dash and similar characters present in lab markdown strings).
All 33 TestWidgetReturnCompleteness tests failed on Windows with
UnicodeDecodeError; passing PYTHONUTF8=1 made them all green.
Fix: add encoding="utf-8" to the open() call in read_source().
Iterates on the post-merge 404 redesign across all 8 sub-sites:
- SVG roofline-plot fonts bumped for readability without changing
layout: axis labels 9.5pt to 14pt, region labels 9.5pt to 14pt,
title and "your page (404)" annotation 11pt to 16pt.
- Random-joke font shrunk from 1.6rem to 1.3rem (1.1rem on mobile)
so the joke no longer dominates the SVG above it.
- Removed the static "Looks like this page slipped past our load
balancer" subline from 6 sub-sites — it read as a competing static
joke alongside the random rotation. Slides and instructors keep
their informational sublines.
- Joke pool tightened 97 to 79 via a strict ML-systems-centric test:
if you could swap "page" for any generic web/ops resource and the
joke still works, cut. Cuts removed the H&P canonical material
(Amdahl's Law, TLB miss, Dennard scaling, false sharing, fat-tree
topology) that is general computer architecture rather than ML
systems specifically. 19 borderline jokes were rewritten to anchor
punchlines in concepts only ML practitioners decode (KV cache,
gradient AllReduce, prefill, ZeRO, speculative-decode acceptance,
1F1B schedule, ridge point, BPE merges).
Replace the inline-game 404 across all 8 sub-sites with a unified
design: roofline-plot illustration, random pick from a 97-joke
editorial-curated pool, dual CTA (home + playground), and a
GitHub-issue-backed contribute link for community submissions.
The joke pool was curated through a multi-agent editorial process:
4 generators (each with a different ML systems lens) produced 240
candidates; 5 reviewer personas (architect, NLP researcher, production
engineer, undergrad, copy editor) scored each; the synthesizer kept 92
surviving jokes plus 5 new canonical-architecture additions (Amdahl,
TLB, Dennard, false sharing, fat-tree topology) the architect flagged
as missing from the H&P-tradition canon.
Self-contained CSS with prefers-color-scheme: dark adapts to host
themes without coupling to each sub-site's bespoke styling. SVG
follows the book's semantic palette (blue memory-bound slope, green
compute roof, MIT red error annotation). Per-site navigation
preserved across all sub-sites.
Adds .github/ISSUE_TEMPLATE/404_joke.yml so contributions arrive
structured (joke text, theme dropdown, license checkbox) and
auto-tagged (site, 404-joke, good-first-issue) for triage.
Strip the typographic noise from the inventory section: drop monospace
font from lab numbers and cluster meta (was JetBrains Mono / Fira Code),
remove varied letter-spacing, consolidate font sizes and color shades.
Result: a single font (Inter, inherited from site), three sizes
(0.85 / 1 / 1.3 rem), two weights (400 / 700), three colors (#1e293b
primary text, #64748b secondary, #2563EB accent). Same information,
much calmer surface.
Mono fonts on lab numbers were a dev-tools instinct (mono signals
"this is a data identifier") but the lab numbers aren't identifiers
anyone will type — they're just ordering. Sans-serif blue carries the
ordering signal without breaking from the rest of the page.
Fresh-eyes review surfaced a real defect: the labs site sidebar uses
"I. Foundations / II. Build / III. Optimize / IV. Deploy / Capstone"
(5 sections per volume) but my inventory had invented a different
6-cluster taxonomy ("Foundations & Workflow / Architecture / Training
& Optimization / Performance & Deployment / Operations / Capstone")
with a different lab assignment.
The mismatch made readers wonder if "Capstone" in the sidebar referred
to the same thing as "Capstone" in the inventory, and forced a mental
translation between two parallel taxonomies for the same content.
Aligned the inventory cluster headings and lab assignments exactly to
the sidebar's canonical structure. Lab 07 (Kernel Fusion Dividend)
moves back to "II. Build" where it always was per the sidebar — a
prior round of review had argued for moving it to Performance, but
that argument was never reconciled against the existing canonical
organization. Inventory now mirrors the sidebar's chapter shape 1:1.
Capstone cluster contains both Lab 15 (regular card) and Lab 16
(full-width with CAPSTONE marker) per the sidebar's grouping.
Round 2 polish based on multi-perspective feedback:
- Cluster headings get more vertical breathing room (2.4rem top
margin, was 1.6rem), slightly larger font (1rem, was 0.95rem),
and a thin top rule. Reviewer flagged that the previous treatment
competed with lab-card titles for visual weight.
- Volume II heading gets a dashed top rule + extra top margin (4.5rem)
to mark the pivot from single-machine (Vol I) to distributed (Vol II).
The two volumes were reading as parallel lists rather than as a
staircase; the dashed rule signals "you've crossed into Vol II"
without breaking the calm aesthetic.
- Vol II Cluster 1 renamed "Scale Foundations" → "Scale Mechanics"
per distributed-systems reviewer; "foundations" reads as consensus/
clocks/failure-models in distributed systems vocabulary, but the
cluster is actually about why naive scale assumptions break.
- Bridge between hero and pedagogy section rewritten to stop recapping
the three-step ritual the hero already showed; instead names the hero
as one instance and pivots to the pattern. Folds the prediction-lock
callout into the pedagogy paragraph where it belongs.
Quarto wraps each markdown heading in a <section> AND copies the
heading's classes onto the section element. So .cluster-heading::after
fired twice — once on the <h4> (correct, before card grid) and once on
the wrapping <section> (incorrect, after the section's last child,
which is the card grid).
Invisible on desktop (inline ::after blends into trailing whitespace),
visible on mobile where the media query bumps ::after to display:block
and the orphan meta line appears below the capstone card.
Scope the rule to h3.volume-heading::after / h4.cluster-heading::after
so it only fires on the heading element itself, not the section wrapper.
Replace the two flat markdown tables with a clean card grid grouped
into thematic clusters per volume. Each card shows lab number, title,
and core question only — no chips, ribbons, or accent rainbow. Single
accent color matches the labs site brand (#2563EB).
Lab 07 (Kernel Fusion Dividend) moves from Architecture to Performance
& Deployment — kernel fusion is a runtime optimization, not a
model-design choice. Capstones get their own dedicated cluster and
span the full grid row; distinction comes from layout and placement,
not color.
Verified responsive from 320px to 1440px with zero horizontal overflow.
Adds tools/capture_landing_shots.py — Playwright-based screenshot
helper used during the redesign, reusable for future inventory work.
- Added 'KV Cache Packer' to teach PagedAttention and KV fragmentation
- Added 'Cluster Commander' to teach Slurm scheduling and fleet fragmentation
- Registered all 14 games in the runtime registry
- Fixed WebGL rendering loops to avoid performance overhead and crashes
- Updated 404 pages across all workspaces to route to the new games Playground
- Overrode default Quarto 'S' search shortcut to Shift+? to free up typing controls
Switch labs navbar from the M-badge SVG to the shared SEAS shield PNG
used by tinytorch / kits / mlsysim / instructors. Browser-tab favicon
keeps the M-badge SVG since favicons render at sizes where the shield
becomes illegible.
This commit introduces the following fixes to the Marimo labs architecture:
1. Interactive Testing: Updates test_widget.py to dynamically extract, simulate clicks, and verify the interactive states hidden behind mo.stop() to ensure execution pipelines don't crash.
2. Ledger Continuity: Fixes an issue in 4 Volume 2 labs where the ledger.save() was mistakenly passed a string key (e.g. 'v2_05') instead of an integer.
3. WASM Relative Pathing: Modifies tools/build_site.sh to duplicate built Pyodide wheel assets into vol1/wheels and vol2/wheels to satisfy Pyodide's worker.js relative path resolution, which was causing the labs to hang at startup on GitHub Pages with BadZipFile errors.
Align the MLSys·im code, docs, paper, website, workflows, and lab wheel for the 0.1.1 release. This also fixes runtime/API issues found during release review and prepares the paper PDF plus archive package.
Align public README and site messaging around the curriculum components, adoption paths, and current early-release status so newcomers can move from reading to building, deployment, practice, and teaching.
Before: four Quarto sites each shipped their own copy of the
`.sidebar-navigation .sidebar-item a` rule block, each hard-wired to a
different accent variable ($accent, $kits-accent, $colabs-accent,
$mlsysim-accent). Plus one 1350-line orphan file
(book/quarto/assets/styles/style.scss) that was referenced nowhere and
carried yet another copy of the same rules.
After: every site imports shared/styles/partials/_sidebar.scss once.
Each site aliases $accent to its own site-accent (book does it via
themes/_theme-harvard which sets `$accent: $brand-crimson`; kits/labs/
mlsysim already had `$accent: $site-accent` at the top of their
style.scss), so the partial's accent-keyed hover/active states resolve
to the right color per site without needing a per-site rule copy.
Files:
- shared/styles/partials/_sidebar.scss
(unchanged in this PR — already is the canonical form after #1514)
- book/quarto/assets/styles/_base-styles.scss
replace ~40 lines of local sidebar rules + .part-divider with
`@import '.../partials/sidebar'`
- book/quarto/assets/styles/style.scss
DELETE (1350 lines, zero build references — confirmed by grep
across _quarto*.yml, Makefile, .sh, .py, .js)
- kits/assets/styles/style.scss
replace local sidebar rules with partial import; keep the
site-specific `.sidebar-title` rule (not part of the shared shape)
- labs/assets/styles/style.scss
replace local sidebar rules with partial import
- mlsysim/docs/styles/style.scss
replace local sidebar rules with partial import; the
.sidebar-navigation > .sidebar-menu-container dividers below
are site-specific and stay
Net: +23 / -1531 lines. Behavior is identical (the partial was already
tightened to these same values in PR #1514, so this is a
refactor-not-redesign). slides and instructors already had no sidebar
overrides and are not touched.
TinyTorch was already importing the partial directly and is unchanged.
The ecosystem now has one sidebar rule block, in one file, controlling
every Quarto site.
Parts B and C of the data engineering lab showed the numerical answer
(~11 hours for transfer time; 10.8% error cascade output) in the context
text immediately above the prediction radio button, making it trivial to
select the correct option without reasoning through the problem.
Remove the pre-computed specific values from the pre-prediction text,
keeping only the formula structure so learners can apply the formula
themselves. The full worked examples remain available after the
prediction is submitted (interactive calculator + Math Peek accordion).
Fixes#1417
Co-Authored-By: Octopus <liyuan851277048@icloud.com>
Every site's announcement bar now follows one template:
Line 1 — identity + primary CTA (what is THIS site)
Line 2 — the book (or, on book sites, the other volume)
Line 3 — "Alongside the book:" sibling row (3 most-relevant verbs)
Line 4 — newsletter
The book is the anchor of the curriculum; every other site is a verb
applied to it — TinyTorch (build), Hardware Kits (deploy), MLSys·im
(simulate), Labs (explore), Slides + Instructors (teach). Making that
shape visible in every bar turns nine independent sites into chapters
of one curriculum.
Removed stale copy: "Happy New Year!" on kits + labs, "coming in 2026"
on labs (we are in 2026; replaced with "Coming Summer 2026"), TinyTorch
v0.1.10 single-line release notice (belonged in a release changelog,
not the always-visible nav bar).
Normalized outbound links: /book -> /vol1/ and /vol2/, consistent
trailing-slash hygiene on every URL. Newsletter link points to
https://mlsysbook.ai/newsletter/ (the actual Quarto page) instead of
#subscribe (which only resolves on the landing page).
The landing site (mlsysbook.ai) uses a 5-line variant that covers all
four learner verbs + the teacher-tools row; every other site uses the
4-line form.
StaffML is a Next.js app, not a Quarto site, so its banner is out of
scope for this PR and will ship as a separate React component change.
* polish(tinytorch/diagrams): align 23 Lab Guide SVGs with book style guide
The hand-authored diagrams under tinytorch/quarto/assets/images/diagrams/
were drifting from the ML Systems SVG style guide — they used Tailwind
CSS greys and custom oranges instead of the book palette. This made them
visually clash with the canonical big-picture-module-flow.svg (under
assets/images/svg/) and with every other diagram in the textbook.
Normalize to book style:
- Fills: #f4f5f7, #f8f9fa, #e5e7eb → #f7f7f7 / #bbb (book neutrals)
- Text: #1f2937, #6b7280 → #333 / #999 (book text hierarchy)
- Arrows: #9ca3af → #555 (book neutral stroke)
- Accent orange: #fff1e8 / #ff8246 → #fdebd0 / #c87b2a (book routing)
- Corner radius: rx="2" → rx="4" per style-guide standard
- Stroke width: flat "1" → "1.2" (secondary tier per style guide)
- Section titles: font-size="11" (bold) → "12" per style-guide headers
No content, layout, or positional changes — only style-token swaps.
All 23 SVGs re-validated as well-formed XML.
Side effect: the orphan diagrams/00_big-picture-module-flow.svg is also
normalized. The canonical big-picture is under assets/images/svg/ and
was already book-styled (not touched by this pass).
* polish(tinytorch/01_tensor): port Cache Tiling systems callout from ABOUT.md
The stashed work from the tinytorch-updates branch included a Cache
Tiling "Systems Implication" callout for Module 01, but it lived in
tinytorch/src/01_tensor/ABOUT.md — a path that was retired when the
module docs consolidated into tinytorch/quarto/modules/. The companion
"Contiguous Memory & Strides" callout from the same stash already made
it over during the consolidation; this one got left behind.
Insert the callout right after the matmul O(n³) explanation (the
natural bridge into "why does the naive loop lose to BLAS?"), and
before the Shape Manipulation section. Progressive-disclosure check:
every concept the callout introduces (cache misses, BLAS, O(n³) vs
O(n²)) is already on the page or introduced inline in the callout
itself (Memory-Bound / Compute-Bound are defined parenthetically).
The pattern matches the existing book-style sidebar on line 587.
* deps: bump TypeScript 5→6, @types/node 20→25, ipykernel 6→7
Resolves 9 stale Dependabot PRs (#1494, #1497, #1490, #1491, #1479,
#1477, #1475, #1467, #1454) whose package-lock conflicts blocked
auto-rebase.
VSCode extensions: bump TypeScript devDep to ^6.0.3 and @types/node to
^25.6.0 across book-ext, mlsysim-ext, labs-ext, kits-ext, tinytorch-ext.
Regenerate lockfiles.
TypeScript 6.0 no longer auto-includes ambient @types packages; add
explicit "types": ["node", "vscode"] to each tsconfig so
child_process / fs / Buffer / vscode API types continue to resolve.
All 5 extensions now compile clean with TS 6.0.3 + @types/node 25.6.0.
TinyTorch: bump ipykernel floor to >=7.2.0 in pyproject.toml,
requirements.txt, and binder/requirements.txt.
Add `text` language tag to 25 unlabeled fenced code blocks across the
public-facing READMEs. Mostly directory-tree listings, all-contributors
bot instructions, and pseudo-output ASCII blocks — none were getting
syntax highlighting anyway, but the explicit tag silences markdownlint
MD040 and signals intent ("this is plain text, not a forgotten lang").
GitHub's github-markdown-css applies:
.markdown-body table { display: block; width: max-content; max-width: 100%; }
The HTML width="100%" attribute is a presentational hint with lower
specificity than the class selector, so tables with short cell content
were sizing to max-content and not stretching to fill the column.
Tables with long sentences per cell stretched fine, masking the bug.
Add inline style="width:100%" (specificity 1,0,0,0) which overrides
the class-selector rule. Keep width="100%" attribute as a fallback for
non-GitHub renderers (VSCode preview, GitLab, plain HTML viewers).
54 tables updated across 10 READMEs + the two contributor-sync scripts
that regenerate auto-managed tables.
The sub-project READMEs used an old-school nested-table card design
with hardcoded bgcolor="#ffffff", "#cfd6dd", "#eef2f7" plus deprecated
HTML4 attributes (cellpadding, cellspacing, border). It looked good in
light mode but produced harsh white islands in GitHub's dark theme,
which is what most readers see today.
Across 11 sub-READMEs:
- Strip the card wrapper so data tables are just clean
<table width="100%"> with semantic <thead>/<tbody>. Headers keep
their column widths; bgcolor/valign/zebra-stripe cruft is removed
(GitHub provides its own theme-aware striping).
- Convert the early-release callouts (and mlperf-edu's two-tier
status block + "source of truth" note + interviews' two info boxes)
to GitHub-native > [!NOTE] / > [!WARNING] / > [!TIP] callouts.
These are theme-aware, get proper icons, and render correctly in
light AND dark mode.
Net result: 528 lines of HTML cruft removed, 230 lines of clean
markdown added. Visual identity is preserved (callouts still stand
out, tables still stretch full-width) while becoming dark-mode safe
and consistent with the main README.
Add `width="100%"` to every HTML content and contributor table across all
project READMEs so they render full-width on GitHub instead of collapsing
to natural content width. Cell-level `width="X%"` percentages were already
in place but only take effect once the table itself has an explicit width.
Also update the contributor-sync scripts so the auto-generated tables stay
consistent on the next bot run:
- .github/workflows/contributors/generate_main_readme.py
- .github/workflows/contributors/generate_readme_tables.py
Scope: 27 files, 85 tables. Sub-project READMEs that already use the
"card" pattern (labs/, kits/ content sections with <table width="98%">
wrappers) are intentionally untouched.
Brings the TinyTorch lab guide's Quarto project in line with
book/quarto/, the only other in-tree Quarto publication that builds
both web and PDF outputs from a single source. The previous name had
three redundancies:
- already under tinytorch/, so "site-" prefix wasn't disambiguating
- also produces the PDF lab guide, so "site-" was misleading
- the top-level site/ dir made "site-quarto" read as "the site's
quarto config" rather than "the tinytorch site, in quarto"
After this rename the convention is straightforward:
book/quarto/ -> the textbook (web + PDF)
tinytorch/quarto/ -> the TinyTorch lab guide (web + PDF)
mlsysim/docs/ -> mlsysim API reference (kept as docs/, since it
really is API reference, not a publication)
Touches 7 GitHub workflows, both .gitignore files, the rename target's
own self-references (Makefile, _quarto.yml configs, STYLE.md,
measure-pdf-images.py), and 6 copies of subscribe-modal.js plus a few
shared scripts/configs whose comments documented the old path.
Verified: rebuilt pdf/TinyTorch-Guide.pdf (2.1M) cleanly from the new
location with 'make pdf' from tinytorch/quarto/.