explico 0.12.0 → 0.13.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 (279) hide show
  1. package/CHANGELOG.md +1122 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +4 -3
  4. package/dist/compiler/astro.js +45 -15
  5. package/dist/compiler/cli.d.ts +26 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +9 -1
  8. package/dist/compiler/collections.js +55 -16
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +14 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +87 -6
  51. package/dist/compiler/manifest.js +174 -31
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +2 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +2 -2
  61. package/dist/reference/base-operators.mjs +2 -2
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figures-validate.d.ts +1 -1
  88. package/dist/reference/gate-abbreviations.d.ts +1 -1
  89. package/dist/reference/gate-abbreviations.js +1 -1
  90. package/dist/reference/gate-course-rules.d.ts +4 -3
  91. package/dist/reference/gate-course-rules.js +9 -4
  92. package/dist/reference/gate-heading-length.d.ts +2 -0
  93. package/dist/reference/gate-heading-length.js +7 -5
  94. package/dist/reference/gate-heading-structure.js +26 -0
  95. package/dist/reference/gate-indices.d.ts +35 -0
  96. package/dist/reference/gate-indices.js +299 -0
  97. package/dist/reference/gate-math.d.ts +32 -2
  98. package/dist/reference/gate-math.js +68 -12
  99. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  100. package/dist/reference/gate-mdx-expression.js +29 -0
  101. package/dist/reference/gate-part-pages.d.ts +1 -1
  102. package/dist/reference/gate-prop-text.d.ts +28 -0
  103. package/dist/reference/gate-prop-text.js +106 -0
  104. package/dist/reference/gate-prose-notation.d.ts +3 -1
  105. package/dist/reference/gate-prose-notation.js +4 -2
  106. package/dist/reference/gate-retired-keys.d.ts +35 -0
  107. package/dist/reference/gate-retired-keys.js +58 -0
  108. package/dist/reference/gloss.d.ts +2 -0
  109. package/dist/reference/katex-measure.d.ts +17 -0
  110. package/dist/reference/katex-measure.js +223 -0
  111. package/dist/reference/keyed-float-validate.js +12 -0
  112. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  113. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  114. package/dist/reference/lab-math-scope.d.ts +1 -0
  115. package/dist/reference/lab-math-scope.js +13 -2
  116. package/dist/reference/math-glyphs.d.mts +94 -2
  117. package/dist/reference/math-glyphs.mjs +851 -46
  118. package/dist/reference/mixed-content.d.ts +8 -3
  119. package/dist/reference/mixed-content.js +4 -2
  120. package/dist/reference/page-notation.d.ts +34 -4
  121. package/dist/reference/page-notation.js +54 -2
  122. package/dist/reference/references.d.ts +11 -1
  123. package/dist/reference/references.js +6 -0
  124. package/dist/reference/registry.js +104 -10
  125. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  126. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  127. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  128. package/dist/reference/remark-citation.mjs +0 -0
  129. package/dist/reference/remark-notation.mjs +84 -20
  130. package/dist/reference/render-lab-math.d.ts +2 -0
  131. package/dist/reference/render-lab-math.js +17 -5
  132. package/dist/reference/sections-validate.d.ts +28 -3
  133. package/dist/reference/sections-validate.js +31 -4
  134. package/dist/reference/sections.d.mts +24 -6
  135. package/dist/reference/sections.mjs +92 -17
  136. package/dist/reference/tables-validate.d.ts +1 -1
  137. package/dist/reference/tables-validate.js +24 -1
  138. package/dist/reference/text-measure.d.ts +10 -0
  139. package/dist/reference/text-measure.js +41 -0
  140. package/dist/reference/text-widths.d.ts +10 -0
  141. package/dist/reference/text-widths.js +1238 -0
  142. package/dist/reference/types.d.ts +64 -8
  143. package/dist/rules/catalog.js +191 -30
  144. package/dist/rules/in-effect.d.ts +1 -1
  145. package/dist/rules/rules.schema.json +79 -24
  146. package/dist/rules/settings.d.ts +1 -1
  147. package/dist/rules/settings.js +7 -0
  148. package/dist/session/PreferenceStore.d.ts +18 -1
  149. package/dist/session/PreferenceStore.js +29 -1
  150. package/docs/authoring/README.md +32 -21
  151. package/docs/authoring/agents.md +38 -7
  152. package/docs/authoring/components.md +111 -6
  153. package/docs/authoring/migrating.md +663 -0
  154. package/docs/authoring/notation.md +281 -29
  155. package/docs/authoring/retired.md +1 -0
  156. package/docs/authoring/structure-rules.md +10 -41
  157. package/docs/authoring/structure.md +53 -2
  158. package/docs/authoring/visualizations.md +144 -18
  159. package/package.json +12 -7
  160. package/src/compiler/astro.ts +57 -17
  161. package/src/compiler/cli.ts +143 -35
  162. package/src/compiler/collections.ts +92 -19
  163. package/src/compiler/content-schema.ts +108 -0
  164. package/src/compiler/content-schemas.ts +391 -0
  165. package/src/compiler/course-config.ts +86 -15
  166. package/src/compiler/exports/combined-html.ts +669 -0
  167. package/src/compiler/exports/download-index.ts +36 -0
  168. package/src/compiler/exports/export-chapters.ts +233 -0
  169. package/src/compiler/exports/export-pages.ts +44 -0
  170. package/src/compiler/exports/html-tree.ts +62 -0
  171. package/src/compiler/exports/latex/capture.ts +651 -0
  172. package/src/compiler/exports/latex/compile.ts +168 -0
  173. package/src/compiler/exports/latex/document.ts +479 -0
  174. package/src/compiler/exports/latex/explico.cls +415 -0
  175. package/src/compiler/exports/latex/numbering.ts +74 -0
  176. package/src/compiler/exports/latex/serialize.ts +787 -0
  177. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  178. package/src/compiler/exports/latex-export.ts +255 -0
  179. package/src/compiler/exports/markdown/serialize.ts +632 -0
  180. package/src/compiler/exports/markdown-export.ts +329 -0
  181. package/src/compiler/exports/standalone-html.ts +39 -70
  182. package/src/compiler/exports/static-tree.ts +944 -0
  183. package/src/compiler/exports/zip.ts +88 -0
  184. package/src/compiler/lesson-derivation.ts +13 -8
  185. package/src/compiler/manifest.ts +299 -28
  186. package/src/compiler/render-diagnostics.ts +111 -0
  187. package/src/components/HoverPanel.astro +204 -6
  188. package/src/components/PartOverview.astro +12 -9
  189. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  190. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  191. package/src/components/assessments/AssessmentSet.astro +14 -1
  192. package/src/components/block-heading.ts +38 -0
  193. package/src/components/card-arrow.ts +18 -4
  194. package/src/components/card-carry.ts +406 -0
  195. package/src/components/card-drag.ts +1 -30
  196. package/src/components/card-fetch.ts +87 -0
  197. package/src/components/card-manager.ts +1222 -38
  198. package/src/components/card-memory.ts +5 -1
  199. package/src/components/card-placement.ts +211 -13
  200. package/src/components/card-side.ts +281 -31
  201. package/src/components/citation/CitationLayer.astro +45 -40
  202. package/src/components/diagram/Diagram.astro +123 -44
  203. package/src/components/figure/Figure.astro +11 -0
  204. package/src/components/figure/FloatCaption.astro +4 -17
  205. package/src/components/float-toolbar.ts +274 -0
  206. package/src/components/memo/Memo.astro +21 -10
  207. package/src/components/memo/ShortNote.astro +4 -0
  208. package/src/components/notation/NotationGlossary.astro +22 -17
  209. package/src/components/notation/NotationLayer.astro +300 -79
  210. package/src/components/notation/NotationMath.astro +8 -1
  211. package/src/components/notation/render-notation-math.ts +51 -5
  212. package/src/components/readable-text.ts +25 -0
  213. package/src/components/reference/FloatRefLayer.astro +162 -89
  214. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  215. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  216. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  217. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  218. package/src/components/starlight/LessonExportActions.astro +384 -310
  219. package/src/components/starlight/LessonFooter.astro +47 -9
  220. package/src/components/starlight/PrintController.astro +28 -0
  221. package/src/components/starlight/TextSizeControl.astro +137 -0
  222. package/src/components/starlight/print-controller.ts +380 -0
  223. package/src/components/starlight/section-summary.ts +13 -5
  224. package/src/components/table/Table.astro +13 -21
  225. package/src/components/table/table-truncation.ts +15 -0
  226. package/src/content-config.ts +20 -312
  227. package/src/curriculum/validation.ts +72 -24
  228. package/src/reference/base-abbreviations.mjs +2 -2
  229. package/src/reference/base-operators.mjs +2 -2
  230. package/src/reference/base-roman-labels.mjs +4 -3
  231. package/src/reference/citation-format.mjs +74 -0
  232. package/src/reference/consistency.ts +47 -61
  233. package/src/reference/curriculum-alignment.ts +4 -41
  234. package/src/reference/derived-entries.ts +68 -0
  235. package/src/reference/derived-notation.mjs +121 -0
  236. package/src/reference/diagram-fit.ts +104 -0
  237. package/src/reference/diagram-layout.ts +182 -51
  238. package/src/reference/diagrams-validate.ts +2 -1
  239. package/src/reference/equations-validate.ts +41 -2
  240. package/src/reference/equations.mjs +120 -22
  241. package/src/reference/examples-validate.ts +1 -0
  242. package/src/reference/figure-fit.ts +139 -0
  243. package/src/reference/figure-pictures.ts +96 -0
  244. package/src/reference/figures-validate.ts +2 -1
  245. package/src/reference/gate-abbreviations.ts +1 -1
  246. package/src/reference/gate-course-rules.ts +22 -7
  247. package/src/reference/gate-heading-length.ts +25 -7
  248. package/src/reference/gate-heading-structure.ts +40 -1
  249. package/src/reference/gate-indices.ts +470 -0
  250. package/src/reference/gate-math.ts +102 -13
  251. package/src/reference/gate-mdx-expression.ts +45 -0
  252. package/src/reference/gate-prop-text.ts +145 -0
  253. package/src/reference/gate-prose-notation.ts +19 -5
  254. package/src/reference/gate-retired-keys.ts +101 -0
  255. package/src/reference/gloss.ts +2 -0
  256. package/src/reference/katex-measure.ts +384 -0
  257. package/src/reference/keyed-float-validate.ts +22 -0
  258. package/src/reference/keyed-jsx-floats.mjs +11 -14
  259. package/src/reference/lab-math-scope.ts +15 -1
  260. package/src/reference/math-glyphs.mjs +851 -46
  261. package/src/reference/mixed-content.ts +11 -2
  262. package/src/reference/page-notation.ts +100 -3
  263. package/src/reference/references.ts +14 -0
  264. package/src/reference/registry.ts +154 -8
  265. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  266. package/src/reference/rehype-heading-anchors.mjs +11 -1
  267. package/src/reference/remark-citation.mjs +0 -0
  268. package/src/reference/remark-notation.mjs +84 -20
  269. package/src/reference/render-lab-math.ts +29 -5
  270. package/src/reference/sections-validate.ts +67 -3
  271. package/src/reference/sections.mjs +92 -17
  272. package/src/reference/tables-validate.ts +40 -2
  273. package/src/reference/text-measure.ts +63 -0
  274. package/src/reference/text-widths.ts +133 -0
  275. package/src/reference/types.ts +75 -8
  276. package/src/rules/catalog.ts +205 -29
  277. package/src/rules/settings.ts +10 -0
  278. package/src/session/PreferenceStore.ts +53 -0
  279. package/src/styles/global.css +291 -75
package/CHANGELOG.md CHANGED
@@ -14,6 +14,1126 @@ under **Breaking** below; a PATCH never breaks.
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.13.0] - 2026-10-07
18
+
19
+ ### Breaking
20
+
21
+ - **A float's caption has no chain icon, and an equation's Copy LaTeX
22
+ button moved to its bottom border.** The toolbar on a float's number
23
+ (Added, below) copies the link instead. `a.float-caption__anchor` is gone
24
+ from `FloatCaption.astro` (figures, diagrams) and `Table.astro`, and
25
+ `button.keyed-equation__copy` is now a child of `.keyed-equation` rather
26
+ than of `.keyed-equation__controls`. Course CSS or browser tests that
27
+ target either need the new place: hover the number, then use
28
+ `a.float-toolbar__link` (it carries the same `data-copy-link` name).
29
+
30
+ - **A course with `lessonExports.standaloneHtml` on needs
31
+ `canonicalBaseUrl`** once its landing page or a part's page links anywhere
32
+ else in the course, which a splash hero's actions always do. Those pages
33
+ are exported now (below), and an exported page that links out without the
34
+ base fails the build, as a lesson always did. Set
35
+ `lessonExports.canonicalBaseUrl` to the deployed course root.
36
+
37
+ - **Astro 7.3, Starlight 0.42 and `@astrojs/react` 7.** The exact
38
+ `peerDependencies` pins move: `astro` 7.2.7 → 7.3.5, `@astrojs/starlight`
39
+ 0.41.9 → 0.42.4, `@astrojs/react` 6.0.4 → 7.0.0 and
40
+ `@astrojs/markdown-remark` 7.2.4 → 7.3.1. A course moves the same four in
41
+ its own `package.json`, together: Starlight 0.42 requires `astro` 7.2.10
42
+ and `@astrojs/markdown-remark` 7.3.0 or later. What moved upstream that a
43
+ course can depend on:
44
+ - Starlight 0.42 drops support for browsers older than Chrome 116,
45
+ Safari 17 and Firefox 125.
46
+ - Its mobile menu is a `#starlight__sidebar` popover opened by a plain
47
+ `button.sl-menu-button`: the `<starlight-menu-button>` wrapper, its
48
+ `aria-expanded` and `body[data-mobile-menu-expanded]` are gone. Course
49
+ CSS or scripts that target them match nothing; use `.sl-menu-button`
50
+ and `:popover-open` (`body:has(#starlight__sidebar:popover-open)`).
51
+ - Starlight's unused top-level `tagline` option is removed (a splash
52
+ page's `hero.tagline` is unaffected).
53
+ - `@astrojs/react` 7 removes the `babel` option. A course that passes
54
+ `react({ babel: … })` moves those plugins to `@rolldown/plugin-babel`
55
+ in `vite.plugins`.
56
+ - `astro build` now prints `[WARN] [content] The collection "i18n" does
57
+ not exist or is empty` (twice). It is harmless: Starlight silences it
58
+ through `console.warn`, which Astro 7.3's logger no longer goes
59
+ through.
60
+
61
+ - **A page that is not a chapter no longer downloads on its own** (D8),
62
+ reversing the per-page downloads added earlier in this cycle (no release
63
+ wrote them, but a site deployed from `main` has): the build stops
64
+ writing `downloads/index.html` (the landing page),
65
+ `downloads/<part>/index.html` (a part's page), `downloads/glossary.html`,
66
+ `downloads/curriculum-map.html` and `downloads/figures-and-tables.html`.
67
+ Their content is in `downloads/part/<dir>.html` (a part's page, then its
68
+ chapters) and `downloads/course.html` (all of them). A link to one of the
69
+ old files now 404s; point it at the part's or the course's file. The
70
+ Download menu lists the scopes that contain the page: "This chapter" on a
71
+ chapter, "This part" on a chapter or its part's page, "Whole course" on
72
+ every page, each with the chapter's, part's or course's name; the course
73
+ file is saved under the course's name (`explico-playground-motion.html`).
74
+ A download's accessible name names its scope ("Download this chapter as
75
+ standalone HTML", "… this part …", "… the whole course …"). During
76
+ `astro dev` only a chapter's HTML builds on request.
77
+
78
+ - **`editorialStatus` is retired.** Lessons are reviewed continuously, so
79
+ the draft / in-review / reviewed flag never meant a page was finished; it
80
+ only put a label in front of readers. No collection reads it now and
81
+ nothing shows it: the part page's chapter list, the notation card, the
82
+ glossary entry, the reference list and the citation card lose their
83
+ status label. What a course changes:
84
+ - **Delete the key from every content file.** A page, notation entry,
85
+ abbreviation, competency, assessment, source or track that still sets
86
+ it fails the build with `retired-key`, once per file, with the file and
87
+ line. The check is the new `retired-keys` invariant
88
+ (`reference/gate-retired-keys.ts`), reported under `curriculum` so a
89
+ course's own `validate:content` script fails on it unchanged. In a JSON
90
+ record the key is often the last one, so deleting its line leaves a
91
+ trailing comma.
92
+ - **The manifest schema is now v29**: `lessons[].status`, the
93
+ `editorialStatus` of the notation definitions under `lessons[]` and
94
+ `pages[]`, and the `status` of `notation.definitions[]` and
95
+ `notation.bundles[]` are removed.
96
+ - **The `content` CLI:** `content status` no longer counts drafts
97
+ (`--json` loses `draftDebt`), `content context` loses `lesson.status`,
98
+ and `content new lesson` / `content new term` no longer write the key.
99
+ A course's own `scripts/content.ts` that prints `report.draftDebt` or
100
+ `report.lesson.status` must stop: `tsx` does not type-check, so
101
+ `report.draftDebt.lessons` throws at run time.
102
+ - **Diagnostic codes:** `review-state` and `alignment-review-state` are
103
+ removed with the checks that compared review states. The unrelated
104
+ check that shared `review-state`, a lesson that `teaches` but declares
105
+ no `assumptions`, now reports `missing-assumptions`.
106
+ - **Exports:** `EditorialStatus` and the `status` field of
107
+ `SharedNotationDefinitionInput`, `NotationLessonInput`, the notation
108
+ definition records and `NotationPageBundle` (`explico/reference/types`);
109
+ `LessonDefinition.status` and `SourceDefinition.editorialStatus`
110
+ (`explico/curriculum/validation`); the per-definition `editorialStatus`
111
+ of `NotationLayer`. A course test that builds these inputs drops
112
+ `status`.
113
+ - **CSS hooks:** `.notation-status`, `.glossary-status`,
114
+ `.citation-status` and `.citation-panel-status` no longer exist, and a
115
+ glossary entry's header has two columns, not three.
116
+
117
+ - **`estimateBoxSize` is gone from `explico/reference/diagram-layout`.**
118
+ Diagram boxes are measured now (Changed, below); `nodeBoxSize` takes the
119
+ label and the formula's measured box (`measureFormula`) in its place.
120
+ - **A quoted prop holding `$…$`, `[[…]]` or `[@…]` fails
121
+ `validate:content`** (`markup-in-prop`, invariant `prop-text`). A prop
122
+ reaches the page as plain text, so `short="Speed $v$"` showed its dollars
123
+ in the caption line, the preview card and the "Figures and tables" page,
124
+ and a `[[…]]` or `[@…]` printed as typed; meanwhile the math gate demanded
125
+ a notation entry for the symbol. Write a symbol in a prop in Unicode or
126
+ words, and reference or cite from the prose next to the component. The
127
+ math gates no longer read prop strings. None of the four courses on the
128
+ maintainer's machine has such a prop.
129
+ - **A second `\label{eq:…}` in one display block fails `validate:content`**
130
+ (`eq-label-repeated`). A `$$…$$` block is one numbered equation: only its
131
+ first label named it, and a `[[eq-…]]` to a later one failed with "no
132
+ display equation on the page carries" a label the block visibly holds.
133
+ Give each equation you refer to its own block.
134
+ - **A notation key that starts with a reference prefix fails where it is
135
+ defined.** `[[eq-…]]`, `[[tab-…]]`, `[[fig-…]]`, `[[dia-…]]`, `[[ex-…]]`,
136
+ `[[check-…]]`, `[[sec-…]]` and `[[memo-…]]` are routed by prefix before
137
+ notation keys are looked at, so an entry keyed `ex-ante` or `eq-fake` could
138
+ never be referenced, and its use failed with a float error that never said
139
+ the key was a notation entry. The registry now reports `invalid-key` at
140
+ the definition, naming the prefixes, and `content new term` refuses such
141
+ a key. None of the four courses on the maintainer's machine has one.
142
+ - **A worked-example set or a knowledge check inside a memo or a short
143
+ note fails rule S6** (`heading-nested`). Its own `<h2>` landed under the
144
+ memo's `<h6>`, the heading order jumped, and "On this page" never listed
145
+ it, with no diagnostic. None of the courses on the maintainer's machine
146
+ has one.
147
+ - **A `<Table>` holding two tables fails `validate:content`**
148
+ (`tab-several-tables`). The page showed both under one number and
149
+ caption, and the PDF, LaTeX and Markdown exports kept only the first,
150
+ silently. None of the courses on the maintainer's machine has one.
151
+ - **A citation in a link's text fails `validate:content`**
152
+ (`citation-in-link`). `[as measured in [@id]](https://…)` printed the
153
+ citation as typed on the page and in every export, while the manifest
154
+ counted the source as cited. The lesson's sources are now read the way
155
+ the page reads them, and the message says to cite after the link. None of
156
+ the courses on the maintainer's machine has one.
157
+ - **Rule N23: an index is part of the symbol**
158
+ (`docs/authoring/notation.md#n23`). A subscript on a symbol must be
159
+ explained by the indexed symbol's own entry (`latex: v_0`), a family entry
160
+ whose index runs (`latex: x_k` explains `x_1`, `x_{k+1}`), or an index entry
161
+ for the subscript (`latex: "{}_0"`, Added below); a card on the base alone
162
+ no longer passes. Arithmetic on an index letter with its own card passes
163
+ (`X_{t-1}`), and a subscript on an operator or a bracket is not an index
164
+ (`\sum_{i=1}`). `validate:content` fails with four errors
165
+ (`gate-indices.ts`), in a lesson body located and once per form per page
166
+ (per letter for `index-range`), in a shared formula once per formula:
167
+ `index-explained` (a digit, a label or a base-library `i`, `e`, `d`
168
+ explained by nothing), `index-swallowed` (`\explain{key}{v_x}` where `key`'s
169
+ `latex` has no index), `index-range` (a running index — a lowercase letter
170
+ not marked `fixedIndex` — whose set is stated neither as `range` on its card
171
+ or its family entry nor after a `\quad` on the first display line that uses
172
+ it) and `derived-key-taken`. Notation formulas and assessments get the first
173
+ two from `validate:content` too; lab math (`renderLabMath`) and `<Diagram>`
174
+ formulas fail `astro build` with the same findings as they render. A course
175
+ fixes each form, or waives one place in a lesson body with `{/*
176
+ explico-allow N23: <reason> */}`; `rules.N23.enabled: false` turns all of it
177
+ off. `docs/authoring/migrating.md` has the one-pass migration.
178
+ - **An indexed entry no longer lends its card to other symbols** (rule N4).
179
+ An entry with a fixed index — digits or a `\mathrm{…}` label, `v_0`,
180
+ `H_0`, `T_{\mathrm{opt}}` — lent its card to the bare base and to every
181
+ other index: `H_1` opened "Null hypothesis". It lends to nothing now. A
182
+ family entry (`\alpha_i`) lent its card to the bare symbol (`\alpha`, a
183
+ different object); it lends only to its own instances now. A function form
184
+ still lends by its head: `D(0,t)` explains a bare `D`, and `V_0(1_t)`
185
+ explains `V_0(1_2)` but no longer a bare `V`. A symbol that had a card only
186
+ by borrowing one fails `unresolved-glyph`, and the message names the
187
+ indexed entry with that base: give it its own entry, or an
188
+ `\explain{key}{\alpha}` where it really is the family's shorthand. An
189
+ entry with a superscript only (`j^{(m)}`) still lends to its base.
190
+ - **The manifest schema is now v31 (additive)**: the `notation` bundle of
191
+ `lessons[]` and `pages[]` gains `derived`, the derived cards (Added below)
192
+ its body math uses; a notation definition record carries `reads`, `range`,
193
+ `fixedIndex` and an index entry's `formulaTemplate` when they are set; a
194
+ bundle definition carries `range`. `diagnostics.math` carries the N23
195
+ codes.
196
+ - **`resolveMathGlyphs` resolves an indexed symbol differently**, which
197
+ changes a card a reader sees without a content edit; a course records it
198
+ in its own changelog (rule C1). Under a power or a mark, a whole form
199
+ still wins (`v_0^{2}` is `v_0` squared; Fixed, below). An instance takes
200
+ its family's card ahead of a plain entry for its base (`x_1` opens the
201
+ `x_k` card, not the `x` card), through a font-styled family too
202
+ (`\mathbf{v}_k` explains `\mathbf{v}_1`), and only through a family whose
203
+ index has the same shape — the same number of comma-separated slots, and
204
+ no fixed label in a running slot (`x_{\mathrm{e}}` is not an instance of
205
+ `x_k`). The whole instance is one target. Two families on one base that
206
+ both fit an instance fail `ambiguous-base-fallback`, as two lending
207
+ entries did before.
208
+ - **Rule S5 is retired into rule N4, and `CourseConfig.conventions` is no
209
+ longer read.** Its check (`convention-single-definition`) matched one
210
+ finance course's wording and one key per course, and never fired in any
211
+ course. A `conventions` block now does nothing and warns
212
+ `config-deprecated-key`; the type keeps it as a deprecated `unknown` until
213
+ the next MINOR. `CourseConventions` is gone, and `runConsistencyChecks`
214
+ no longer takes `catalog` or `conventions`. Rule N4 says how to write a
215
+ convention instead: on its notation entry, `meaning` names the other
216
+ choice and the body says how to convert.
217
+
218
+ ### Added
219
+
220
+ - **A toolbar on every float's number.** Hovering or focusing the number of
221
+ an equation, a figure, a table or a diagram shows Pin and Dock just above
222
+ it and Copy link just below it, none of them over the number. Pin and
223
+ Dock keep the float's own reference card, pinned next to the float or
224
+ docked beside the article; Copy link copies the float's named link, as
225
+ the chain icon did. A click on the number itself still only puts its
226
+ anchor in the address bar. An equation's Copy LaTeX sits on its bottom
227
+ border, under the number, or slid along the border in an equation too
228
+ short for both, and shows while the equation is hovered. A touch screen
229
+ has no toolbar. The toolbar is `components/float-toolbar.ts`; it opens
230
+ the card through the new `keepCardFor(layer, anchor, mode)` in
231
+ `card-manager.ts`, and a card source can name an item's own anchor
232
+ (`CardSource.ownAnchor`), so a card pinned from a number comes back after
233
+ a reload like any kept card.
234
+
235
+ - **A docked card widens from its right edge, and a width menu sizes
236
+ them all.** A card beside the article has a handle along its right edge:
237
+ drag it, or focus it and use the arrow keys (Shift for bigger steps);
238
+ Home or a double click gives the card back the column's width. It may
239
+ reach past the "On this page" rail: the rail then goes under the cards
240
+ (`data-card-over-rail` on `<html>`), and its `>>>` collapses it. Once a
241
+ reader has resized a card, a width menu joins the switch at the top of
242
+ the side column: Default, Wide (65 characters a line) or Very wide (90),
243
+ counted in the card's own text so they follow the text size; it lists a
244
+ size only when it fits beside the article with both panels collapsed. A
245
+ size applies to every docked card and the next ones; a card resized by
246
+ hand reads "Custom". The width is remembered with the card, and the
247
+ size per reader (`card-side.ts`, `card-manager.ts`, `HoverPanel.astro`,
248
+ `card-memory.ts`).
249
+ - **A lab's picture is in the built page, or the build says so** (rule
250
+ V3). A lab draws its plot in the browser, after the page loads; until
251
+ then the page held an empty box, so a reference card from another page,
252
+ print without JavaScript and the HTML export had no picture. The lab's
253
+ wrapper now draws its default picture at build time and hands it to the
254
+ island as children, which the island shows until it has drawn its own
255
+ (the playground's `content/labs/still-plot.ts` shows the pattern). The
256
+ build warns about a figure whose picture is an island with nothing drawn
257
+ in the built page: `lab-picture-undrawn`, a warning
258
+ (`reference/figure-pictures.ts`, run by `compiler/astro.ts`). A reference
259
+ card copies the build's picture until the island has drawn
260
+ (`FloatRefLayer.astro`, `cloneFigureBody`, which used to drop an
261
+ island's children), and `global.css` hides it the moment the live plot
262
+ in `.lab-chart-live` exists.
263
+
264
+ - **A text size on touch screens**: 100%, 90%, 80% or 70%, beside the
265
+ theme switch in the phone menu (and in the header of a tablet held wide).
266
+ A PC has the browser's zoom; a phone's text-size settings are buried in
267
+ its browser's menus. The choice scales the root font size
268
+ (`data-text-size` on `<html>`, `styles/global.css`), so the text, the
269
+ math, the spacing and the cards follow; it is remembered per reader and
270
+ applied before the page first paints, by an inline script that
271
+ `PreferenceStore.ts` builds (`inlineRootAttributeScript`), so storage is
272
+ still touched in that file alone. It shows only where there is no hover,
273
+ never in print, and a card's text keeps a 12 px floor
274
+ (`TextSizeControl.astro`, rendered by `LayoutSocialIcons.astro`, which a
275
+ course already wires as its `SocialIcons` override).
276
+
277
+ - **Carried cards stay on screen** (rules K2–K4). With carrying on, a kept
278
+ card — pinned, docked or moved — scrolls with its trigger until it
279
+ reaches the top edge of the window (under the header, and in the side
280
+ column under its controls row) or the bottom edge, then stays there,
281
+ and follows its trigger again once its place comes back. Cards at one
282
+ edge stack outward from it in the order they were opened, only those
283
+ over each other; when the cards held over each other are taller than the
284
+ window, the older ones collapse to their header row, and a click or the
285
+ keyboard's focus opens one again. A card opened from inside a held card
286
+ moves with it. `heldAt` in `card-placement.ts` is the rule, pure; the
287
+ card manager fixes a held card at its edge (`data-card-held="top"` or
288
+ `"bottom"`, `data-card-collapsed`) from one scroll listener, on only
289
+ while carrying is on and a card is kept. A held card does not print.
290
+
291
+ - **A "Carry cards" switch** (K1, D6–D8). It joins "Article left" in
292
+ the side column's controls row while the column is in use; otherwise it
293
+ shows alone at the article's top right, while a card is kept. Its
294
+ tooltip says what it does. It is off until a reader turns it on, and the
295
+ reader's choice is remembered (`card-carry.ts`, through
296
+ `PreferenceStore`). A course can start every reader with it on: the new
297
+ `cards.carry` course setting (`CourseConfig.cards`, `normalizeCards`),
298
+ default `false`. The manifest schema is now v26: `cards`, the course's
299
+ card settings with the defaults applied, which `LessonFooter.astro`
300
+ hands the page in a hidden `[data-explico-card-config]` element.
301
+
302
+ - **Carried cards come along to every page of the course** (K5, D9):
303
+ by a link, the sidebar, search, Next and Previous, Back and Forward, a
304
+ reload or a typed address. On the page a card was opened on it attaches
305
+ to its trigger again, at its place; anywhere else it is rebuilt from that
306
+ page, fetched once, and held at the window edge it was at, at the same
307
+ place across the window. A figure card brings its plot as it was drawn.
308
+ Closing a card ends its journey; turning carrying off closes the cards
309
+ from other pages. Each tab keeps its own carried set in session storage,
310
+ and every change also writes it as the course's last set in local
311
+ storage, which a tab with no set of its own (a new tab, a browser
312
+ restarted without its tabs) takes. A page the browser brings back from
313
+ its cache takes the set as another page left it. While cards from other
314
+ pages sit on the bottom edge, the page grows by their height, so its
315
+ Next and Previous links scroll clear of them. Card memory per page rests
316
+ while carrying is on. New: `CardSource.build(item, page)` and
317
+ `CardSource.awayItem(item, page)` in `card-manager.ts`, which every card
318
+ layer implements; `card-fetch.ts`, the fetch-once page cache that
319
+ `FloatRefLayer.astro` kept to itself until now; the carried set's
320
+ decoder in `card-carry.ts`; and `createSessionPreferenceStore` in
321
+ `PreferenceStore.ts`, still the one module that touches browser storage.
322
+
323
+ - **A page's own symbol, carried, says where it means what it says**
324
+ (K6, D4). Away from its page, its card opens with "Defined in
325
+ *‹page title›* · means this only there", and its definition link goes
326
+ back to that page. When the reader points at a symbol on the new page
327
+ whose notation entry has the same LaTeX, the carried card's arrow to it
328
+ is dashed and grey: the same letters, defined elsewhere. A shared
329
+ symbol's arrow to the same symbol on another page stays solid, and
330
+ pointing at a carried card shows the nearest such place on the page.
331
+ Notation entries carry `data-notation-scope` and `data-notation-latex`,
332
+ a notation card `data-card-scope` and `data-card-glyph`; a source tells
333
+ the manager a trigger's glyph (`CardSource.glyphOf`) and the triggers
334
+ with a glyph (`CardSource.lookalikes`), and the arrow overlay draws the
335
+ dashed kind (`ArrowOverlay.show(…, { lookalike: true })`,
336
+ `.explico-card-arrow.is-lookalike`).
337
+
338
+ - **With carrying on, every kept card links back to where it was opened**
339
+ (K7, D15): "Opened at § 2.1.1" beside its "Go to …", or "Opened at
340
+ (2.1.3)" when it was opened from inside a numbered equation, the worked
341
+ example or knowledge check it was in ("Opened at Example 1.2.1"), or
342
+ "Opened at the start" before a page's first section. A fold's bar and a
343
+ memo are no sections. With carrying off, a kept card stays by its
344
+ trigger and shows no such link. Its arrow says where that is (↑ ↓
345
+ on this page, → on another), and its tooltip says it in full: "Back to
346
+ where you opened this card: Projectile Range, § 2.1.1 From launch to
347
+ landing". The link is the section's or the equation's anchor, so it works
348
+ in a new tab and without JavaScript; with it, the page scrolls to the
349
+ trigger itself and lights it a moment (`.is-card-origin`), after loading
350
+ when the card was opened on another page. A card opened from inside
351
+ another goes back to where the outermost one was opened. The link is an
352
+ `a.hover-panel-action[data-card-opened-at]` the card manager adds; where
353
+ a card was opened travels in the carried set.
354
+
355
+ - **A carried card says whether it means the same everywhere** (K8,
356
+ plan §4). While carrying is on, a kept card shows, at the left of its
357
+ header's buttons, a globe — shared notation, citations, abbreviations,
358
+ numbered references — or a page and "this page", for one of a page's own
359
+ symbols; its tooltip says which. `HoverPanel.astro` renders the mark
360
+ (`[data-card-mark="global"|"page"]`), hidden until the manager shows it.
361
+
362
+ - **LaTeX source and PDF of every chapter** (`lessonExports.latex`). After
363
+ Astro's static build, each chapter's LaTeX folder is zipped to
364
+ `downloads/<slug>-latex.zip`: `main.tex`, explico's book class
365
+ `explico.cls`, the chapter as readable source (named environments for
366
+ memos, short notes, worked examples and knowledge checks; `\label` and
367
+ `\ref` for every cross-reference; the author's `\explain` markers kept
368
+ in the math, printed by the class), its figures, a `latexmkrc` and a
369
+ README. The same folder is compiled with `latexmk` to
370
+ `downloads/<slug>.pdf`, a tagged PDF 2.0 with each chapter's answer key
371
+ at its end. Under LuaLaTeX it is PDF/UA-2 with MathML for every formula
372
+ (veraPDF passes every playground chapter); pdfLaTeX tags less (no
373
+ MathML, table rules untagged, a box broken across pages leaves its
374
+ marked content unbalanced, which poppler reports as "Mismatched EMC
375
+ operator"), so its PDF claims no standard.
376
+ - **Numbers are LaTeX's own.** The class counts parts, chapters,
377
+ sections, equations, floats, worked examples and knowledge checks the
378
+ way the website does, so an edited download renumbers itself. When the
379
+ source is compiled unchanged, every printed number is checked against
380
+ the website's, and a difference fails the build.
381
+ - **Figures stay vector.** A lab or other figure a browser draws is
382
+ captured from the finished site with the course's own Playwright
383
+ Chromium and included as a vector PDF, its caption's live clause
384
+ filled in. explico's `<Diagram>` is drawn as TikZ from the layout Dagre
385
+ gave it, so LaTeX sets its text and math.
386
+ - **A float inside a memo or a worked example** is set where it stands
387
+ with the `float` package's `[H]` (`ExplicoHere` in `explico.cls`), so
388
+ tagging gives the float its own caption. The minipage and `\captionof`
389
+ used before passed PDF/UA-2 under TeX Live 2025, but TeX Live 2026 tags
390
+ the minipage as a Div, which leaves the caption mid-way through the
391
+ box's text (veraPDF, ISO 14289-2 rule 8.2.5.27). Without `float` the
392
+ class falls back to the minipage. CI needs `float` in its TeX Live
393
+ packages.
394
+ - **Reproducible.** The same source compiles to the same bytes, and two
395
+ builds write identical downloads. `main.tex` fixes TeX's random seed
396
+ before tagging loads (tagpdf names a structure namespace with random
397
+ digits) and sets the PDF's file identifier, which the engines would
398
+ derive from the output file's path; a captured figure's PDF carries the
399
+ course's content date, not the moment Chromium printed it. A figure's
400
+ alt text keeps its punctuation (`45°, where`), which its braces
401
+ protect.
402
+ - **Figure labels match the book.** A captured figure's SVG labels are
403
+ set in KaTeX's Computer Modern, which explico's stylesheet loads on
404
+ every page, so any machine draws the same figure, and they are sized
405
+ so the most common one prints at 9 pt (`\footnotesize`) once LaTeX
406
+ fits the figure to the page. An Observable Plot axis title moves clear
407
+ of its grown tick labels; no label grows into another. A figure that
408
+ still draws text in one of the machine's fonts is named in a warning.
409
+ - **Engine.** LuaLaTeX by default (TeX Live 2025 or later);
410
+ `EXPLICO_LATEX_ENGINE=pdflatex` switches to pdfLaTeX. In CI (`CI` set)
411
+ or with `EXPLICO_LATEX_STRICT=1`, a missing engine or browser fails the
412
+ build; elsewhere the build warns, still writes the source zips, and
413
+ leaves an uncaptured figure as a placeholder. explico ships the TeX
414
+ Live package list as `src/compiler/exports/latex/texlive-packages.txt`.
415
+ - `lessonExports.latexPaper` sets the paper size: `letter` (default,
416
+ LaTeX's own) or `a4`.
417
+ - **Each part and the course as a book** (plan step 6):
418
+ `downloads/part/<dir>-latex.zip` and `.pdf`, `downloads/course-latex.zip`
419
+ and `.pdf` (`bookSource`), with cells in the Download menu. A title page
420
+ from About (the course or part, the content version, the authors, the
421
+ website), the contents (parts, chapters, sections) and the lists of
422
+ figures, tables and diagrams that have entries; then each part, opening
423
+ on its own page (`\ExplicoPart`, numbered as on the website) followed by
424
+ its page's text and objectives, and its chapters in order; then the
425
+ appendices: the book's notation, in order of first appearance (a symbol
426
+ one chapter defines says so), and the course's abbreviations. A
427
+ reference between chapters is a `\ref`; each chapter keeps its own
428
+ reference list, numbered as on its page, its citation keys prefixed with
429
+ the chapter; its notation is in the appendix, and under its title only
430
+ the link to its live version. Every number in a book is checked against
431
+ the website. Contents and lists are black, their number columns wide
432
+ enough for `2.1` and `2.1.10`.
433
+ - **Every document compiles at once**, up to one per core. On the
434
+ playground's 8 cores the LaTeX step takes 31 s for its 7 documents, where
435
+ the 4 chapters alone took about 55 s one after another; the books add
436
+ 79 s of compile time (parts 48 s, course 31 s), and the course book sets
437
+ the wall time. The build logs these times.
438
+ - **The manifest schema is now v27**: `lessonExports.latex` and
439
+ `lessonExports.latexPaper`, and every `assessmentContent` item carries
440
+ its `type`, a choice item's `options` and `correctOptionId`, and a
441
+ numeric item's `answer`.
442
+ - **Every part and the whole course download as one offline HTML file**
443
+ (`lessonExports.standaloneHtml`; `compiler/exports/combined-html.ts`):
444
+ `downloads/part/<dir>.html` is the part's page, then its chapters;
445
+ `downloads/course.html` is the landing page, the curriculum map, the
446
+ figures and tables, every part and the glossary last. Each opens with a
447
+ list of contents. They are assembled from the pages the build wrote, not
448
+ rendered again, so each page keeps its own notation, citation and
449
+ float-reference cards, labs and checks. Every id in a page gains the page
450
+ as a suffix (`eq-range--projectiles-projectile-range`), with every
451
+ reference to it (links, ARIA id lists, `for`, `popovertarget`,
452
+ `url(#…)`); a suffix keeps the `eq-`, `fig-` and `cite-` prefixes the card
453
+ layers read. A link to another page of the same file becomes a link into
454
+ it, so its hover preview works offline. A React island whose `prefix`
455
+ an earlier page used gets its own, with the ids React renders from it, so
456
+ hydration matches. Each page is a section with `content-visibility: auto`
457
+ and starts a new sheet when printed.
458
+ - The notation layers of a page share one card source, so a document
459
+ with several (each page's) serves every page's symbols; a citation
460
+ card finds its reference through the marker's link instead of building
461
+ `#cite-<n>`, so a file with several reference lists numbered from 1
462
+ shows the right one. A kept citation card is remembered by its
463
+ reference-list item, not its number.
464
+ - The print controller moved out of the header into the page's footer
465
+ (`PrintController.astro`, `print-controller.ts`), inside `<main>`: a
466
+ downloaded page keeps only `<main>`, and lost Ctrl+P's lab loading with
467
+ the header.
468
+ - **Markdown of every chapter, part and the course** (`lessonExports.markdown`;
469
+ `compiler/exports/markdown-export.ts`, `markdown/serialize.ts`), built with
470
+ the site (D2) from the same reading trees as the LaTeX export:
471
+ `downloads/<slug>-markdown.zip` holds `<name>-markdown/lesson.md` and its
472
+ `assets/`; `downloads/part/<dir>-markdown.zip` an `index.md` (the part's
473
+ page, then its contents) and one file per chapter
474
+ (`02-01-projectile-range.md`); `downloads/course-markdown.zip` an
475
+ `index.md`, each part's page and every chapter, with shared `assets/` and
476
+ relative links between the files. The Download menu gains a Markdown
477
+ column.
478
+ - GitHub's flavour with `$…$` and `$$…$$` math for remark-math, KaTeX or
479
+ MathJax, the author's `\explain` markers stripped. Headings keep their
480
+ numbers, a display equation its `\tag{…}`, each float its number and
481
+ captions, and every anchor an `<a id>`; a reference into the download
482
+ is a link into its file, any other goes to the website. A bar in table
483
+ math is `\vert`/`\Vert`, which a pipe table can hold. Memos, notes,
484
+ examples and checks are titled quotes, always open; Starlight's asides
485
+ are GitHub alerts (`[!NOTE]`, `[!TIP]`, `[!WARNING]`, `[!CAUTION]`).
486
+ Each chapter ends with its references, its notation and the answers to
487
+ its checks (D3).
488
+ - A figure a browser draws is a PNG at two device pixels per CSS pixel,
489
+ taken from the same clean page as its vector PDF; an image is the
490
+ site's own file; an explico `<Diagram>` is a PNG with its boxes and
491
+ arrows listed as text. The capture now also draws a diagram, and the
492
+ LaTeX and Markdown exports share one reading of the chapters and one
493
+ capture of their figures (`compiler/exports/export-chapters.ts`).
494
+ - New dependencies: `mdast-util-to-markdown`, `mdast-util-gfm-table`,
495
+ `mdast-util-math`, the versions Astro already installs.
496
+ - A part's page in print exports leaves out its generated stats line and
497
+ chapter list (the export's own contents replace them) and keeps its
498
+ objectives.
499
+ - **The manifest schema is now v28**: `lessonExports.markdown`.
500
+ - **`downloads/index.json`** lists every file the exports wrote, by its
501
+ path below `downloads/`, with its size in bytes
502
+ (`compiler/exports/download-index.ts`). The Download menu reads it.
503
+ - **Ctrl+P and Cmd+P load the labs before printing**, as the Print button
504
+ does: the page catches the shortcut, loads every lab that will be on
505
+ paper, and then opens the print dialog. A downloaded page keeps this.
506
+ The browser's own File → Print cannot wait, so a lab that never loaded
507
+ prints a one-line note saying how to include it.
508
+
509
+ - **Rule V7: a figure's picture fits on one screen.** The default slot of a
510
+ `<Figure>` (plot, key and tables) takes at most `maxPercent` of the
511
+ viewport height, 80% unless the course sets `rules.V7.maxPercent`, so the
512
+ reader sees the picture and its caption line together. The height is
513
+ only known in a laid-out page, so the check runs in a course's Playwright
514
+ suite: `explico/reference/figure-fit` exports `checkFigureFit(page,
515
+ { maxPercent?, allow? })`, which opens folded figures, waits for islands
516
+ to hydrate and the height to settle, and returns a `figure-too-tall`
517
+ finding (warning) per picture over the budget; `lessonsWithFigures(manifest)`
518
+ lists the pages to open, and `allow` exempts a figure by anchor with a
519
+ stated reason. The rule page shows the test to add; the playground runs it
520
+ at a laptop and a phone viewport.
521
+
522
+ - **Rule V8: a diagram is no wider than the lesson column.** A `<Diagram>`
523
+ that lays out wider than `columnWidth` px (707 unless the course sets
524
+ `rules.V8.columnWidth`: the lesson column at a 1366px-wide window) gets a
525
+ `dia-too-wide` warning naming the lesson file and the diagram `id`. Only
526
+ the rendered page knows a diagram's width (a wrapper component's nodes
527
+ never reach the manifest walk), so `Diagram.astro` marks a diagram over
528
+ its limit with `data-diagram-too-wide`, `astro build` reports each mark
529
+ as it finishes (`oversizedDiagrams` in `explico/reference/diagram-fit`)
530
+ and writes them to `build/render-diagnostics.json`, and `astro dev`
531
+ prints the same warning as the page renders. `content warnings`,
532
+ `content diagnostics`, `content status`, `content context` and
533
+ `content check` list the last build's findings beside the manifest's,
534
+ under the `render:` source (`render:dia-too-wide`). `warnAboveWidth={false}`
535
+ still opts a diagram out, and a number in it still sets that diagram's
536
+ own limit.
537
+
538
+ - **Build-time measurement of a formula and of a label.**
539
+ `explico/reference/katex-measure` exports `measureFormula(latex, fontPx)`:
540
+ the width, height and depth of notation LaTeX as KaTeX lays it out, from
541
+ KaTeX's own layout tree and font metrics plus the widths `katex.css`
542
+ adds, each glyph rounded up to a whole pixel as Chromium on Linux draws
543
+ it. It is never narrower than Chromium's width and at most a few pixels
544
+ wider, which `tests/reference/katex-measure.test.ts` holds against 92
545
+ formulas measured in Chromium (`playground/scripts/measure-katex-widths.ts`).
546
+ `explico/reference/text-measure` exports `textWidth` and `wrapText`,
547
+ which measure text in DejaVu Sans, the widest common system font, from
548
+ an advance-width table generated from the font
549
+ (`scripts/gen-text-widths.mjs`). `renderNotationFormula` also returns the
550
+ `latex` it rendered, glyphs bound.
551
+ - **A page-local notation key that equals a shared one warns**
552
+ (`local-shadows-shared`). The local entry wins on its page, so
553
+ `[[motion.range]]` meant "Length of the field" there and "Range" in the
554
+ glossary and on every other page, and nothing said so. None of the
555
+ courses on the maintainer's machine has such a key.
556
+ - **`contentCheck` takes `lessons`, several ids each checked.** The
557
+ playground's `content check a b` checked `a` alone and exited 0 with `b`
558
+ silently ignored, so an agent could take both as passed; its wrapper now
559
+ passes every id, and `context`, `status`, `rules`, `warnings` and `new`
560
+ refuse an argument they do not take. A course's own `scripts/content.ts`
561
+ can do the same by passing `lessons: args.positionals`.
562
+ - **A symbol in a card from another lesson explains itself, even one this
563
+ page never uses.** The equation a `[[lesson/slug#eq-…]]` reference
564
+ previews brought its symbols along, but only those this page explained
565
+ opened a card: on "Projectile Range", ρ and φ in equation (1.1.3) of
566
+ "Position and Coordinate Systems" did nothing. Each now opens the card
567
+ that lesson gives it, read off the page the preview came from, and one of
568
+ that lesson's own symbols says "Defined in … · means this only there", as
569
+ a carried card always did (K6). Such a card is kept, remembered and
570
+ carried like any other: a shared symbol becomes the page's own card
571
+ wherever a page explains it. A kept card of a page's own symbol now names
572
+ the page it was read off in its carrying mark (K8), not the page it
573
+ was opened on. `card-fetch.ts` `markForeign` marks a card built from
574
+ another page (`data-card-from`, `data-card-from-slug`); nothing in a
575
+ course changes (`NotationLayer.astro`, `FloatRefLayer.astro`,
576
+ `card-manager.ts`).
577
+ - **Index entries and derived cards** (rule N23). A notation entry whose
578
+ `latex` is a subscript on an empty group (`"{}_0"`, `"{}_x"`, quoted in
579
+ YAML) explains that subscript on every symbol of the page that has a card of
580
+ its own: `\mathbf{v}_0` becomes one target with a **derived card**, composed
581
+ by the build (`derived-notation.mjs`) and keyed `<base>.sub.<index>`. Its
582
+ title is the index entry's new `reads` with `{base}` replaced by the base's
583
+ label ("Velocity vector evaluated at t = 0"; the default is "`<label>` of
584
+ `{base}`"), its summary the index entry's `meaning`, its formula the index
585
+ entry's `formula` with `{base}` bound to the base symbol, so the base's card
586
+ is one level deeper, and its units the base's. A derived card is never
587
+ listed; an index entry is listed with a square for the symbol (`\square_0`,
588
+ like an operator entry) and takes no `units`, `dimensionless`, `range` or
589
+ `fixedIndex` — the loader rejects them, and rule N19 skips it; `reads`
590
+ belongs to an index entry only, names the symbol with `{base}`, and is plain
591
+ prose (no `$`, `\` or backtick). A table header's screen-reader label reads
592
+ the derived title, so `v_x` and `v_y` columns differ. A whole form or a
593
+ family still wins over an index entry.
594
+ - **`range` and `fixedIndex` on a notation entry** (rule N23). `range` is
595
+ the set a running index letter runs over, as LaTeX (`"1, \\dots, n"`); the
596
+ card and the glossary show it as "Runs over …". `fixedIndex: true` says a
597
+ letter, as an index, is a label (a component, a size), or that an indexed
598
+ entry's index is fixed, so it is no family and lends nothing.
599
+ - **Derived cards wherever page math renders**: a lesson body, an
600
+ assessment, lab math and a `<Diagram>` formula; never a notation formula,
601
+ which resolves against its own card's table. A lab composes them when it
602
+ passes the table `labMathScope` returned to `renderLabMath` unchanged:
603
+ `labMathScope` ties the table to its page (`bindScopeToPage`), and
604
+ `renderLabMath` and `Diagram.astro` register the derived cards they use
605
+ (`registerPageDerivedNotation`), which `derivedNotationForPage` composes
606
+ for the page's payload. `renderNotationFormula` takes `{ derive }` and
607
+ returns `derived` and `indices`; `splitMixedContent` takes `{ derive,
608
+ derived }`.
609
+ - `resolveMathGlyphs` takes `derive` (page math sets it) and returns
610
+ `indices`, how each indexed symbol is explained, and `derived`, the derived
611
+ cards it used; an unresolved item carries `lenders`, the indexed entries
612
+ with its base, and a decoration `wholeFormExplicit`. `math-glyphs.mjs`
613
+ exports `derivedNotationKey`, `indexOfEntry`, `indexDisplayLatex`,
614
+ `entryHasIndex`, `baseSymbolLatex`, `derivedNotationLatex` and
615
+ `isRunningIndexLetter`; a glyph-table row may carry `fixedIndex`.
616
+ - **`accent-swallowed`** (rule N22, a warning): an `\explain{beta}{\hat{\beta}}`
617
+ around a decorated form counted as its whole form even though the `beta`
618
+ card never mentions the hat. It is reported now, the decoration twin of
619
+ N23's `index-swallowed` (`consistency.ts` `checkAccentExplained`).
620
+
621
+ ### Changed
622
+
623
+ - **A symbol of a page's own has a card key scoped to its page**: the card
624
+ for `notation.local`'s `theta-star` on "The Optimal Launch Angle" is
625
+ `[data-card-key="notation:theta-star@projectiles/optimal-launch-angle"]`,
626
+ not `notation:theta-star`, so a carried card (K5) never stands for
627
+ another page's symbol of the same name (K6). A shared symbol's key
628
+ is unchanged. Browser tests that select a local symbol's card by its key
629
+ need the scoped form; the page's slug comes from `NotationLayer`'s new
630
+ `page` prop, which `LessonFooter.astro` passes. A card's memory saved
631
+ under the old key is not restored.
632
+
633
+ - **Every card layer registers on every course page**, with nothing of its
634
+ own to show or not, so a card carried in can be built there: the
635
+ citation and abbreviation layers no longer wait for a marker on the page,
636
+ the reference layer for a reference or a numbered float, and a page
637
+ without notation still renders the notation card's template, in a
638
+ hidden `.notation-layer`. The page's notation layers — the page's own and
639
+ the glossary's panel — share one card source. A symbol in a card cloned
640
+ from another page opens this page's card only for a shared symbol, never
641
+ for one of this page's own.
642
+
643
+ - **A reference card's "Go to …" link sits in a row of its own**,
644
+ `p.float-ref-panel-links`, which a kept card's "Opened at" link joins
645
+ (K7). Course CSS that placed `.float-ref-panel-jump` as a grid item of
646
+ `.float-ref-panel-body` targets the row instead.
647
+
648
+ - **The side controls row's switches share one class**,
649
+ `.explico-side-switch` (its knob `.explico-side-switch-knob`), which
650
+ replaces `.explico-keep-left` and `.explico-keep-left-switch`. Course CSS
651
+ that styled "Article left" by those names targets the new ones; the
652
+ `[data-keep-article-left]` hook is unchanged, and "Carry cards" has
653
+ `[data-carry-cards]`.
654
+
655
+ - **A reference card names its float by its number in the title**:
656
+ "Equation (1.1.3)", "Figure 2.2.1 · Range vs. launch angle explorer". The
657
+ column that repeated the number beside the content is gone, an equation's
658
+ too, so the content takes the card's width, and the "Go to …" link no
659
+ longer repeats the number either: "Go to the equation in Position and
660
+ Coordinate Systems", "Go to the table". A course's browser test that reads
661
+ the link by its old name ("Go to figure 2.1.1", "Go to equation (1.1.3)")
662
+ needs the new one; `[data-panel-number]` moved into the title and keeps
663
+ its text, except an equation's, which reads "Equation (1.1.3)" instead of
664
+ "(1.1.3)" (`FloatRefLayer.astro`).
665
+ - **A figure card's picture is 80% of its width in the article**, and the
666
+ card is as wide as the picture: the caption wraps under it instead of
667
+ widening the card. A figure from another page is laid out unseen in this
668
+ page's article and measured there (`FloatRefLayer.astro`,
669
+ `figureCardWidth`).
670
+ - **Every card's text is 14 px** (`--sl-text-sm`, the article's caption
671
+ size). The notation card alone was 90% of the article's text, 14.4 px;
672
+ its units line is now 13 px (`--sl-text-xs`), a step below the text
673
+ (`NotationLayer.astro`).
674
+ - **"Keep the article left" reads "Article left"**, so it and the width
675
+ menu fit one row at the top of a 1920 px window's side column; its
676
+ tooltip still says it in full. The switch keeps
677
+ `[data-keep-article-left]`, inside a new row, `.explico-side-controls`,
678
+ which now holds the fixed position.
679
+ - **A heading's chain icon shows on hover in a narrow window too.** Below
680
+ `50em` it used to show on every heading, a rule meant for phones that a
681
+ narrow PC window matched as well. It now shows on every heading only on a
682
+ touch screen (`hover: none`); with a mouse it appears on hover at any
683
+ width (`styles/global.css`).
684
+ - **A card's footer links are outline buttons in the site's accent
685
+ colour.** "Go to the full definition", "Go to the source", "Go to the
686
+ reference list", "Go to the glossary" and a reference card's "Go to …"
687
+ lose their underline and take a hairline border, tinted on hover. The
688
+ card layer sits outside the article, so these links never got the site's
689
+ link colour and showed the browser's default blue. They are still links,
690
+ with the shared class `hover-panel-action` (`HoverPanel.astro`). A link
691
+ inside a card's text keeps its underline.
692
+
693
+ - **The header's download link is a Download menu** (`LessonExportActions.astro`).
694
+ The Download button opens a native popover listing the scopes that
695
+ contain the page, one row each ("This chapter", "This part", "Whole
696
+ course", with the chapter's, part's or course's name), and the formats in
697
+ columns (HTML, and a chapter's PDF and LaTeX source with
698
+ `lessonExports.latex`), each with its size from `downloads/index.json`; a
699
+ download the build did not write is left out. The Print button stays
700
+ beside it. A link's accessible name names its scope and format
701
+ ("Download this chapter as standalone HTML"); the button is "Download
702
+ this page". During `astro dev` every cell but a chapter's HTML says it is
703
+ built when the site is deployed.
704
+
705
+ - **Every page offers the print button and the Download menu**, not only
706
+ the chapters: the landing page, each part's page, the glossary and every
707
+ other page in the manifest's `pages` (`LayoutSocialIcons.astro`). Such a
708
+ page downloads as part of its part's and the course's files (see
709
+ Breaking). Ctrl+P on those pages now also gets the print preparation:
710
+ links moved onto `canonicalBaseUrl`, off-screen figures drawn. The print
711
+ button's accessible name says "this page" instead of "this lesson"
712
+ ("Print or save this page as PDF"), and the group's is "Page exports".
713
+ The `lessonExports` setting keeps its name.
714
+
715
+ - **A diagram's boxes are sized from what they hold.** A node is as wide
716
+ as its formula or its label, whichever is wider, plus an inset. The label
717
+ is measured in DejaVu Sans and wrapped evenly to a box of at most 220px;
718
+ the formula is measured from KaTeX's layout at the size it renders
719
+ (KaTeX's own 1.21em of the 13.6px label; `katex.css` is unlayered, so the
720
+ component's `font-size: 1em` for it never applied and is gone) and never
721
+ wraps, so a wide one widens its box and the label wraps to it. Each glyph
722
+ counts as the wider of the two ways a browser draws it: hinted at a
723
+ whole-pixel size and rounded, as Chromium on Linux does, or at its exact
724
+ advance, as macOS and iOS do. Glyphs KaTeX has no metrics for (`\iint`,
725
+ `\iiint`, `\oiint`, …) are measured too. The height follows from the
726
+ lines of text and the formula's own height. Before, the box was
727
+ estimated from the formula's character count alone, ignoring the label,
728
+ so a label could wrap past its box or spill out of it in a wide system
729
+ font. A node's `width` without a `height` now takes effect: the label
730
+ wraps inside it and the height follows (both used to be needed). A
731
+ `width` narrower than the node's formula, or a `height` shorter than what
732
+ it holds, is raised to fit; either used to cut the formula off. A
733
+ diagram wider than its V8 limit is laid out again with its labels wrapped
734
+ narrower, down to 120px boxes, before it is reported. Diagrams change
735
+ size, so screenshots of them need re-shooting. The node label carries
736
+ `text-wrap: balance`.
737
+
738
+ - **`<Diagram warnAboveWidth>` defaults to the course's column (rule V8),
739
+ 707px, instead of 800px**, and its warning is a `dia-too-wide` diagnostic
740
+ with the lesson file rather than an untagged `console.warn`.
741
+ - **The manifest schema is now v30 (additive).** `lessons[].headings[].text`
742
+ is the text a reader sees — a formula as it reads, a `[[key]]` term as its
743
+ label — and `math: true` marks a heading with inline math. Slugs are
744
+ unchanged. Each choice option in `assessmentContent` carries
745
+ `labelSegments`, its label split into text and math like
746
+ `promptSegments`.
747
+ - **The anchor of a heading with math reads as the heading does.**
748
+ `## The range $\explain{motion.range}{R}$ in a heading` was anchored
749
+ `#the-range-explainmotionranger-in-a-heading`, in the address bar, a copied
750
+ link, a `[[sec-…]]` reference and both exports; it is now
751
+ `#the-range-r-in-a-heading`. A Greek letter slugs as LaTeX names it, so a
752
+ heading whose math holds no markup keeps its anchor
753
+ (`$e^{\lambda\cos(u)}$` is still `…elambdacosu`); one with `\explain`,
754
+ `\frac` or the like gets a new one. No heading in the courses on the
755
+ maintainer's machine changes.
756
+ - **Two notation formulas may name each other, local or shared.** A cycle
757
+ through `notation.local` formulas failed with `reference-cycle`, while the
758
+ same cycle through shared formulas passed. Formulas no longer count toward
759
+ a cycle for either kind: `confidence-level` (γ = 1 − α) and
760
+ `significance-level` (α = 1 − γ) are both right, and so are three entries
761
+ that each show the one model equation they appear in (data-science-I and
762
+ time-series do both). A cycle through meanings and bodies still fails.
763
+
764
+
765
+ ### Fixed
766
+
767
+ - **A nest of `\left … \right` brackets grows outward in the LaTeX book and
768
+ PDF, as it does on the website.** The KaTeX patch behind rule N17 sets
769
+ `\delimitershortfall` to `-1pt`, but `explico.cls` kept TeX's 5pt, so the
770
+ exports drew nested pairs at one size. The class now sets it too; rebuild
771
+ the PDF to pick it up.
772
+
773
+ - **A formula's notation marker links to its entry in the LaTeX book.** A
774
+ `\explain{key}{tex}` in a part or course PDF now links to the key's entry in
775
+ the book's notation appendix, when the book has one; a chapter's own PDF,
776
+ which has no appendix, still prints the TeX. An entry's defining formula in
777
+ the appendix links the other entries it names, not its own symbol. Where
778
+ markers nest, as the resolver writes `\dot{x}`, only the outermost with an
779
+ entry links, since a link inside a link breaks the PDF's tagging. A marker
780
+ in a heading, caption or title, or inside a link, prints its TeX without a
781
+ link, since a link there breaks the build; a diagram's labels print theirs
782
+ too. The class defines `\ExplicoNotation` (`explico.cls`).
783
+
784
+ - **A card wider than the article column stays clear of the side panels.**
785
+ A figure card is up to `80ch` wide, which is wider than the article
786
+ column at 1200–1280 px with both panels open. Such a card was centred in
787
+ the whole window, and the navigation painted over its left part. Cards
788
+ are now placed between the navigation and the rail, the room a dragged
789
+ card already kept to, and a card wider than that room is narrowed to it
790
+ (`components/card-placement.ts`, `cardBand`).
791
+ - **The splash hero prints its light image.** Print is always light, but a
792
+ reader in dark mode printed the hero's dark image, whose pale strokes
793
+ vanish on white paper (`styles/global.css`).
794
+ - **A declared operator's card binds only that operator.** The glyph
795
+ resolver fingerprinted every `\operatorname{…}` and `\mathop{…}` by its
796
+ node type alone, so they all looked alike (`reference/math-glyphs.mjs`,
797
+ `signatureParts`). A page's one operator card wrapped every other declared
798
+ operator on the page: `\operatorname{Cov}` opened the
799
+ `\operatorname{Var}` card, with no diagnostic. Two operator cards on one
800
+ page failed the build with `ambiguous-canonical-notation`. A declared
801
+ operator is now fingerprinted by its letters with spacing dropped, the
802
+ name rule N8 already uses, so `\operatorname{arg\,max}` and
803
+ `\operatorname{argmax}` are still one operator. `\mathop{\mathrm{Var}}`
804
+ and `\operatorname{Var}` stay distinct. A course that put a name on
805
+ `rules.N8.allow` only because its card was refused can give the card back
806
+ and drop the name.
807
+ - **The engine's two hooks into Starlight's mobile menu follow the 0.42
808
+ markup**, where the old selectors would match nothing: the menu button
809
+ rises above the raised header as `.sl-menu-button` (`global.css`), and
810
+ open cards hide while the menu is open through
811
+ `body:has(#starlight__sidebar:popover-open)` (`HoverPanel.astro`).
812
+
813
+
814
+ - **Copy LaTeX no longer glues a command to the next letter.**
815
+ `stripNotationMarkers` (`reference/math-glyphs.mjs`) joined the two sides
816
+ of a removed `\explain{…}{…}` or `\slot{…}{…}` marker with nothing
817
+ between them, so `\times\explain{k}{m}` copied as `\timesm`,
818
+ `\left\lVert\explain{k}{X}` as `\left\lVertX` and
819
+ `\explain{a}{\lambda}\explain{b}{u}` as `\lambdau`: undefined commands to
820
+ any TeX that pasted them. A space now separates a control word from a
821
+ letter on either edge of a removed marker. Every copy button (an
822
+ equation's, a notation card's, a lab's) and the `data-latex` of inline
823
+ math take the fix.
824
+ - **The Print button no longer waits forever for a lazy image** the reader
825
+ never scrolled to: it switches such images to load now and opens the
826
+ print dialog after at most five seconds of waiting for images.
827
+ - **`content check` points a render error at its line in the file.** It
828
+ rendered a lesson's body with the frontmatter removed and its `import`
829
+ lines deleted, so an error was reported that many lines too early
830
+ (line 4 for an error on line 16, which `astro build` reports as 16).
831
+ The frontmatter is now blanked with spaces, as Astro's MDX plugin does,
832
+ and an `import` line is emptied, not removed (`compiler/cli.ts`,
833
+ `lessonSource`).
834
+ - **`validate:content` and `content check` reject what `astro build`'s
835
+ collection schemas reject.** The manifest walk read content with its own
836
+ checks and never applied the schemas in `content-config.ts`, so a notation
837
+ `meaning` with a `$`, a source `type` of `web` or `studyMinutes: [10, 5]`
838
+ passed both and then failed the build with `InvalidContentEntryDataError`.
839
+ The schemas now live in `explico/compiler/content-schemas`, which
840
+ `content-config.ts` and the manifest both use, and every record that does
841
+ not match is a `content-schema` error at the field's line (invariant
842
+ `content-schema`). In the other direction, a shared notation entry may now
843
+ leave out `aiAssisted` and `alignment`, as the schema already allowed: the
844
+ walk threw "aiAssisted must be a boolean" where the build used the
845
+ defaults. The plain-prose message for a notation `meaning` also says how
846
+ to write an amount of money.
847
+ - **An equation or a float in a list item, a quote or a `:::note` aside
848
+ is numbered the same way by the manifest and the page.** The manifest
849
+ counted every `$$…$$` in a lesson's text, and every `<Table>`, `<Figure>`
850
+ and `<Diagram>`, while the page numbered only those at the top level and
851
+ in a `<Memo>`, `<ShortNote>` or `<Fold>`. One unlabelled equation in a
852
+ list item, a `>` quote or an aside, or an inline `$$x$$` in a sentence,
853
+ made the next labelled equation fail the build with "Inconsistent equation
854
+ numbering … recompile the manifest", which never helped. Now both count
855
+ the lesson's flow, list items and quotes included, and neither counts an
856
+ aside, which is illustrative like `<Aside>` (a `<Table>` or `<Figure>` in
857
+ one fails the build, naming the aside). A display equation is a `$$`
858
+ block whose fences sit on lines of their own, as remark-math reads it;
859
+ `$$x$$` inside a sentence is inline math (`reference/equations.mjs`,
860
+ `reference/keyed-jsx-floats.mjs`). `content check` now parses `:::`
861
+ directives as the build does, through the new `remark-directive`
862
+ dependency.
863
+ - **The PDF numbers citations as the page does.** The page lists a source
864
+ once per locator it is cited with (`[@s]`, `[@s; ch. 3]` and
865
+ `[@s; eq. (4.21)]` are three entries, [1], [2] and [4]), but the LaTeX
866
+ export gave every entry of a source the same `\bibitem` key: LuaLaTeX
867
+ reported it multiply defined, and every `\cite` of it printed the last
868
+ entry's number, so the PDF read [4] where the page read [1]. A source
869
+ cited with several locators now gets one key per entry (`s/cite-4`), a
870
+ source cited once keeps its id, and `\cite` no longer repeats the
871
+ locator, which the entry already gives (`exports/latex/serialize.ts`).
872
+ - **A heading can no longer take an anchor the page already uses.**
873
+ `## References` on a page that cites a source gave the page two
874
+ `id="references"` elements (the author's heading and the generated
875
+ References section), and `## Tab totals` beside `<Table id="totals">` two
876
+ `#tab-totals`: every link went to the first, "On this page" listed
877
+ References twice, and the LaTeX export defined the label twice. Such a
878
+ heading now fails `validate:content` as `heading-anchor-collision`
879
+ (keyed-floats invariant), naming what holds the anchor: a keyed equation,
880
+ table, figure, diagram, example set or knowledge check, a `<Memo>` or
881
+ `<ShortNote>`, or the generated References section
882
+ (`reference/sections-validate.ts`, `validateHeadingAnchors`).
883
+ - **A heading with math reads as it renders everywhere it is named.**
884
+ Astro lists a heading by all the text in it, and KaTeX writes a formula
885
+ three times (MathML, its LaTeX annotation, its glyphs), so
886
+ `## The range $\explain{motion.range}{R}$` showed as "The range
887
+ R\explain{motion.range}{R}R" in "On this page", the heading link's
888
+ screen-reader label, the "Link copied" bubble and the copied link. Each
889
+ now reads "The range R": "On this page" takes the manifest's text for a
890
+ heading with math (`LayoutPageSidebar.astro`), the link's label skips the
891
+ annotation and the glyph copy (`rehype-heading-anchors.mjs`), and the
892
+ copied link and a card's "Opened at" read the heading through
893
+ `readableText`. The manifest's `lessons[].headings[].text` is the text a
894
+ reader sees, a `[[key]]` term as its label, and is what an unnumbered
895
+ `[[sec-…]]` quotes; a heading's anchor is unchanged. Rule S1 measures the
896
+ same text: `\explain{…}{…}` no longer counts against the limit.
897
+ - **The site search indexes a formula once, as it reads.** Pagefind indexed
898
+ KaTeX's MathML and its LaTeX annotation beside the glyphs, so a search for
899
+ `dfrac` or `explain` matched every page with a formula and the excerpts
900
+ read "…35.3 m\explain{motion.range}{R} = \dfrac{…". KaTeX's MathML copy is
901
+ now `data-pagefind-ignore`, in lesson math (`rehypeFailKatexErrors`, the
902
+ pass after KaTeX), in notation cards and diagrams (`renderNotationMath`)
903
+ and in lab math (`renderLabMath`).
904
+ - **A knowledge check's options render their math.** An option label went
905
+ to the page as plain text, so `"$30^{\circ}$"` showed its dollars and
906
+ LaTeX, and nothing warned. A label's math is now resolved and typeset like
907
+ the prompt's: rule N4 checks it, `AssessmentRunner` renders it, and the
908
+ LaTeX and Markdown answer keys typeset it (`assessmentContent` options
909
+ carry `labelSegments`). Rule N9 still does not apply to a label, so
910
+ "30 degrees" stays valid.
911
+ - **A reference or a citation in a knowledge check's text fails
912
+ `validate:content`.** A check renders its prompt, explanation and options
913
+ as text and math, so `[[projectiles/projectile-range#eq-range]]` or
914
+ `[@source]` there reached students exactly as written, with no
915
+ diagnostic. Each is now an `invalid-assessment` error naming the item and
916
+ the field. The error for a numeric answer's tolerance also says what is
917
+ wrong: it must be greater than 0, and an exact whole-number answer takes
918
+ one below one half.
919
+ - **A diagram's labels fit their boxes at any browser font size.** The
920
+ boxes are measured at build time for 13.6px text, but a label was
921
+ `0.85rem`, so a reader with a larger default font size (Chrome's "Large",
922
+ 20px) got labels that outgrew their boxes and were cut off, in the
923
+ playground's own diagrams too. A label is now set in px at the size its
924
+ box was measured for (`Diagram.astro`); browser zoom still enlarges it.
925
+ Scaling the whole drawing with the font size was ruled out: WebKit on iOS
926
+ scales an SVG's geometry but not the HTML in its `foreignObject`.
927
+ - **A part with nothing in it yet no longer fails the build.** A part
928
+ listed in `course.settings.json` before its directory existed took an
929
+ ordinal in `parts[]` and a sidebar group, while lessons were numbered as
930
+ if it were absent; every page built, then the LaTeX export failed with
931
+ "LaTeX numbers part/… differently from the website". Such a part is now
932
+ left out of the sidebar and of every part number until it has a chapter
933
+ or an overview page, with a `part-empty` warning; a part with only an
934
+ overview page counts in both.
935
+ - **A lesson in a directory no part lists fails `validate:content`** as
936
+ `lesson-outside-parts`, naming the `parts` entry to add. It used to pass
937
+ and then fail the build with a message about `remarkNotation` wiring.
938
+ - **A lesson's `draft: true` fails `validate:content`** as
939
+ `unsupported-key`, with the advice to keep the lesson outside
940
+ `content/docs/`: Starlight dropped the page from the production build
941
+ while the sidebar still named it, so the build crashed with "The slug …
942
+ specified in the Starlight sidebar config does not exist". A chapter's
943
+ `sidebar.order`, which does nothing, warns as `ignored-key`.
944
+ - **The marker of an open memo the reader cannot see goes to it.** A
945
+ memo's marker toggles the memo, so on an open memo off screen (one with
946
+ `defaultOpen`, under `disclosure.memos.defaultOpen: true`, or one the
947
+ reader left open) a click closed it out of sight: no scroll, no URL
948
+ change, as if nothing happened. The marker now scrolls to such a memo and
949
+ sets `#memo-…`; it folds a memo only while some of it is on screen
950
+ (`Memo.astro`).
951
+ - **A number alias into a short note opens it.** A deep link such as
952
+ `#eq-2.2.3` to an equation inside a closed `<ShortNote>` was rewritten to
953
+ the equation's named anchor after the short note had already looked at
954
+ the address, so the note stayed closed and the target had no size; the
955
+ named anchor itself worked. The short note now looks again once every
956
+ script has run (`ShortNote.astro`).
957
+ - **The build warns when a PDF leaves characters blank.** The LaTeX
958
+ export's fonts have no Greek, CJK or emoji glyphs, and LuaLaTeX only logs
959
+ a `Missing character` line and leaves a blank, so a chapter with
960
+ "Ελληνικά" or "中文" in its prose shipped a PDF reading "Ε𝜆𝜆" and "��"
961
+ with no warning. Each chapter whose PDF misses characters is now named in
962
+ a build warning with the characters (`exports/latex/compile.ts`,
963
+ `missingCharacters`).
964
+ - **Pandoc's `[@a; @b]` cites both sources.** The second source was read
965
+ as the first one's locator, so the page showed one `[1]` and its entry
966
+ read "Ling et al., … @openstax-college-physics-2e.", with no diagnostic.
967
+ A `;`-separated part that is only `@id` now starts another citation with
968
+ its own marker and entry; any other part is still the locator, so
969
+ `[@a; §1.2; eq. 3]` reads as before. The page, the manifest's citations
970
+ and the lesson's derived sources share one parser
971
+ (`citation-format.mjs`, `bracketCitations`).
972
+ - **A diagram edge from a node to itself fails the build** with a message
973
+ saying how to draw the repetition instead. The layout drew it as a stray
974
+ line out of the node.
975
+ - **A diagram formula that cannot be bound says why.** The error quoted
976
+ the same two guesses for every failure; it now gives the resolver's own
977
+ message (an unknown glyph, a shared base, a parse error). A diagram
978
+ written straight into MDX gets no notation scope, so none of its symbols
979
+ can resolve; the error now says so and names `labMathScope`.
980
+ - **An equation in a reference card sits flush in it.** The card's
981
+ `margin-block: 0` for a displayed equation was inside `@layer
982
+ starlight.components`, and `katex.css` is unlayered, so KaTeX's `1em`
983
+ margins won and left air above and below every `[[eq-…]]` preview. An
984
+ engine test now fails on any layered rule aimed at a KaTeX class.
985
+ - **The LaTeX and Markdown exports punctuate a short caption as the page
986
+ does.** Each added its own period unless the short caption ended in one,
987
+ so `short="Is it zero?"` printed "Is it zero?." in the PDF's caption,
988
+ the Markdown caption line and both alt texts. They now use the page's
989
+ `captionTitle`, which keeps a closing `!`, `?`, `…`, `:` or `;`.
990
+ - **LaTeX's numbering and reference habits fail with the explico form.**
991
+ The gate parses every span as inline math, so `\begin{align}`,
992
+ `\begin{equation}` or `\tag{A}` in a `$$…$$` block was reported as "can
993
+ be used only in display mode". The error now says the block already is
994
+ the display, numbered from its `\label{eq:key}`, and names the inner
995
+ environment to use (`aligned`, `gathered`, `alignedat`) or says to drop
996
+ the wrapper or the `\tag`. An unprefixed `\label{…}` names
997
+ `\label{eq:key}`, and `\eqref{eq:first}` or `\ref{…}` in math names
998
+ `[[eq-first]]`. In prose, where MDX failed on its braces with "Could not
999
+ parse expression with acorn", `validate:content` now reports it first
1000
+ (`eq-latex-ref`).
1001
+ - **The N8, N10 and N14 messages name the setting to use.** Each told an
1002
+ author to allowlist a term in `operators.allow`, `romanLabels.allow` or
1003
+ `abbreviations.allow` "in course.config.ts", keys that raise
1004
+ `config-deprecated-key`. They now name `rules.N8.allow`,
1005
+ `rules.N10.allow` and `rules.N14.allow` in the course config
1006
+ (`course.config.ts`, or the `course.settings.json` it reads), as the
1007
+ README and the doc comments do.
1008
+ - **A course rule reads what a reader sees: a link's text, not its
1009
+ address, and a caption.** A `forbid` pattern matched inside
1010
+ `[a page](https://example.org/basically/)` and skipped
1011
+ `<Table caption="This caption says basically.">`. Link destinations and
1012
+ `[ref]: https://…` definitions are now blanked from the prose surface
1013
+ (so N9 and N14 no longer read an address either), and the text of a
1014
+ `caption`, `short`, `title` or `label` prop is matched as a paragraph of
1015
+ its own under the `paragraph` and `prose` scopes.
1016
+ - **`content check <lesson>` fails on a fault in a shared notation entry
1017
+ the lesson shows**, as `validate:content` does. A formula of a shared
1018
+ entry naming an unknown key is reported against the entry's own file, so
1019
+ the per-lesson check left it out and printed `ok` for every page that
1020
+ showed the card.
1021
+ - **Money that remark-math would read as math fails `validate:content`**
1022
+ (`currency-dollar`). remark-math pairs any two `$` on a line, so
1023
+ `pay $5 ~ $6` showed "pay 5 6" with every gate passing (the gate's own
1024
+ span scan follows Pandoc and skipped the pair), and "between $5 and $6"
1025
+ failed the build on the letters of "and". A pair whose content starts with
1026
+ a digit and ends in a space or runs into a digit is now an error that says
1027
+ to write `\$5 to \$6`. None of the courses on the maintainer's machine
1028
+ has one.
1029
+ - **A table one row over `truncateAfter` shows whole.** Truncating it hid
1030
+ that one row behind a "… 1 more row …" line just as tall, and the reader
1031
+ had to click to see it. A table now starts truncated only when that hides
1032
+ two rows or more (`table-truncation.ts`).
1033
+ - **A pinned card whose section the reader collapses waits out of sight.**
1034
+ It stayed where it stood, over the text that moved up into its section's
1035
+ place, and scrolled with the page as if still attached. It now hides,
1036
+ keeping its size, while its trigger is folded away, and comes back beside
1037
+ the trigger when the section or fold opens. A closed `<details>` keeps
1038
+ its content's last boxes in current browsers (`content-visibility:
1039
+ hidden`), so the card manager asks `checkVisibility()`, not
1040
+ `getClientRects()`, whether the trigger is folded.
1041
+ - **N9 quotes a hand-typed sub/superscript whole.** The pattern took three
1042
+ characters, so `2^10` was reported as "2^1" and `e^{i t}` as "e^{". It
1043
+ now takes the whole runs on both sides, or a braced group.
1044
+ - **A folded section counts the numbered floats and equations inside a
1045
+ short note.** Everything in a `<ShortNote>` counted as part of the note,
1046
+ and the note itself was never counted, so a section whose table or display
1047
+ equation sat in one said only "Hidden here: 1 paragraph". A numbered float
1048
+ or equation in a short note now counts, and a float with a short caption
1049
+ is named, as in a `<Fold>`; the note's prose still belongs to its
1050
+ paragraph.
1051
+ - **Three notation errors say what to write.** A glyph two page entries
1052
+ share ("structurally identical LaTeX") now cites rule N4 and says to write
1053
+ one of them differently or keep one off the page, and that `\explain`
1054
+ does not lift the clash (it never did). An invalid key says what it most
1055
+ likely meant: `[[motion.range|reach]]` that a reference takes no link
1056
+ text (write `[[motion.range]]`), `[[dia-My_Diagram]]` that a diagram's
1057
+ id is lower-case kebab-case, anything else the key grammar.
1058
+ - **An unclosed `[[` fails `validate:content`** (`malformed-reference`, at
1059
+ its line). Only the render caught it, and `content check` printed Astro's
1060
+ whole error object (its syntax tree, `cause`, `place`) above its own
1061
+ line; `renderLesson` now drops that log and carries the error's line and
1062
+ column into its diagnostic.
1063
+ - **"Opened at" quotes a section the course shows unnumbered**, as a
1064
+ reference to it prints: `Opened at “What this chapter uses”`, not the
1065
+ bare title (`sectionPlace`).
1066
+ - **A caption prop written as an expression warns**
1067
+ (`fig-unread-prop`, `tab-…`, `dia-…`, `ex-…`). `<Figure short={"…"}>`
1068
+ rendered its short caption, but the manifest reads only a quoted prop, so
1069
+ the float's `[[…]]` card, a copied link, a folded section's line and the
1070
+ "Figures and tables" page named it without one, silently.
1071
+ - **A float id the manifest rejects is named as such at render.**
1072
+ `<Diagram id="My_Diagram">` failed `validate:content` with
1073
+ `dia-invalid-key`, and then the render (and `content check`) said "Stale
1074
+ Explico manifest … Recompile the manifest before rendering", which no
1075
+ recompile fixes. It now says the id is not a valid key and to fix it.
1076
+ - **A section reference to a part's page says why it cannot resolve.**
1077
+ `[[motion#sec-…]]` failed with "names lesson motion, which does not
1078
+ exist", though the page exists. It now says the page is not a chapter, a
1079
+ section reference reaches a chapter's top-level headings only (rule S10),
1080
+ and how to link the heading with a Markdown link instead.
1081
+ - **`remark-citation.mjs` is a text file again.** It used a literal NUL
1082
+ character as a key separator, so git showed every change to it as
1083
+ `Bin … bytes` and grep skipped it. It now writes `\u0000`, and a test fails
1084
+ on a NUL byte in any source, doc or content file.
1085
+ - **A PDF table's caption no longer sits on the table.** With
1086
+ `\DocumentMetadata`, which every export's `main.tex` has, the kernel's
1087
+ tagging code sets each caption itself, so none of the class's
1088
+ `\captionsetup` applied: a table's caption, set above it, kept the
1089
+ kernel's space above and none below, and its top rule ran about 1pt under
1090
+ the caption's descenders. A table's caption now gets the same space on both
1091
+ sides (8pt), and every caption the class's form: small type, and a quad
1092
+ after "Table 2.1.1" instead of a colon, as on the web page.
1093
+ - **Prose in another script reaches the PDF.** Under LuaLaTeX, a Greek
1094
+ word, Cyrillic, Chinese, Japanese, Korean and emoji come from fallback
1095
+ fonts, the first the build machine has (CMU Serif from TeX Live's
1096
+ `cm-unicode`, now in `texlive-packages.txt` as optional, then Noto Serif or
1097
+ DejaVu Serif; Noto CJK or Fandol; Noto Color Emoji), loaded only for a
1098
+ document that needs them (the class option `scripts`). A Greek word was set
1099
+ in math italic, letter by letter, and an accented letter, Chinese and emoji
1100
+ were blank. A Greek letter on its own stays a math symbol, as in a caption's
1101
+ "polar coordinates ρ and φ".
1102
+ - **pdfLaTeX no longer fails the whole build on a character it cannot set.**
1103
+ `EXPLICO_LATEX_ENGINE=pdflatex` stopped at "Unicode character έ not set up
1104
+ for use with LaTeX" after every page had rendered. The character now stays
1105
+ blank and the build warns, as under LuaLaTeX without a fallback font.
1106
+ - **A LaTeX compile no longer reruns until it gives up.** latexmk counted
1107
+ luaotfload's shared font caches as inputs; the build's parallel compiles
1108
+ rewrite them, so a compile could see its inputs change on every run and fail
1109
+ with "'lualatex' needed too many passes". The build and the shipped
1110
+ `latexmkrc` ignore them, and a failure latexmk reports itself now shows
1111
+ latexmk's words instead of the log's last lines.
1112
+ - **Braces in prose fail `validate:content`, at their line** (`mdx-expression`,
1113
+ invariant `prop-text`). MDX reads `{a}` as JavaScript; both checks passed
1114
+ it, and the build failed with "a is not defined" in a compiled chunk, with
1115
+ no file or line. The message says to write `\{a\}`, or `$\{a\}$` for a set.
1116
+ None of the courses on the maintainer's machine has one.
1117
+ - **An equation wider than the column shows that it goes on.** On a phone a
1118
+ wide display equation scrolls sideways inside its box with no scrollbar,
1119
+ and it simply stopped mid-formula beside its number. The edge it is cut
1120
+ at now fades (`data-overflow` on `.katex-display`, set while it overflows
1121
+ and kept current as it scrolls); focus and print show it whole.
1122
+ - **A part page's stats read apart in search and to a screen reader.** They
1123
+ were spans in a row, separated only by space on screen, so the search
1124
+ index read "Part 1 of 23 chapters8 numbered equations". They are a list
1125
+ now (`PartOverview.astro`), and search reads "Part 1 of 2. 3 chapters."
1126
+ - **A power or a mark no longer breaks a whole form** (rule N23).
1127
+ KaTeX parses `v_0^{2}` as one node with the power, so it never matched the
1128
+ entry `v_0`: only the letter was highlighted, and where the letter had an
1129
+ entry of its own the card was wrong (`\sigma_k^2` opened the population
1130
+ standard deviation). The whole form is matched and wrapped now, the power
1131
+ outside it; a mark (`v_0'`) stays rule N22's. A whole form inside another
1132
+ whole form's subscript keeps its own card too: `T_{\mathrm{opt}}` in
1133
+ `B_{T_{\mathrm{opt}}}^{\mathrm{dirty}}` is one target, not a highlighted `T`.
1134
+ Authors' `\explain{key}{v_0}^{2}` workarounds still work.
1135
+
1136
+
17
1137
  ## [0.12.0] - 2026-09-28
18
1138
 
19
1139
  ### Breaking
@@ -2312,7 +3432,8 @@ under **Breaking** below; a PATCH never breaks.
2312
3432
  system with its completeness gate, the curriculum-graph validator, the
2313
3433
  Astro/Starlight UI, the LMS / session / analytics seams, and the `content` CLI.
2314
3434
 
2315
- [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.12.0...HEAD
3435
+ [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.13.0...HEAD
3436
+ [0.13.0]: https://github.com/danieltuzes/explico/compare/v0.12.0...v0.13.0
2316
3437
  [0.12.0]: https://github.com/danieltuzes/explico/compare/v0.11.1...v0.12.0
2317
3438
  [0.11.1]: https://github.com/danieltuzes/explico/compare/v0.11.0...v0.11.1
2318
3439
  [0.11.0]: https://github.com/danieltuzes/explico/compare/v0.10.0...v0.11.0