explico 0.12.0 → 0.13.1

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