explico 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/CHANGELOG.md +879 -1
  2. package/README.md +200 -30
  3. package/dist/compiler/astro.d.ts +15 -2
  4. package/dist/compiler/astro.js +40 -11
  5. package/dist/compiler/cli-main.d.ts +43 -0
  6. package/dist/compiler/cli-main.js +392 -0
  7. package/dist/compiler/cli.d.ts +424 -9
  8. package/dist/compiler/cli.js +1289 -8
  9. package/dist/compiler/collections.d.ts +88 -1
  10. package/dist/compiler/collections.js +170 -25
  11. package/dist/compiler/course-config.d.ts +1 -1
  12. package/dist/compiler/course-config.js +105 -1
  13. package/dist/compiler/decks.d.ts +161 -0
  14. package/dist/compiler/decks.js +235 -0
  15. package/dist/compiler/exports/combined-html.d.ts +5 -1
  16. package/dist/compiler/exports/deck-fit.d.ts +55 -0
  17. package/dist/compiler/exports/deck-fit.js +90 -0
  18. package/dist/compiler/exports/export-chapters.d.ts +37 -6
  19. package/dist/compiler/exports/export-chapters.js +64 -5
  20. package/dist/compiler/exports/export-pages.d.ts +30 -3
  21. package/dist/compiler/exports/export-pages.js +24 -1
  22. package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  23. package/dist/compiler/exports/latex/cache.d.ts +78 -0
  24. package/dist/compiler/exports/latex/cache.js +129 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +10 -1
  26. package/dist/compiler/exports/latex/capture.js +3 -1
  27. package/dist/compiler/exports/latex/compile.d.ts +13 -2
  28. package/dist/compiler/exports/latex/compile.js +12 -1
  29. package/dist/compiler/exports/latex/deck.d.ts +92 -0
  30. package/dist/compiler/exports/latex/deck.js +349 -0
  31. package/dist/compiler/exports/latex/document.d.ts +51 -6
  32. package/dist/compiler/exports/latex/document.js +71 -12
  33. package/dist/compiler/exports/latex/explico-common.sty +198 -0
  34. package/dist/compiler/exports/latex/explico.cls +13 -183
  35. package/dist/compiler/exports/latex/serialize.d.ts +60 -4
  36. package/dist/compiler/exports/latex/serialize.js +4 -2
  37. package/dist/compiler/exports/latex-export.d.ts +24 -2
  38. package/dist/compiler/exports/latex-export.js +107 -26
  39. package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
  40. package/dist/compiler/exports/markdown/serialize.js +85 -8
  41. package/dist/compiler/exports/markdown-export.d.ts +6 -1
  42. package/dist/compiler/exports/markdown-export.js +50 -11
  43. package/dist/compiler/exports/standalone-html.d.ts +10 -5
  44. package/dist/compiler/exports/standalone-html.js +3 -3
  45. package/dist/compiler/exports/static-tree.d.ts +38 -1
  46. package/dist/compiler/exports/static-tree.js +120 -4
  47. package/dist/compiler/knowledge-version.d.ts +26 -2
  48. package/dist/compiler/knowledge-version.js +27 -17
  49. package/dist/compiler/lesson-derivation.d.ts +143 -6
  50. package/dist/compiler/lesson-derivation.js +145 -13
  51. package/dist/compiler/manifest.d.ts +133 -4
  52. package/dist/compiler/manifest.js +359 -58
  53. package/dist/compiler/page-versions.d.ts +160 -0
  54. package/dist/compiler/page-versions.js +198 -0
  55. package/dist/compiler/render-diagnostics.d.ts +5 -1
  56. package/dist/compiler/resolution-report.d.ts +24 -6
  57. package/dist/compiler/resolution-report.js +22 -4
  58. package/dist/compiler/validate-content.d.ts +7 -0
  59. package/dist/compiler/validate-content.js +160 -0
  60. package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
  61. package/dist/curriculum/competency-graph.d.ts +54 -0
  62. package/dist/curriculum/competency-graph.js +41 -0
  63. package/dist/reference/base-library.d.mts +8 -0
  64. package/dist/reference/base-library.mjs +19 -0
  65. package/dist/reference/consistency.d.ts +8 -4
  66. package/dist/reference/consistency.js +76 -26
  67. package/dist/reference/deck-remark.d.mts +123 -0
  68. package/dist/reference/deck-remark.mjs +600 -0
  69. package/dist/reference/deck-source.d.ts +107 -0
  70. package/dist/reference/deck-source.js +140 -0
  71. package/dist/reference/decks-validate.d.ts +133 -0
  72. package/dist/reference/decks-validate.js +359 -0
  73. package/dist/reference/derived-entries.d.ts +5 -1
  74. package/dist/reference/diagram-fit.d.ts +1 -1
  75. package/dist/reference/figure-symbols.d.ts +23 -6
  76. package/dist/reference/figure-symbols.js +51 -15
  77. package/dist/reference/float-name.d.ts +43 -0
  78. package/dist/reference/float-name.js +58 -0
  79. package/dist/reference/folds-validate.d.ts +1 -1
  80. package/dist/reference/gate-abbreviations.d.ts +1 -1
  81. package/dist/reference/gate-indices.js +5 -4
  82. package/dist/reference/gate-math.d.ts +29 -2
  83. package/dist/reference/gate-math.js +99 -18
  84. package/dist/reference/gate-part-pages.d.ts +1 -1
  85. package/dist/reference/lab-katex-validate.d.ts +10 -6
  86. package/dist/reference/lab-katex-validate.js +17 -9
  87. package/dist/reference/lab-math-scope.js +29 -4
  88. package/dist/reference/math-glyphs.d.mts +25 -0
  89. package/dist/reference/math-glyphs.mjs +189 -25
  90. package/dist/reference/page-notation.d.ts +22 -3
  91. package/dist/reference/page-notation.js +32 -10
  92. package/dist/reference/references.d.ts +14 -1
  93. package/dist/reference/references.js +13 -0
  94. package/dist/reference/registry-scope.d.mts +33 -0
  95. package/dist/reference/registry-scope.mjs +34 -0
  96. package/dist/reference/registry.js +29 -10
  97. package/dist/reference/rehype-deck.d.mts +6 -0
  98. package/dist/reference/rehype-deck.mjs +286 -0
  99. package/dist/reference/rehype-heading-anchors.d.mts +20 -0
  100. package/dist/reference/rehype-heading-anchors.mjs +48 -6
  101. package/dist/reference/remark-notation.mjs +249 -40
  102. package/dist/reference/source-lines.d.mts +12 -0
  103. package/dist/reference/source-lines.mjs +65 -0
  104. package/dist/reference/types.d.ts +28 -0
  105. package/dist/rules/catalog.d.ts +21 -4
  106. package/dist/rules/catalog.js +221 -2
  107. package/dist/rules/course-rules.d.ts +5 -1
  108. package/dist/rules/course-rules.js +17 -3
  109. package/dist/rules/course-rules.schema.json +12 -0
  110. package/dist/rules/in-effect.d.ts +13 -3
  111. package/dist/rules/in-effect.js +30 -7
  112. package/dist/rules/rules.schema.json +259 -1
  113. package/dist/rules/settings.d.ts +1 -1
  114. package/dist/session/PreferenceStore.d.ts +6 -0
  115. package/dist/session/PreferenceStore.js +18 -1
  116. package/dist/session/deck-memory.d.ts +116 -0
  117. package/dist/session/deck-memory.js +143 -0
  118. package/dist/session/reading-memory.d.ts +96 -0
  119. package/dist/session/reading-memory.js +133 -0
  120. package/docs/VERSIONING.md +21 -18
  121. package/docs/authoring/README.md +29 -9
  122. package/docs/authoring/agents.md +63 -16
  123. package/docs/authoring/components.md +15 -0
  124. package/docs/authoring/deck.md +588 -0
  125. package/docs/authoring/migrating.md +1045 -16
  126. package/docs/authoring/notation.md +49 -6
  127. package/docs/authoring/structure.md +80 -1
  128. package/docs/authoring/versioning.md +54 -2
  129. package/docs/authoring/visualizations.md +7 -2
  130. package/docs/course-agent/AGENTS.md +37 -0
  131. package/docs/course-agent/agents/rules-reviewer.md +33 -0
  132. package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
  133. package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
  134. package/docs/course-agent/skills/release-content/SKILL.md +24 -0
  135. package/docs/diagram-scaling.md +148 -0
  136. package/docs/privacy.md +59 -0
  137. package/package.json +8 -2
  138. package/src/compiler/astro.ts +60 -21
  139. package/src/compiler/cli-main.ts +469 -0
  140. package/src/compiler/cli.ts +1905 -16
  141. package/src/compiler/collections.ts +284 -25
  142. package/src/compiler/course-config.ts +267 -0
  143. package/src/compiler/decks.ts +455 -0
  144. package/src/compiler/exports/deck-fit.ts +127 -0
  145. package/src/compiler/exports/export-chapters.ts +105 -6
  146. package/src/compiler/exports/export-pages.ts +46 -2
  147. package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  148. package/src/compiler/exports/latex/cache.ts +183 -0
  149. package/src/compiler/exports/latex/capture.ts +5 -2
  150. package/src/compiler/exports/latex/compile.ts +18 -0
  151. package/src/compiler/exports/latex/deck.ts +419 -0
  152. package/src/compiler/exports/latex/document.ts +117 -21
  153. package/src/compiler/exports/latex/explico-common.sty +198 -0
  154. package/src/compiler/exports/latex/explico.cls +13 -183
  155. package/src/compiler/exports/latex/serialize.ts +25 -21
  156. package/src/compiler/exports/latex/texlive-packages.txt +6 -2
  157. package/src/compiler/exports/latex-export.ts +187 -45
  158. package/src/compiler/exports/markdown/serialize.ts +118 -9
  159. package/src/compiler/exports/markdown-export.ts +56 -11
  160. package/src/compiler/exports/standalone-html.ts +8 -7
  161. package/src/compiler/exports/static-tree.ts +175 -3
  162. package/src/compiler/knowledge-version.ts +56 -21
  163. package/src/compiler/lesson-derivation.ts +282 -16
  164. package/src/compiler/manifest.ts +558 -63
  165. package/src/compiler/page-versions.ts +359 -0
  166. package/src/compiler/resolution-report.ts +38 -7
  167. package/src/compiler/validate-content.ts +212 -0
  168. package/src/components/CourseContents.astro +124 -6
  169. package/src/components/CurriculumMap.astro +49 -1
  170. package/src/components/FloatIndex.astro +1 -1
  171. package/src/components/PartOverview.astro +51 -5
  172. package/src/components/assessments/AssessmentSet.astro +41 -15
  173. package/src/components/block-heading.ts +15 -6
  174. package/src/components/card-carry.ts +7 -4
  175. package/src/components/card-manager.ts +14 -2
  176. package/src/components/card-memory.ts +13 -9
  177. package/src/components/deck/DeckEnhancer.astro +13 -0
  178. package/src/components/deck/deck-context.ts +104 -0
  179. package/src/components/deck/deck-controller.ts +1287 -0
  180. package/src/components/deck/deck-map.ts +91 -0
  181. package/src/components/deck-lane.ts +163 -0
  182. package/src/components/diagram/Diagram.astro +131 -86
  183. package/src/components/disclosure/persisted-disclosure.ts +9 -1
  184. package/src/components/figure/Figure.astro +7 -2
  185. package/src/components/figure/FloatCaption.astro +5 -3
  186. package/src/components/fold/Fold.astro +2 -2
  187. package/src/components/notation/NotationFormula.astro +81 -11
  188. package/src/components/notation/NotationGlossary.astro +132 -1
  189. package/src/components/notation/NotationLayer.astro +15 -7
  190. package/src/components/starlight/AboutPanel.astro +186 -3
  191. package/src/components/starlight/CollapsibleSections.astro +24 -3
  192. package/src/components/starlight/DiagramEnhancer.astro +5 -1
  193. package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
  194. package/src/components/starlight/LayoutHeader.astro +17 -0
  195. package/src/components/starlight/LayoutPageTitle.astro +110 -0
  196. package/src/components/starlight/LayoutSidebar.astro +807 -21
  197. package/src/components/starlight/LayoutSocialIcons.astro +62 -6
  198. package/src/components/starlight/LessonExportActions.astro +27 -9
  199. package/src/components/starlight/LessonFooter.astro +59 -0
  200. package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
  201. package/src/components/starlight/TextSizeControl.astro +24 -13
  202. package/src/components/starlight/page-marks.ts +139 -0
  203. package/src/components/starlight/section-summary.ts +1 -1
  204. package/src/components/starlight/sidebar-editions.ts +245 -0
  205. package/src/components/table/Table.astro +15 -7
  206. package/src/content-config.ts +87 -1
  207. package/src/curriculum/competency-graph.ts +95 -0
  208. package/src/reference/base-library.mjs +19 -0
  209. package/src/reference/consistency.ts +126 -39
  210. package/src/reference/deck-remark.mjs +600 -0
  211. package/src/reference/deck-source.ts +273 -0
  212. package/src/reference/decks-validate.ts +604 -0
  213. package/src/reference/diagram-layout.ts +3 -1
  214. package/src/reference/figure-symbols.ts +108 -31
  215. package/src/reference/folds-validate.ts +1 -1
  216. package/src/reference/gate-indices.ts +5 -3
  217. package/src/reference/gate-math.ts +169 -25
  218. package/src/reference/lab-katex-validate.ts +30 -15
  219. package/src/reference/lab-math-scope.ts +34 -3
  220. package/src/reference/math-glyphs.mjs +189 -25
  221. package/src/reference/page-notation.ts +69 -15
  222. package/src/reference/references.ts +28 -0
  223. package/src/reference/registry-scope.mjs +34 -0
  224. package/src/reference/registry.ts +41 -11
  225. package/src/reference/rehype-deck.mjs +286 -0
  226. package/src/reference/rehype-heading-anchors.mjs +48 -6
  227. package/src/reference/remark-notation.mjs +249 -40
  228. package/src/reference/source-lines.mjs +65 -0
  229. package/src/reference/types.ts +28 -0
  230. package/src/rules/catalog.ts +257 -4
  231. package/src/rules/course-rules.ts +21 -1
  232. package/src/rules/in-effect.ts +54 -8
  233. package/src/session/PreferenceStore.ts +23 -0
  234. package/src/session/deck-memory.ts +263 -0
  235. package/src/session/reading-memory.ts +243 -0
  236. package/src/styles/deck.css +1150 -0
  237. package/src/styles/global.css +958 -27
  238. package/src/styles/zones.css +277 -0
  239. /package/src/{components → reference}/float-name.ts +0 -0
@@ -128,6 +128,18 @@ _How to apply._ See the `notation` collection schema
128
128
  (`src/compiler/content-schemas.ts`). A letter that exists only so one entry's own `formula` can
129
129
  be stated is a **gloss**, not a card — it creates no curriculum edge.
130
130
 
131
+ Which tier, in one question: *is this letter already defined?* Ask
132
+ `pnpm content symbol <latex>` first. If a card with this meaning exists,
133
+ reference it (`[[key]]` once in prose, bare math after). If the letter already
134
+ carries another meaning in the course, declare it here: `notation.local` on a
135
+ lesson, a gloss in a card's formula — that reuse is the one reason for the
136
+ page tier, and it needs no acknowledgement. Otherwise make a shared card: the
137
+ next lesson that needs it will reference it instead of copying it. Use count
138
+ plays no part — a constant used on one page is vocabulary, a letter used on
139
+ six pages with six meanings is a reused letter each time — and the sign that
140
+ a meaning was not page-bound is that it got defined twice, which
141
+ `definition-repeated` and `meaning-repeated` report.
142
+
131
143
  Do not satisfy this rule by dumping every `[[key]]` a page uses into one
132
144
  trailing "Notation used on this page" (or similarly named) section near the
133
145
  end of the lesson. `LessonFooter.astro` already injects an interactive
@@ -266,13 +278,21 @@ $\theta$ after replacing $\theta$ with $-\theta$.
266
278
 
267
279
  _Enforcement._ Build-enforced. The completeness gate (`gate-math.ts`, `pnpm
268
280
  validate:content`; `astro build` re-runs the same resolver) fails on an
269
- unresolved glyph (`unresolved-glyph`) in every rendered-math context;
281
+ unresolved glyph (`unresolved-glyph`) in every rendered-math context,
282
+ a notation entry's body included: it binds every shared entry, without
283
+ `[[key]]`, and no `notation.local` (`registry-scope.mjs`), and the glossary
284
+ fails `astro build` on an entry whose body did not render;
270
285
  `consistency.ts` adds the corpus-level lints `glyph-unique-in-corpus`
271
- (warning), `weak-local`, `gloss-wants-promoting`, `card-wants-demoting`
272
- (info); a final rendered-trigger/payload parity check backs it at render time,
286
+ (warning: two shared cards on one glyph — a local entry or a gloss may reuse
287
+ a letter), `definition-repeated` (warning: one local key declared in two
288
+ lessons), `meaning-repeated` (info: one symbol and name defined at two sites,
289
+ in any tier), `weak-local` and `card-wants-demoting` (info); the gate reports
290
+ `explain-redundant` (info) for an `\explain` wrap whose bare symbol already
291
+ resolves to the same card; a final rendered-trigger/payload parity check backs it at render time,
273
292
  and a last pass over every built page fails a symbol a figure shows that KaTeX
274
293
  drew outside a notation binding (`figure-symbols.ts`: a lab that called KaTeX
275
- itself — see [V3](visualizations.md#v3)).
294
+ itself — see [V3](visualizations.md#v3)); a plain base-library letter is not
295
+ one, here as in a lesson.
276
296
  This rule is **locked** — the notation layer is this rule, so a course cannot
277
297
  turn it off.
278
298
 
@@ -1253,7 +1273,7 @@ text where every other card shows a name.
1253
1273
 
1254
1274
  _How to apply._ Keep the name to a few words with no sentence punctuation or
1255
1275
  relative clause. If the symbol needs a description, promote the gloss to a
1256
- notation entry (the `gloss-wants-promoting` hint under N4 says when).
1276
+ notation entry (the `meaning-repeated` hint under N4 says when).
1257
1277
 
1258
1278
  _Enforcement._ Build-enforced (`consistency.ts` `checkGlossNameShape`, `pnpm
1259
1279
  validate:content`) — warning `gloss-name-shape` for a name over the word
@@ -1415,7 +1435,11 @@ the same precedence [N22](#n22) uses for decorations:
1415
1435
  object (it needs its own entry, or an `\explain{key}{x}` where it really
1416
1436
  is shorthand). A digit fills a running slot, but a `\mathrm{…}` or
1417
1437
  `\text{…}` label never does: `x_{\mathrm{e}}` is not an instance of
1418
- `x_k`. Two families on one base that both fit an instance fail
1438
+ `x_k`. A double index written as juxtaposed letters is one slot of that
1439
+ many letters: the family `x_{ij}` (or `K_{ij}`) gives its card to `x_{i1}`,
1440
+ `x_{12}` and `x_{ji}`, a run of as many letters and digits, but not to
1441
+ `x_i`, `x_{ijk}` or `x_{i+1}`; `x_{i,j}` is the same family with two slots.
1442
+ Two families on one base that both fit an instance fail
1419
1443
  `ambiguous-base-fallback`: keep one per page, or `\explain` the instance.
1420
1444
  An entry whose index is fixed (`v_0`, `H_1`, `T_{\mathrm{opt}}`, or one
1421
1445
  marked `fixedIndex: true`) is a single symbol and lends its card to
@@ -1457,6 +1481,10 @@ or `d` there needs a card too.
1457
1481
 
1458
1482
  A subscript on an operator or a bracket is not an index: `\sum_{i=1}`,
1459
1483
  `\lim_{t \to 0}`, `\log_{10}`, `\lVert x\rVert_2`, `\left.f\right\rvert_{x=0}`.
1484
+ Nor is a letter that belongs to a symbol with a card of its own inside the
1485
+ subscript (`b` of `b_n` in `\nabla_{b_n}`; the `b_n` is checked in its own
1486
+ right), or any letter of a subscript that states a condition with a relation
1487
+ (`Z \sim g` in `\mathbb{E}_{Z \sim g}`).
1460
1488
  A superscript is a power (`y^{(m)}` is out of scope). An entry whose `latex`
1461
1489
  has a superscript and no subscript (`j^{(m)}`) is a whole form for that
1462
1490
  symbol and, having no index, still lends its card to its bare base.
@@ -1491,6 +1519,13 @@ says `fixedIndex: true` (a component `x`, a label). An uppercase or Greek
1491
1519
  letter in a subscript (`X_N`, `p_\theta`) is a size or a symbol, not a
1492
1520
  running index, and needs no set.
1493
1521
 
1522
+ A letter is fixed everywhere on the page, so `fixedIndex: true` on the card of
1523
+ `m` also turns a family indexed by it (`r_m`) into a single symbol that lends
1524
+ its card to nothing, `r_2` included. A letter that is the index of a family
1525
+ runs over a set: give its card a `range` rather than `fixedIndex`. The
1526
+ `unresolved-glyph` message for an `r_2` that fails this way names the card
1527
+ that fixed the letter.
1528
+
1494
1529
  ### `\explain` around an indexed symbol
1495
1530
 
1496
1531
  `\explain{key}{v_x}` makes the whole `v_x` one target with `key`'s card. If
@@ -1501,6 +1536,14 @@ or name an entry for the indexed symbol. An index inside a larger wrap
1501
1536
  (`\explain{survival-probability}{S(0,t_i)}`) is the wrap's, as every glyph
1502
1537
  inside one is.
1503
1538
 
1539
+ The wrap claims the indexed symbol, so its base takes no other entry's card:
1540
+ `\explain{component-sd}{\sigma_1}` under the family `\sigma_k` leaves the `\sigma`
1541
+ alone even when the page also has a plain `\sigma`. The other direction nests:
1542
+ an `\explain` inside an instance's own subscript,
1543
+ `\beta_{\explain{regressor-count}{K}}`, still finds the family `\beta_j`, and
1544
+ the instance's wrap goes around it, as
1545
+ `\explain{beta-j}{\beta_{\explain{regressor-count}{K}}}`.
1546
+
1504
1547
  | Code | Severity | When |
1505
1548
  | --- | --- | --- |
1506
1549
  | `index-explained` | error | a subscript on a symbol has no whole form, family or index entry, and a slot has no letter with its own card — a digit, a label, a base-library letter — or the base itself has no card |
@@ -65,6 +65,19 @@ Consequences:
65
65
  is that same unit's position in the reading order. A file is a lesson; its
66
66
  ordinal is its chapter number.
67
67
 
68
+ ### A lesson's two editions
69
+
70
+ A lesson can have two editions: its **chapter**, `<name>.mdx`, and its
71
+ **deck**, `<name>.deck.mdx`, the slide edition ([deck.md](deck.md)). A deck
72
+ beside its chapter is served at `<lesson>/slides/`, and the lesson's one
73
+ sidebar row opens one edition and carries the other as an icon
74
+ ([deck.md](deck.md#in-the-sidebar)). It is not a level of its own and takes
75
+ no chapter number. A
76
+ lesson with a deck and no chapter is **slides-first**: the deck is the lesson,
77
+ at the lesson's own URL, with a chapter number in its part, so the chapters
78
+ after it keep theirs. Inside a deck, `##` opens a section of frames and `###` is
79
+ a frame (rule D1), so levels 4 and 5 read as section and frame there.
80
+
68
81
  ## What the playground demonstrates
69
82
 
70
83
  The playground is the worked example for every level. It was deliberately
@@ -174,7 +187,9 @@ reading order with their descriptions, the competencies the part teaches, the
174
187
  parts it builds on and leads to — so the page cannot fall out of date with the
175
188
  content. Only the two paragraphs and the optional `chain` (the part's argument,
176
189
  one step per line, which no manifest field knows) are authored. `rail={false}` drops the
177
- side rail and keeps the reading list.
190
+ side rail and keeps the reading list. In a course with a deck its reading list
191
+ has the contents list's glyphs and slides column (below); `slides={false}`
192
+ drops them.
178
193
 
179
194
  ### Study time
180
195
 
@@ -232,6 +247,70 @@ cannot leave a stale copy behind on the front page. `descriptions={false}`
232
247
  drops the one-line summaries; `headingLevel={3}` fits it under an existing
233
248
  `##`.
234
249
 
250
+ **Slides.** When the course has a deck, the list says which lessons have
251
+ slides, whatever the reader's sidebar controls show: it answers "what is in
252
+ the course", and the controls only choose what a sidebar row opens.
253
+
254
+ - Each title starts with the glyph of what it opens: a chapter's lined sheet,
255
+ or a slides-only lesson's slide frame. A part's heading starts with
256
+ Overview's two sheets.
257
+ - The counts gain the decks ("4 slide decks"), and a key under them names the
258
+ glyphs the list uses: Chapter, Slides, and Slides for a whole part when a
259
+ part has a deck.
260
+ - A slides column ends every row. A lesson's own deck is a tile on its title
261
+ line. A part's deck is one bar along the chapters it covers, from the first
262
+ covered title to the last, its first cell carrying the part deck's two
263
+ frames; each cell links to that chapter's section of the deck and is named
264
+ as the sidebar names it ("Slides: *chapter*", "Slides 17–25 of the part's
265
+ slides"). A part's deck that skips a chapter breaks into runs; one that
266
+ covers a chapter with a deck of its own runs in a second column. A
267
+ slides-only lesson's tile is out of the tab order, since its title opens the
268
+ same page.
269
+
270
+ A course with no deck renders the list as before. `slides={false}` drops the
271
+ column, the glyphs, the key and the count; `<PartOverview slides={false} />`
272
+ does the same on a part's page.
273
+
274
+ ## When the path is not in the course
275
+
276
+ Author `content/docs/404.mdx` as a splash page. Starlight serves that entry for
277
+ an unknown URL; without it, the fallback page works but every build warns that
278
+ the `docs` entry `404` was not found. A minimal page is:
279
+
280
+ ```mdx
281
+ ---
282
+ title: "404: Page outside the syllabus"
283
+ description: The requested course page could not be found.
284
+ template: splash
285
+ hero:
286
+ title: "404"
287
+ tagline: We followed this URL as far as the course map goes. It does not land on a page.
288
+ pagefind: false
289
+ sidebar:
290
+ hidden: true
291
+ editUrl: false
292
+ ---
293
+
294
+ import { LinkButton } from '@astrojs/starlight/components';
295
+
296
+ <div style="display: flex; justify-content: center;">
297
+ <LinkButton href={import.meta.env.BASE_URL} icon="left-arrow" iconPlacement="start">
298
+ Return to the course
299
+ </LinkButton>
300
+ </div>
301
+ ```
302
+
303
+ Use `import.meta.env.BASE_URL` for a link to the course root: a relative `./`
304
+ would resolve against the missing URL, not against `404.html`, while a bare `/`
305
+ would escape a subpath deployment. In `astro.config.mjs`, set
306
+ `disable404Route: process.env.NODE_ENV === 'production'` in `starlight({ … })`:
307
+ Starlight catches arbitrary missing paths in development, while the docs entry
308
+ owns `/404.html` in a static build without colliding with the injected route.
309
+ The 404 is a utility page, not a chapter: it has no `teaches`, notation,
310
+ knowledge check, numbering, sidebar row
311
+ or lesson exports. Set Astro's top-level `site` to the published origin as well
312
+ so Starlight can write its sitemap; `base` continues to hold any subpath.
313
+
235
314
  ## Where the reader is
236
315
 
237
316
  Two marks, both derived, neither authored:
@@ -4,7 +4,9 @@
4
4
  learning-facing content, independent of the `explico` engine's own version. The
5
5
  declared value is `about.version` in `content/course.config.ts`; the history and
6
6
  the reader-facing notes are in `content/CHANGELOG.md`. Both are surfaced to
7
- readers by the About `[?]` panel.
7
+ readers by the About `[?]` panel. Each chapter and part page also carries the
8
+ version its own content last changed in ([C5](#c5)), kept in
9
+ `content/versions.json`, which the build writes.
8
10
 
9
11
  See [README.md](README.md) for what "Build-enforced" and "Review-only" mean here.
10
12
  The engine's own versioning is a separate policy —
@@ -91,7 +93,9 @@ one commit:
91
93
  2. Rename the heading to `## [x.y.z] - <today>`; add a fresh empty
92
94
  `## [Unreleased]` above it.
93
95
  3. Set `about.version` in `content/course.config.ts` to `x.y.z`.
94
- 4. `pnpm validate:content` — no `knowledge-*` warning.
96
+ 4. `pnpm validate:content` — no `knowledge-*` warning. It rewrites
97
+ `content/versions.json`, where every page changed since the last release
98
+ takes `x.y.z` ([C5](#c5)); commit it in the same commit.
95
99
 
96
100
  `pnpm release:content` does steps 2 and 3 for you.
97
101
 
@@ -106,3 +110,51 @@ edits landed in one commit — it warns until the second half lands.
106
110
 
107
111
  _Exceptions._ None.
108
112
 
113
+ ---
114
+
115
+ ## C5
116
+
117
+ **Each chapter and part page carries the knowledge version its content last
118
+ changed in, kept in `content/versions.json`, which `validate:content` writes
119
+ and the course commits with the change.**
120
+
121
+ Under a chapter's title, its PDF and Markdown say "unchanged since version
122
+ 0.2.0", the release its content last changed in, not the course's current
123
+ version; a chapter changed since the last release says "with changes after
124
+ version 0.4.0, not yet released". The `[?]` panel shows the same line for the
125
+ page it is on, and a part's or the course's PDF carries the newest version
126
+ among its pages. A document whose version did not move keeps its PDF's date
127
+ and identifier, so a release recompiles only what it changed
128
+ (`EXPLICO_LATEX_CACHE`).
129
+
130
+ _Rationale._ The course's version moves at every release, whatever the release
131
+ touched, so stamped on a chapter it only says "built after 0.4.0". A reader
132
+ wants to know whether the chapter they printed is the one on the site. A build
133
+ sees one commit and cannot tell what a page was at an earlier release, so the
134
+ record is a committed file, kept like a lockfile: the build derives it, the
135
+ course commits it.
136
+
137
+ _How to apply._ Nothing by hand. `pnpm validate:content` writes
138
+ `content/versions.json` from the committed one and the content; commit what it
139
+ wrote with every content change, and with every release ([C3](#c3) step 4).
140
+ What counts as the page: its own file, its `.checks.yml` and the assessments it
141
+ lists, the shared notation entries its text uses, the sources it cites, and its
142
+ numbers (`part.chapter` and the float offsets its earlier chapters leave it). A
143
+ page changed while `## [Unreleased]` holds any bullet is "not yet released"
144
+ until the next release; a change with no bullet (a fix [C1](#c1) lets go
145
+ unlisted) keeps the current version. A lab, `content/domain/`, a part's label
146
+ or the engine change a page's PDF without changing its version. Rule off
147
+ (`rules.C5.enabled: false` in the course settings): the file is neither read
148
+ nor written, and every page carries the course's version.
149
+
150
+ _Enforcement._ Build-enforced (`page-versions.ts`). `pnpm validate:content` on
151
+ a developer's machine rewrites a stale `content/versions.json` and says so; in
152
+ CI (`CI` set) it fails with `page-versions-stale` (`syncPageVersions`), so a
153
+ content change cannot land without its record. A file that is not a record
154
+ `validate:content` wrote warns `page-versions-invalid` (`readPageVersions`) and
155
+ is rebuilt, every page counting as new. Partial: which bullet of
156
+ `## [Unreleased]` belongs to which page is not known, so every page changed
157
+ while there is any bullet counts as not yet released.
158
+
159
+ _Exceptions._ None. A course that does not want the stamp turns the rule off.
160
+
@@ -207,9 +207,14 @@ component.
207
207
  _Enforcement._ Partly build-enforced. `pnpm validate:content` follows each
208
208
  lesson's relative imports to the course's components and fails one that
209
209
  imports `katex` itself (`lab-katex-validate.ts`, `lab-katex-direct`, an
210
- error), naming the chain of imports that reaches it. `astro build` then reads
210
+ error), naming the chain of imports that reaches it, once for every lesson that
211
+ reaches the file; a lesson that reaches none is clean. `astro build` then reads
211
212
  every figure it rendered and fails a symbol KaTeX drew outside a notation
212
- binding (`figure-symbols.ts`, rule [N4](notation.md#n4)); a course's browser
213
+ binding (`figure-symbols.ts`, rule [N4](notation.md#n4)), listing every page
214
+ that has one rather than the first. A plain base-library `d`, `e`, `i` or `π`
215
+ (`renderLabMath` leaves it without a card, as a lesson's math does) is not such
216
+ a symbol, in a power too; a decorated or styled one is, and a library letter as
217
+ an index is rule [N23](notation.md#n23)'s, checked as the lab renders. A course's browser
213
218
  test runs the same `unboundFigureSymbols` on a hydrated figure's
214
219
  `outerHTML`, which also holds what an island draws only in the browser, as
215
220
  the playground's `labs.spec.ts` does. The same build warns about a figure
@@ -0,0 +1,37 @@
1
+ # AGENTS.md
2
+
3
+ This course is built on [explico](https://www.npmjs.com/package/explico). Read
4
+ `node_modules/explico/docs/authoring/agents.md` first: it is the engine's guide
5
+ for an agent working on a course, and it ships with the version installed here.
6
+ This file holds only what is particular to this course. Two complete courses to
7
+ compare against — the playground, which the engine's docs cite as
8
+ `playground/…`, and credit-and-derivatives — are downloadable from
9
+ https://danieltuzes.github.io/explico/start/ ([`sources.json`](https://danieltuzes.github.io/explico/downloads/sources.json)).
10
+
11
+ ## The loop
12
+
13
+ - `pnpm content help` lists the CLI. Start every task there, not with a file
14
+ hunt: `content context <lesson>` for a lesson's scope, `content rules
15
+ [--element …]` for the rules this course has on, `content rule <ID>` for one
16
+ of them whole, `content symbol <latex>` before declaring any notation.
17
+ - While writing math: `content equation "<latex>" --lesson <id>`; after:
18
+ `content bindings <lesson>` and `content check <lesson>`.
19
+ - Before handing back: `pnpm validate:content` (passes on its first run; commit
20
+ `content/versions.json` if it changed), `pnpm content check --changed`,
21
+ `astro check`, `astro build`.
22
+ - Every content change adds a bullet under `## [Unreleased]` in
23
+ `content/CHANGELOG.md` (rule C1).
24
+ - Upgrading `explico`: `pnpm content migrate` prints exactly the sections to
25
+ apply.
26
+
27
+ The procedures above are project skills under `.claude/skills/` and the
28
+ `rules-reviewer` agent under `.claude/agents/`, installed by
29
+ `pnpm content init-agent --write` from the engine's `docs/course-agent/`;
30
+ rerun it after an upgrade to refresh them.
31
+
32
+ ## This course
33
+
34
+ <!-- What an agent needs that the engine's guide cannot know: the course's
35
+ subject and audience, where its sources live, its own `X` rules and why,
36
+ parts that are off limits, how it deploys. Keep it short; it is read on
37
+ every turn. -->
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: rules-reviewer
3
+ description: Reviews one explico lesson against the guide-only authoring rules (the ones no script checks) in a fresh context, and returns findings with line numbers in at most a thousand tokens. Use before handing a drafted or revised lesson back; the draft-lesson skill invokes it. Give it the lesson id and, when the course is not the playground, the course package path.
4
+ tools: Bash, Read, Grep, Glob
5
+ model: inherit
6
+ effort: medium
7
+ ---
8
+
9
+ You review one lesson of an explico course against the rules a script cannot
10
+ check. You are the second reader the drafter does not have: fresh context,
11
+ only the rules and the lesson in it. Return findings, not a rewrite.
12
+
13
+ Inputs: a lesson id such as `projectiles.projectile-range`, and the course
14
+ package directory (the one whose `package.json` has the `content` script;
15
+ `playground/` in the explico repository). Run the CLI from that directory
16
+ with `pnpm -s content …`.
17
+
18
+ 1. `pnpm -s content check <lesson>`: if it fails, report the failure first and
19
+ stop — a build error outranks style.
20
+ 2. `pnpm -s content context <lesson> --json > /tmp/ctx.json` is too big to
21
+ read whole; instead `pnpm -s content context <lesson>` for the header, then
22
+ read the lesson file (`content/docs/<slug>.mdx`) with line numbers.
23
+ 3. `pnpm -s content rules --enforcement guide` lists the rules this course has
24
+ on that no script checks, by element, each with a one-line summary. Apply
25
+ only those; a rule absent from the list is off. Open `pnpm -s content rule
26
+ <ID>` only when a summary does not settle a doubt.
27
+ 4. Read the lesson once, rule list beside it, and note each place a guide-only
28
+ rule is not followed: the rule id, the line, what you saw, the fix in one
29
+ clause. A JSX prop string (`caption="…"`) is not prose for the math rules.
30
+ 5. Return at most twelve findings, highest impact first, as lines of the form
31
+ `L<line> <ID>: <what> — <fix>`, then one line `ok: <rules checked>` naming
32
+ the rule ids you applied and found satisfied. No preamble, no praise, no
33
+ restatement of the rules. Under a thousand tokens.
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: draft-lesson
3
+ description: Draft or revise an explico lesson with the content CLI doing the lookups — context, rules in effect, symbol lookup before declaring notation, equation and bindings probes before a build, check, the content changelog. Use when asked to write, extend or fix a lesson, a notation entry or an assessment.
4
+ ---
5
+
6
+ # Draft a lesson
7
+
8
+ Read with the CLI, not by opening files; every verb prints a bounded digest
9
+ and `--json` for the whole. Run them from the course package (the directory
10
+ whose `package.json` has the `content` script); `pnpm content help` lists them.
11
+
12
+ 1. **Scope.** `pnpm content context <lesson>`: the lesson plus exactly its
13
+ notation, references, assumptions and sources (a new lesson:
14
+ `pnpm content new lesson <id>` scaffolds it).
15
+ 2. **Rules.** `pnpm content rules --element <prose,table,…>` lists only the
16
+ rules this course has on, with enforcement and codes; `pnpm content rule
17
+ <ID>` prints one whole, example included. A rule absent from the list is
18
+ off: neither apply it nor flag it.
19
+ 3. **Notation, one question per symbol: is it already defined?**
20
+ `pnpm content symbol <latex>`. A card with this meaning exists → reference
21
+ it: `[[key]]` once in prose, bare math after; `\explain{key}{…}` only for a
22
+ compound or a colliding glyph. The letter already means something else in
23
+ the course → `notation.local` on this lesson (a gloss, in a card's
24
+ formula). Neither → a shared card (`pnpm content new term <key>`), so the
25
+ next lesson references it instead of copying it. Use count plays no part.
26
+ 4. **Math, while writing.** `pnpm content equation "<latex>" --lesson <id>`
27
+ resolves one expression the way the gate will and exits 1 when the build
28
+ would refuse it; `pnpm content bindings <lesson>` shows every span's
29
+ bindings, unresolved glyphs and redundant wraps.
30
+ 5. **Check.** `pnpm content check <lesson>` (schema, references, MDX → KaTeX;
31
+ exits non-zero on an error), then `pnpm content diagnostics --lesson <id>`
32
+ for the rest; the counts come first, the lines on request.
33
+ 6. **Record.** A bullet under `## [Unreleased]` in `content/CHANGELOG.md`
34
+ (rule C1); `pnpm validate:content` rewrites `content/versions.json`, which
35
+ is committed with the change (rule C5). Guide-only rules (`enforcement
36
+ guide`) have no check: before handing back, have the `rules-reviewer`
37
+ agent review the lesson, or review the draft against them yourself.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: migrate-course
3
+ description: Upgrade a downstream explico course to a newer engine version — record the old version, install the new one, apply exactly the migration sections between them with `content migrate`, verify. Use when asked to update, upgrade or migrate a course's explico dependency, including a PATCH.
4
+ ---
5
+
6
+ # Upgrade a course's engine
7
+
8
+ Every `explico` bump is a migration task, PATCH included: each release has a
9
+ section in the shipped `docs/authoring/migrating.md` that either gives exact
10
+ work or says no course changes are required.
11
+
12
+ 1. Record the installed version: `node -p "require('explico/package.json').version"`.
13
+ 2. `pnpm add explico@<version>` — without `-D`, so pnpm updates whichever block
14
+ already lists `explico`.
15
+ 3. `pnpm content migrate` prints exactly the sections to apply, from the
16
+ version `package.json` pinned at HEAD to the one now installed, oldest
17
+ first (`--from`/`--to` override either end), and refuses when a section is
18
+ missing. Apply every one: required and conditional changes across
19
+ `content/`, `content/course.config.ts`, Astro/source wiring, course scripts,
20
+ dependencies and generated files. Do not use `CHANGELOG.md` alone as the
21
+ procedure, and do not assume a release without a **Breaking** heading
22
+ needs no work. If `pnpm content migrate` is not a verb yet (a course
23
+ whose `scripts/content.ts` predates the shim), read the sections from
24
+ `node_modules/explico/docs/authoring/migrating.md`; the section that
25
+ introduced the shim says how to switch to it.
26
+ 4. The checks each section ends with, then: `pnpm validate:content` (passes on
27
+ its first run; commit `content/versions.json` if it changed),
28
+ `pnpm content check --changed`, `astro check`, `astro build`, and the
29
+ course's browser tests if it has any.
30
+ 5. Commit the dependency change, the migration edits and the generated files
31
+ together, naming the versions in the message.
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: release-content
3
+ description: Cut a course content release (rules C1–C3, C5) — move content/CHANGELOG.md's Unreleased section to a dated heading and bump about.version together. Use when asked to release or publish a course's content version.
4
+ ---
5
+
6
+ # Cut a content release
7
+
8
+ A course's content has its own version stream: `content/course.config.ts`
9
+ `about.version` (shown to readers in the About `[?]` panel) and
10
+ `content/CHANGELOG.md`, with rules C1–C3 in `docs/authoring/versioning.md` and
11
+ each page's own version in `content/versions.json` (rule C5).
12
+
13
+ 1. Every change since the last release has a bullet under `## [Unreleased]`
14
+ in `content/CHANGELOG.md` (rule C1). If one is missing, add it first.
15
+ 2. Pick the bump (rule C2): MAJOR when a lesson, competency or notation key
16
+ was removed or renamed or a stated outcome changed; MINOR for something
17
+ new; PATCH for a fix or clarification.
18
+ 3. From the course package: `pnpm release:content [major|minor|patch]`. It
19
+ moves `[Unreleased]` to a dated `## [x.y.z]` heading and sets
20
+ `about.version` to match in the same change (rule C3).
21
+ 4. `pnpm validate:content`: no `knowledge-*` warning; it rewrites
22
+ `content/versions.json` — commit that file with the release, never edit it
23
+ by hand.
24
+ 5. Commit the changelog, the config and `versions.json` together.
@@ -0,0 +1,148 @@
1
+ # Diagram scaling on touch screens
2
+
3
+ > **Status.** In `[Unreleased]` (see [`CHANGELOG.md`](../CHANGELOG.md)).
4
+ > Checked in Chromium, in the end-to-end suite and by measurement. **Not
5
+ > checked in WebKit** (Safari, every iOS browser): no WebKit build is available
6
+ > to the agent sessions that wrote it. It uses the technique the print path
7
+ > already uses for WebKit, but a person should look at a diagram on an iPhone
8
+ > before a release ([Checking it](#checking-it)).
9
+
10
+ A diagram is drawn in px at build time. On a phone, the reader's own text-size
11
+ menu shrinks the whole drawing, boxes and text together, so that a wide diagram
12
+ fits the screen instead of scrolling. This page says how, why it is built the
13
+ way it is, and what an agent must not undo.
14
+
15
+ ## What a reader sees
16
+
17
+ - On a touch screen (`@media (hover: none)`) the phone menu has a text-size
18
+ select beside the theme switch: **50, 59, 71, 84, 100%**, smallest first. The
19
+ steps are a geometric series, each 2^(1/4) (about 1.19) times the one below.
20
+ A screen with hover has the browser's zoom instead and shows no control.
21
+ - The choice scales the root font size, so every `rem` follows: prose, math,
22
+ spacing, cards.
23
+ - A diagram in the lesson shrinks by the same factor, **boxes and text
24
+ together**. The playground's `#dia-range-derivation` is 687 px wide; in a
25
+ 390 px viewport it becomes 577, 488, 405 and 344 px at 84, 71, 59 and 50%.
26
+ The column is 358 to 374 px wide there, so at 50% it fits with no
27
+ scrolling, and the widen button ↔ goes away. At 59% it still scrolls a
28
+ little, and the button stays.
29
+ - A diagram in a preview card (`[[dia-…]]`) keeps its size: a card never takes
30
+ its text under 12 px.
31
+ - With no size chosen (100%), on a screen with hover, and in print, **nothing
32
+ changes**.
33
+
34
+ ## How it works
35
+
36
+ | Piece | Where | Role |
37
+ | --- | --- | --- |
38
+ | The steps | `src/components/starlight/TextSizeControl.astro` | Builds the list from `round(100 · 2^((i−4)/4))`, `i = 0…4`. Saves the choice in `localStorage` (`explico:display:text-size`); an inline script copies it to `data-text-size` on `<html>` before first paint. 100% is no attribute. A saved value that is not a step (an earlier release's 90, 80, 70) is ignored. |
39
+ | The scale | `src/styles/global.css`, `@media screen and (hover: none)` | One `:root[data-text-size='…']` rule per step below 100 sets `--explico-text-scale` (0.5 to 0.84). `:root[data-text-size]` sets the root `font-size` to `calc(var(--explico-text-scale, 1) * 100%)`. Outside that media query the variable is unset, so `var(…, 1)` is 1. |
40
+ | The markup | `src/components/diagram/Diagram.astro` | `.diagram-scroll > .diagram-frame > .diagram-canvas > svg`. The frame carries `--diagram-width` and `--diagram-height` (the build-time layout size, unitless px; the canvas inherits them). The canvas keeps its inline `width`/`height` in px. |
41
+ | The rules | `Diagram.astro` `<style>` | Under the same media query, inside `:root[data-text-size] .sl-markdown-content`: the **frame** gets `width/height = layout size × scale` and `overflow: hidden`; the **canvas** gets `transform: scale(var(--explico-text-scale))` with `transform-origin: 0 0`. |
42
+ | The widen button | `src/components/starlight/DiagramEnhancer.astro` | Decides "does it overflow" from the scroller's `scrollWidth`. Its `ResizeObserver` watches the scroller **and the frame**, because a size change resizes the frame, not the scroller. |
43
+
44
+ The node text stays `font-size: 13.6px` (`.diagram-node-content`) at every
45
+ size. The scale comes from the transform, not from the font.
46
+
47
+ ## Why it is built this way
48
+
49
+ - **Boxes are measured for 13.6 px text.** `src/reference/diagram-layout.ts`
50
+ (`NODE_FONT_PX`, the insets, the KaTeX formula measure) sizes each box from
51
+ the label and formula at build time. A box can only hold its text if both
52
+ keep their relationship, so the finished drawing is scaled as one thing.
53
+ - **Shrinking only the font is not enough.** That was the first version here:
54
+ the text followed the menu and the boxes did not, so the diagram never got
55
+ narrower and did not fit the phone, which is the point of the menu.
56
+ - **Never scale through the SVG.** Not the `viewBox`, not `width`/`height`
57
+ with a `viewBox`, not a `transform` on a `<g>` or on the `<svg>`. Nodes are
58
+ HTML in a `<foreignObject>`, and WebKit paints that HTML (every KaTeX
59
+ `position: relative` span has its own render layer) **without** the SVG's
60
+ internal transforms: formulas pile up at the SVG origin while their boxes
61
+ stay put. `tests/e2e/site.spec.ts` guards the markup for this. See also the
62
+ comments in `Diagram.astro` and in `global.css` (print).
63
+ - **Scale the HTML canvas around the SVG instead.** The SVG stays at its
64
+ build-time size and the `div` around it takes a CSS `transform`. The print
65
+ path in `global.css` already does exactly this for WebKit, so this is not a
66
+ new technique, only a new trigger. CSS `zoom` was not tried: it is expected to
67
+ go through the SVG root's own scaling in WebKit, the thing being avoided, but
68
+ that is a reading of the engine, not a test.
69
+ - **A frame takes the scaled size.** A transform moves no layout box, so the
70
+ canvas would still be its unscaled size in the flow (extra height, and a
71
+ scroll width of the unscaled width in an engine that counts the layout box).
72
+ The frame has the scaled `width` and `height`, which is what the scroller
73
+ measures, and `overflow: hidden` so the canvas's unscaled layout box adds
74
+ nothing. The scaled canvas exactly fills the frame, so nothing visible is
75
+ clipped.
76
+ - **Only the lesson's own diagrams scale.** The rules are scoped to
77
+ `.sl-markdown-content`. That covers the article and an expanded diagram (the
78
+ enhancer moves the figure to `<body>` inside a `.sl-markdown-content`
79
+ wrapper, to keep every rule scoped that way) and leaves preview cards out.
80
+
81
+ ## Rules for changing it
82
+
83
+ 1. Do not set a `font-size` in `.diagram-node-content` that depends on
84
+ `--explico-text-scale`. That scales the text twice.
85
+ 2. Do not scale through the SVG (see above). A change that adds a `transform`
86
+ or a different `viewBox` to anything inside the `<svg>` breaks WebKit.
87
+ 3. Keep the frame's size and the canvas's transform driven by the same
88
+ variable, or the box and its content disagree.
89
+ 4. Keep `--diagram-width` and `--diagram-height` as unitless numbers on the
90
+ frame: the rules multiply them by `1px` and by the scale.
91
+ 5. A new consumer of a diagram's size (a script, a test) reads the **frame**
92
+ or the scroller's `scrollWidth`, not the canvas's `offsetWidth`, which is
93
+ the unscaled size.
94
+ 6. Course CSS or tests that selected `.diagram-scroll > .diagram-canvas` as a
95
+ child need a descendant selector now.
96
+ 7. Print and the exports must stay as they were: the scaling rules are inside
97
+ `@media screen and (hover: none)`, and the print rules in `global.css` set
98
+ the canvas's own size and transform. A change here must not leak out of
99
+ that media query.
100
+
101
+ ### Changing the steps
102
+
103
+ The list is generated in `TextSizeControl.astro` (`STEPS` and the formula);
104
+ the select and the saved-value check both read it. The CSS cannot be
105
+ generated, so add or remove the matching `:root[data-text-size='…']` rule in
106
+ `global.css`, and the `STEPS` constant in
107
+ `playground/tests/e2e/text-size.spec.ts`. That spec fails if the select's
108
+ values and the formula disagree, and its diagram test fails on a step whose
109
+ CSS rule is missing.
110
+
111
+ ## Checking it
112
+
113
+ ```sh
114
+ # unit tests, then the specs for this feature (Chromium)
115
+ pnpm test
116
+ pnpm --filter explico-playground exec playwright test \
117
+ tests/e2e/text-size.spec.ts tests/e2e/about.spec.ts tests/e2e/site.spec.ts \
118
+ tests/e2e/diagram-fit.spec.ts tests/e2e/print-export.spec.ts
119
+ ```
120
+
121
+ `text-size.spec.ts` checks, at each step on a 390 px phone: the root size, the
122
+ canvas transform (`matrix(s, 0, 0, s, 0, 0)`), the frame's width (`built × s`),
123
+ the scroller's `scrollWidth`, that the node text is still 13.6 px and clipped
124
+ nowhere, that at 50% the 687 px diagram fits and its widen button is hidden, and
125
+ that a screen with hover ignores a saved size.
126
+
127
+ **What Chromium cannot show is WebKit's paint.** Before a release, on an iPhone
128
+ (or Safari's responsive mode, which uses the desktop engine and so is not
129
+ enough on its own), open a lesson with a diagram that has formulas, pick each
130
+ size in the phone menu and check that:
131
+
132
+ - every formula sits inside its box (none at the top-left corner of the
133
+ diagram);
134
+ - the arrows meet the boxes;
135
+ - the widen button ↔ appears at 59% and 71% and goes at 50%;
136
+ - a preview card for the diagram is full size;
137
+ - printing the page (Share → Print) still shows the whole diagram.
138
+
139
+ ## Known limits
140
+
141
+ - Text at 50% is 6.8 px on screen (13.6 × 0.5). That is the cost of the step,
142
+ and the reader's choice; there is no floor for a diagram.
143
+ - A diagram wider than the column after scaling still scrolls, and the widen
144
+ button still applies. The expanded view is capped to the viewport as before.
145
+ - Boxes and text shrink together, so the drawing keeps its proportions; the
146
+ layout is not recomputed at the smaller size.
147
+ - A saved size from the earlier steps (90, 80, 70) is ignored once: the page
148
+ opens at 100% until the reader picks again.