explico 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/CHANGELOG.md +894 -1
  2. package/README.md +200 -30
  3. package/dist/compiler/astro.d.ts +15 -2
  4. package/dist/compiler/astro.js +40 -11
  5. package/dist/compiler/cli-main.d.ts +43 -0
  6. package/dist/compiler/cli-main.js +392 -0
  7. package/dist/compiler/cli.d.ts +424 -9
  8. package/dist/compiler/cli.js +1289 -8
  9. package/dist/compiler/collections.d.ts +88 -1
  10. package/dist/compiler/collections.js +170 -25
  11. package/dist/compiler/course-config.d.ts +1 -1
  12. package/dist/compiler/course-config.js +105 -1
  13. package/dist/compiler/decks.d.ts +161 -0
  14. package/dist/compiler/decks.js +235 -0
  15. package/dist/compiler/exports/combined-html.d.ts +5 -1
  16. package/dist/compiler/exports/deck-fit.d.ts +55 -0
  17. package/dist/compiler/exports/deck-fit.js +90 -0
  18. package/dist/compiler/exports/export-chapters.d.ts +37 -6
  19. package/dist/compiler/exports/export-chapters.js +64 -5
  20. package/dist/compiler/exports/export-pages.d.ts +30 -3
  21. package/dist/compiler/exports/export-pages.js +24 -1
  22. package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  23. package/dist/compiler/exports/latex/cache.d.ts +78 -0
  24. package/dist/compiler/exports/latex/cache.js +129 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +10 -1
  26. package/dist/compiler/exports/latex/capture.js +3 -1
  27. package/dist/compiler/exports/latex/compile.d.ts +13 -2
  28. package/dist/compiler/exports/latex/compile.js +12 -1
  29. package/dist/compiler/exports/latex/deck.d.ts +92 -0
  30. package/dist/compiler/exports/latex/deck.js +349 -0
  31. package/dist/compiler/exports/latex/document.d.ts +51 -6
  32. package/dist/compiler/exports/latex/document.js +71 -12
  33. package/dist/compiler/exports/latex/explico-common.sty +198 -0
  34. package/dist/compiler/exports/latex/explico.cls +13 -183
  35. package/dist/compiler/exports/latex/serialize.d.ts +60 -4
  36. package/dist/compiler/exports/latex/serialize.js +4 -2
  37. package/dist/compiler/exports/latex-export.d.ts +24 -2
  38. package/dist/compiler/exports/latex-export.js +107 -26
  39. package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
  40. package/dist/compiler/exports/markdown/serialize.js +85 -8
  41. package/dist/compiler/exports/markdown-export.d.ts +6 -1
  42. package/dist/compiler/exports/markdown-export.js +50 -11
  43. package/dist/compiler/exports/standalone-html.d.ts +10 -5
  44. package/dist/compiler/exports/standalone-html.js +3 -3
  45. package/dist/compiler/exports/static-tree.d.ts +38 -1
  46. package/dist/compiler/exports/static-tree.js +120 -4
  47. package/dist/compiler/knowledge-version.d.ts +26 -2
  48. package/dist/compiler/knowledge-version.js +27 -17
  49. package/dist/compiler/lesson-derivation.d.ts +143 -6
  50. package/dist/compiler/lesson-derivation.js +145 -13
  51. package/dist/compiler/manifest.d.ts +133 -4
  52. package/dist/compiler/manifest.js +359 -58
  53. package/dist/compiler/page-versions.d.ts +160 -0
  54. package/dist/compiler/page-versions.js +198 -0
  55. package/dist/compiler/render-diagnostics.d.ts +5 -1
  56. package/dist/compiler/resolution-report.d.ts +24 -6
  57. package/dist/compiler/resolution-report.js +22 -4
  58. package/dist/compiler/validate-content.d.ts +7 -0
  59. package/dist/compiler/validate-content.js +160 -0
  60. package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
  61. package/dist/curriculum/competency-graph.d.ts +54 -0
  62. package/dist/curriculum/competency-graph.js +41 -0
  63. package/dist/reference/base-library.d.mts +8 -0
  64. package/dist/reference/base-library.mjs +19 -0
  65. package/dist/reference/consistency.d.ts +8 -4
  66. package/dist/reference/consistency.js +76 -26
  67. package/dist/reference/deck-remark.d.mts +123 -0
  68. package/dist/reference/deck-remark.mjs +600 -0
  69. package/dist/reference/deck-source.d.ts +107 -0
  70. package/dist/reference/deck-source.js +140 -0
  71. package/dist/reference/decks-validate.d.ts +133 -0
  72. package/dist/reference/decks-validate.js +359 -0
  73. package/dist/reference/derived-entries.d.ts +5 -1
  74. package/dist/reference/diagram-fit.d.ts +1 -1
  75. package/dist/reference/figure-symbols.d.ts +23 -6
  76. package/dist/reference/figure-symbols.js +51 -15
  77. package/dist/reference/float-name.d.ts +43 -0
  78. package/dist/reference/float-name.js +58 -0
  79. package/dist/reference/folds-validate.d.ts +1 -1
  80. package/dist/reference/gate-abbreviations.d.ts +1 -1
  81. package/dist/reference/gate-indices.js +5 -4
  82. package/dist/reference/gate-math.d.ts +29 -2
  83. package/dist/reference/gate-math.js +99 -18
  84. package/dist/reference/gate-part-pages.d.ts +1 -1
  85. package/dist/reference/lab-katex-validate.d.ts +10 -6
  86. package/dist/reference/lab-katex-validate.js +17 -9
  87. package/dist/reference/lab-math-scope.js +29 -4
  88. package/dist/reference/math-glyphs.d.mts +25 -0
  89. package/dist/reference/math-glyphs.mjs +189 -25
  90. package/dist/reference/page-notation.d.ts +22 -3
  91. package/dist/reference/page-notation.js +32 -10
  92. package/dist/reference/references.d.ts +14 -1
  93. package/dist/reference/references.js +13 -0
  94. package/dist/reference/registry-scope.d.mts +33 -0
  95. package/dist/reference/registry-scope.mjs +34 -0
  96. package/dist/reference/registry.js +29 -10
  97. package/dist/reference/rehype-deck.d.mts +6 -0
  98. package/dist/reference/rehype-deck.mjs +286 -0
  99. package/dist/reference/rehype-heading-anchors.d.mts +20 -0
  100. package/dist/reference/rehype-heading-anchors.mjs +48 -6
  101. package/dist/reference/remark-notation.mjs +249 -40
  102. package/dist/reference/source-lines.d.mts +12 -0
  103. package/dist/reference/source-lines.mjs +65 -0
  104. package/dist/reference/types.d.ts +28 -0
  105. package/dist/rules/catalog.d.ts +21 -4
  106. package/dist/rules/catalog.js +221 -2
  107. package/dist/rules/course-rules.d.ts +5 -1
  108. package/dist/rules/course-rules.js +17 -3
  109. package/dist/rules/course-rules.schema.json +12 -0
  110. package/dist/rules/in-effect.d.ts +13 -3
  111. package/dist/rules/in-effect.js +30 -7
  112. package/dist/rules/rules.schema.json +259 -1
  113. package/dist/rules/settings.d.ts +1 -1
  114. package/dist/session/PreferenceStore.d.ts +6 -0
  115. package/dist/session/PreferenceStore.js +18 -1
  116. package/dist/session/deck-memory.d.ts +116 -0
  117. package/dist/session/deck-memory.js +143 -0
  118. package/dist/session/reading-memory.d.ts +96 -0
  119. package/dist/session/reading-memory.js +133 -0
  120. package/docs/VERSIONING.md +21 -18
  121. package/docs/authoring/README.md +29 -9
  122. package/docs/authoring/agents.md +63 -16
  123. package/docs/authoring/components.md +15 -0
  124. package/docs/authoring/deck.md +588 -0
  125. package/docs/authoring/migrating.md +1087 -14
  126. package/docs/authoring/notation.md +49 -6
  127. package/docs/authoring/structure.md +80 -1
  128. package/docs/authoring/versioning.md +54 -2
  129. package/docs/authoring/visualizations.md +7 -2
  130. package/docs/course-agent/AGENTS.md +37 -0
  131. package/docs/course-agent/agents/rules-reviewer.md +33 -0
  132. package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
  133. package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
  134. package/docs/course-agent/skills/release-content/SKILL.md +24 -0
  135. package/docs/diagram-scaling.md +148 -0
  136. package/docs/privacy.md +59 -0
  137. package/package.json +8 -2
  138. package/src/compiler/astro.ts +60 -21
  139. package/src/compiler/cli-main.ts +469 -0
  140. package/src/compiler/cli.ts +1905 -16
  141. package/src/compiler/collections.ts +284 -25
  142. package/src/compiler/course-config.ts +267 -0
  143. package/src/compiler/decks.ts +455 -0
  144. package/src/compiler/exports/deck-fit.ts +127 -0
  145. package/src/compiler/exports/export-chapters.ts +105 -6
  146. package/src/compiler/exports/export-pages.ts +46 -2
  147. package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  148. package/src/compiler/exports/latex/cache.ts +183 -0
  149. package/src/compiler/exports/latex/capture.ts +5 -2
  150. package/src/compiler/exports/latex/compile.ts +18 -0
  151. package/src/compiler/exports/latex/deck.ts +419 -0
  152. package/src/compiler/exports/latex/document.ts +117 -21
  153. package/src/compiler/exports/latex/explico-common.sty +198 -0
  154. package/src/compiler/exports/latex/explico.cls +13 -183
  155. package/src/compiler/exports/latex/serialize.ts +25 -21
  156. package/src/compiler/exports/latex/texlive-packages.txt +6 -2
  157. package/src/compiler/exports/latex-export.ts +187 -45
  158. package/src/compiler/exports/markdown/serialize.ts +118 -9
  159. package/src/compiler/exports/markdown-export.ts +56 -11
  160. package/src/compiler/exports/standalone-html.ts +8 -7
  161. package/src/compiler/exports/static-tree.ts +175 -3
  162. package/src/compiler/knowledge-version.ts +56 -21
  163. package/src/compiler/lesson-derivation.ts +282 -16
  164. package/src/compiler/manifest.ts +558 -63
  165. package/src/compiler/page-versions.ts +359 -0
  166. package/src/compiler/resolution-report.ts +38 -7
  167. package/src/compiler/validate-content.ts +212 -0
  168. package/src/components/CourseContents.astro +124 -6
  169. package/src/components/CurriculumMap.astro +49 -1
  170. package/src/components/FloatIndex.astro +1 -1
  171. package/src/components/PartOverview.astro +51 -5
  172. package/src/components/assessments/AssessmentSet.astro +41 -15
  173. package/src/components/block-heading.ts +15 -6
  174. package/src/components/card-carry.ts +7 -4
  175. package/src/components/card-manager.ts +14 -2
  176. package/src/components/card-memory.ts +13 -9
  177. package/src/components/deck/DeckEnhancer.astro +13 -0
  178. package/src/components/deck/deck-context.ts +104 -0
  179. package/src/components/deck/deck-controller.ts +1287 -0
  180. package/src/components/deck/deck-map.ts +91 -0
  181. package/src/components/deck-lane.ts +163 -0
  182. package/src/components/diagram/Diagram.astro +131 -86
  183. package/src/components/disclosure/persisted-disclosure.ts +9 -1
  184. package/src/components/figure/Figure.astro +7 -2
  185. package/src/components/figure/FloatCaption.astro +5 -3
  186. package/src/components/fold/Fold.astro +2 -2
  187. package/src/components/notation/NotationFormula.astro +81 -11
  188. package/src/components/notation/NotationGlossary.astro +132 -1
  189. package/src/components/notation/NotationLayer.astro +29 -0
  190. package/src/components/starlight/AboutPanel.astro +186 -3
  191. package/src/components/starlight/CollapsibleSections.astro +24 -3
  192. package/src/components/starlight/DiagramEnhancer.astro +5 -1
  193. package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
  194. package/src/components/starlight/LayoutHeader.astro +17 -0
  195. package/src/components/starlight/LayoutPageTitle.astro +110 -0
  196. package/src/components/starlight/LayoutSidebar.astro +807 -21
  197. package/src/components/starlight/LayoutSocialIcons.astro +62 -6
  198. package/src/components/starlight/LessonExportActions.astro +27 -9
  199. package/src/components/starlight/LessonFooter.astro +59 -0
  200. package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
  201. package/src/components/starlight/TextSizeControl.astro +24 -13
  202. package/src/components/starlight/page-marks.ts +139 -0
  203. package/src/components/starlight/section-summary.ts +1 -1
  204. package/src/components/starlight/sidebar-editions.ts +245 -0
  205. package/src/components/table/Table.astro +15 -7
  206. package/src/content-config.ts +87 -1
  207. package/src/curriculum/competency-graph.ts +95 -0
  208. package/src/reference/base-library.mjs +19 -0
  209. package/src/reference/consistency.ts +126 -39
  210. package/src/reference/deck-remark.mjs +600 -0
  211. package/src/reference/deck-source.ts +273 -0
  212. package/src/reference/decks-validate.ts +604 -0
  213. package/src/reference/diagram-layout.ts +3 -1
  214. package/src/reference/figure-symbols.ts +108 -31
  215. package/src/reference/folds-validate.ts +1 -1
  216. package/src/reference/gate-indices.ts +5 -3
  217. package/src/reference/gate-math.ts +169 -25
  218. package/src/reference/lab-katex-validate.ts +30 -15
  219. package/src/reference/lab-math-scope.ts +34 -3
  220. package/src/reference/math-glyphs.mjs +189 -25
  221. package/src/reference/page-notation.ts +69 -15
  222. package/src/reference/references.ts +28 -0
  223. package/src/reference/registry-scope.mjs +34 -0
  224. package/src/reference/registry.ts +41 -11
  225. package/src/reference/rehype-deck.mjs +286 -0
  226. package/src/reference/rehype-heading-anchors.mjs +48 -6
  227. package/src/reference/remark-notation.mjs +249 -40
  228. package/src/reference/source-lines.mjs +65 -0
  229. package/src/reference/types.ts +28 -0
  230. package/src/rules/catalog.ts +257 -4
  231. package/src/rules/course-rules.ts +21 -1
  232. package/src/rules/in-effect.ts +54 -8
  233. package/src/session/PreferenceStore.ts +23 -0
  234. package/src/session/deck-memory.ts +263 -0
  235. package/src/session/reading-memory.ts +243 -0
  236. package/src/styles/deck.css +1150 -0
  237. package/src/styles/global.css +958 -27
  238. package/src/styles/zones.css +277 -0
  239. /package/src/{components → reference}/float-name.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -14,6 +14,897 @@ under **Breaking** below; a PATCH never breaks.
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.14.0] - 2026-10-11
18
+
19
+ ### Breaking
20
+
21
+ - **A symbol no entry binds in a notation entry's body fails `validate:content`
22
+ (rule N4), and the build fails on an entry whose body did not render.** The
23
+ Markdown body of a `content/notation/*.md` entry was the one rendered-math
24
+ context no gate read. `remarkNotation` rejected an unbound `$w_3$` there, but
25
+ Astro's content layer only logs a render error (`[ERROR] [glob-loader]`) and
26
+ keeps the entry without its body, so `astro build` ended green and the
27
+ glossary showed the entry with no text. `gateContentMath` now resolves every
28
+ `$…$` / `$$…$$` and currency `$` in an entry's body (`unresolved-glyph`,
29
+ `invalid-latex`, `currency-dollar`, …, context `body`) at the file's own
30
+ line, against the table the render uses: every shared entry, built by the new
31
+ `registry-scope.mjs` for both. `NotationGlossary.astro` throws, naming the
32
+ files, when an entry with a body has none rendered, whatever the cause. No
33
+ diagnostic code is new. Measured on the three downstream courses and the
34
+ playground (109 body spans), nothing fails.
35
+ - **Each chapter and part page carries the knowledge version its content
36
+ last changed in (rule C5), kept in `content/versions.json`, which
37
+ `validate:content` writes and the course commits.** `MANIFEST_SCHEMA_VERSION`
38
+ is now v32. The course's version moves at every release, whatever the
39
+ release touched, so stamped on a chapter it said only "built after 0.4.0",
40
+ and in every PDF six times over, so a content release changed every LaTeX
41
+ cache key. Now a chapter's PDF, its source zip's README, its Markdown and
42
+ the `[?]` panel (a new "This page" row, `data-page-since`) say "unchanged
43
+ since version 0.2.0", the release its content last changed in, or "with
44
+ changes after version 0.4.0, not yet released" while `## [Unreleased]`
45
+ holds a change; a part's and the course's PDF and Markdown carry the newest
46
+ among their pages. A document unchanged since an earlier release keeps
47
+ that release's date (`SOURCE_DATE_EPOCH`, its figures' dates) and its
48
+ trailer `/ID`, so with `EXPLICO_LATEX_CACHE` a release recompiles only the
49
+ documents it changed. What counts as the page: its file, its `.checks.yml`
50
+ and assessments, the shared notation entries its text uses, the sources it
51
+ cites and its numbers. `content/versions.json` is a record like a lockfile:
52
+ `compileManifest` reads the committed one and works the next out
53
+ (`page-versions.ts`: a release takes every pending change; an unchanged
54
+ page keeps its version; a change waits while `## [Unreleased]` has
55
+ bullets), `validate:content` writes it on a developer's machine
56
+ (`syncPageVersions`) and in CI (`CI` set) fails with `page-versions-stale`
57
+ until the committed file is the one it would write — **so a course's first
58
+ build after upgrading needs one `pnpm validate:content` and a commit of
59
+ `content/versions.json`**. A file that is not such a record warns
60
+ `page-versions-invalid` and is rebuilt. The manifest gains
61
+ `lessons[].since`, `parts[].overview.since`, `about.knowledge.releases`
62
+ (every released entry with its date) and `about.knowledge.unreleased`, and
63
+ `pageVersions`; `content status` reports `pageVersions` (`tracked`,
64
+ `unreleased`), `content context` a lesson's `since`.
65
+ `explico.cls`: `\ExplicoSource`'s second argument and `\ExplicoTitlePage`'s
66
+ third are the phrase, printed as given ("From *Course*, unchanged since
67
+ version 0.2.0."; "Content unchanged since version 0.2.0"), where they were
68
+ a bare version. Rule off (`rules.C5.enabled: false`): nothing is read or
69
+ written, and every page carries the course's version as before.
70
+
71
+ - **The course asks each reader before it keeps their reading in their
72
+ browser, and keeps to the answer.** A small, non-blocking question at the
73
+ bottom of the window ("Remember your reading?", `ReadingMemoryNotice.astro`,
74
+ rendered by the header on every page) offers "Remember" and "Don't
75
+ remember", the same size and weight. Reading memory is what records what a
76
+ reader did across visits: the chapters they opened (the sidebar marks, and
77
+ the note under a changed page's title), the cards they keep open on a page
78
+ (`card-memory.ts`) and the course's last set of carried cards
79
+ (`card-carry.ts`). Until the reader says yes, and after a no, none of it
80
+ outlives the tab: no visit is recorded and no mark shown, and kept and
81
+ carried cards live in the tab's session storage, so Back, a reload and a
82
+ link still bring them back until the tab closes. **Kept and carried cards
83
+ therefore no longer come back after a browser restart, or in a new tab,
84
+ unless the reader said yes**; before, they always did. Settings a reader
85
+ chooses (theme, text size, panel layout, a folded section or part, the
86
+ "Carry cards" switch) are kept as before and need no answer. The answer is
87
+ kept per course (`explico-reading-memory:<base path>`) for 13 months, then
88
+ asked again; a no deletes the course's reading memory at once. The `[?]`
89
+ panel gains an "In this browser" section: a "Remember my reading" switch,
90
+ every copy of the panel in step, and "Forget what this course stored",
91
+ which deletes every key of the course (and the settings explico courses on
92
+ the site share, and Starlight's theme) and asks again. Nothing explico
93
+ stores is a cookie, and nothing leaves the browser. The new
94
+ `readingMemory` setting (`"ask"`, default, or `"off"`: never ask, never
95
+ keep reading memory, no marks) is in the manifest as `readingMemory`
96
+ (v32). `session/reading-memory.ts` holds the logic; `PreferenceStore`
97
+ gains an optional `keys()`.
98
+
99
+ ### Added
100
+
101
+ - **The docs point at two downloadable courses.** The README, the shipped
102
+ agent guide, the rule index and the course `AGENTS.md` baseline link
103
+ https://danieltuzes.github.io/explico/start/, where the playground (at the
104
+ engine's latest release, standalone) and credit-and-derivatives are
105
+ downloadable at their newest version, with a machine-readable
106
+ `sources.json`; a `playground/…` path in the docs is a file of that
107
+ download.
108
+ - **The agent procedures ship to courses, and `content init-agent` installs
109
+ them.** `docs/course-agent/` (in the package) holds the `draft-lesson`,
110
+ `migrate-course` and `release-content` skills, the `rules-reviewer`
111
+ sub-agent and a baseline course `AGENTS.md` that points at the engine's
112
+ guide; `pnpm content init-agent [--write]` copies the skills and the agent
113
+ into the course's `.claude/` and writes the baseline only where the course
114
+ has no `AGENTS.md`. `content status` says whether the course's `AGENTS.md`
115
+ points at `docs/authoring/agents.md` and whether the procedures are
116
+ installed (`agentGuide` in the JSON). `docs/authoring/agents.md` replaces its
117
+ paragraph of verbs with a table: what you want to know, the verb, what it
118
+ prints.
119
+ - **The `content` CLI and `validate:content` ship in the engine.**
120
+ `runContentCli(courseConfig, argv?, io?)` (`explico/compiler/cli-main`) is
121
+ the whole router — every verb, the usage text (`CONTENT_USAGE`, printed by
122
+ `pnpm content help`), the flag parsing (`parseContentArgs`), the text output
123
+ and the exit code — and `runValidateContent(courseConfig)`
124
+ (`explico/compiler/validate-content`) the whole validate run. A course's
125
+ `scripts/content.ts` and `scripts/compile-manifest.ts` become three-line
126
+ shims, so a verb added to the engine reaches every course with the package;
127
+ the playground's two scripts are those shims.
128
+ - **The `appearance` setting: how the page tells its parts apart, and how a
129
+ deck's slides sit on it.** `zones` is `tonal` (the default: both sidebars
130
+ one step of Starlight's neutral grey, the header a step further, the
131
+ reading column unchanged), `accent-header` (the header a deep band of the
132
+ course accent in both themes, drawn on dark-theme colours, and the
133
+ dark-theme logo on it; its menus and the search dialog keep the page's
134
+ colours), `paper` (header and sidebars a desk, the reading column a sheet
135
+ on it whose edge casts a soft shadow, a step lighter than the desk in the
136
+ dark theme) or `none` (Starlight's colours). `slides` is `lifted` (the
137
+ default: an edge and a drop shadow, and in the dark theme a sheet a step
138
+ lighter than the page), `edge` (the edge and shadow on the page's colour)
139
+ or `sorter` (the deck on a tinted well). `LayoutHeader.astro` writes the
140
+ choice on `[data-explico-zones]` / `[data-explico-slides]`, and the new
141
+ `src/styles/zones.css` and `deck.css` select it with `:root:has(…)`, so it
142
+ is right in the first painted frame; each style is a set of `--ex-zone-*`
143
+ / `--deck-stage-*` tokens per theme that a course can retune. Screen only:
144
+ print, Present mode and a figure captured for the exports are unchanged.
145
+ `normalizeAppearance` validates it; the manifest carries it as
146
+ `appearance`, and the manifest schema is now v37.
147
+ - **Released courses now receive copy-ready 404-page instructions** in the
148
+ README, the structure guide and the current migration section: add
149
+ `content/docs/404.mdx` as a splash page, use Astro's `BASE_URL` to get home
150
+ from any missing path, and set Astro's `site` origin so Starlight can emit its
151
+ sitemap without a warning. The playground turns the missing URL into a
152
+ trajectory that misses its plotted page and exercises an actually missing
153
+ route.
154
+ - **The contents list and a part's page show each lesson's slides**, when the
155
+ course has a deck. A slides column ends every row of `<CourseContents>` and
156
+ of `<PartOverview>`'s reading list: a lesson's own deck is a tile on its
157
+ title line, and a part's deck is one bar along the chapters it covers, from
158
+ the first covered title to the last, each cell a link to that chapter's
159
+ section of the deck, named and described as the sidebar names it ("Slides:
160
+ Velocity Under Constant Acceleration" / "Slides 17–25 of the part's
161
+ slides"). A part's deck that skips a chapter breaks into runs; one that
162
+ covers a chapter with its own deck runs in a second column. Each title
163
+ starts with the glyph of what it opens (a chapter's sheet, a slides-only
164
+ lesson's frame), a part's heading with Overview's two sheets
165
+ (`--explico-part-glyph`, `global.css`), and the contents list's counts gain
166
+ the decks ("4 slide decks") and a key to the glyphs. A slides-only lesson's
167
+ tile is out of the tab order and hidden from assistive technology: its
168
+ title opens the same page. A course with no deck renders both lists as
169
+ before; `slides={false}` on either component turns it all off. The cells
170
+ come from `deck-lane.ts` (`deckLanes`, `laneLink`).
171
+ - **The glossary lists page-bound meanings.** Under its index, "On one page
172
+ only" lists every `notation.local` entry (linking to its lesson) and every
173
+ gloss (linking to the card whose formula names it), so a reader who looks a
174
+ letter up always finds it and sees which meanings are course-wide and which
175
+ are page-bound; the cards themselves stay the shared entries.
176
+ - **`content move`, `content rename` and `content fix`: the refactors as
177
+ verbs, dry-run by default.** `move <lesson>:<key> [--to <key>]` turns a
178
+ `notation.local` entry into a shared card written from the entry's own
179
+ fields, removes the copy from every lesson that declares the key (reporting
180
+ a copy that differed) and, with a new key, rewrites every `[[key]]` and
181
+ `\explain{key}`; `rename <old> <new>` renames a shared card and everything
182
+ that names it, `seeAlso` and `lint-ignore.yml` included; `fix <lesson>`
183
+ unwraps every `\explain` the gate reports as redundant. Each prints its
184
+ plan as a unified diff and changes nothing without `--write`. `planMove`,
185
+ `planRename`, `planFix`, `renderEditPlan`, `applyEditPlan`,
186
+ `removeLocalItem`, `rewriteKeyReferences` and `sharedCardFromLocal` are
187
+ exported from `explico/compiler/cli`.
188
+ - **`definition-repeated` (warning), `meaning-repeated` (info) and
189
+ `explain-redundant` (info), under rule N4.** One local key declared in two
190
+ lessons, saying whether the copies agree and that moving one copy to
191
+ `content/notation/<key>.md` keeps every `[[key]]` working; one symbol and
192
+ name defined at two sites in any tier (a local card and a gloss, two
193
+ glosses, a local card beside the shared card that already says it); and an
194
+ `\explain{key}{…}` in page math whose bare symbol already resolves to the
195
+ same card with nothing left over. The first two come from `consistency.ts`
196
+ (`checkDefinitionRepeated`, `checkMeaningRepeated`), the third from the
197
+ completeness gate, located at the span; `explainCalls` and
198
+ `redundantExplainWraps` are exported from `explico/reference/gate-math`.
199
+ - **`content equation "<latex>" [--lesson <id>]` resolves one expression the
200
+ way the gate will.** Against the lesson's scope (its `notation.local` and
201
+ the shared cards it introduces, with its suspended decorations) or, with no
202
+ lesson, against every shared card as a notation entry's body is; prints
203
+ each glyph's card, redundant `\explain` wraps, unresolved glyphs with where
204
+ they are defined, and exits 1 when the build would refuse the span. A few
205
+ lines, while drafting. `contentEquation` and `formatEquationReport` are
206
+ exported from `explico/compiler/cli`.
207
+ - **`content migrate [--from <v>] [--to <v>]` prints the migration an
208
+ upgrade needs.** The guide's `## <from> → <to>` sections from the version
209
+ `package.json` pins at HEAD to the one installed (either end overridable),
210
+ oldest first, each section whole; a `0.4.x` range on the left matches its
211
+ patch versions, `Unreleased` is allowed on the right, and a hole in the
212
+ chain is an error that names the version no section continues from. One
213
+ or two sections instead of the forty-thousand-token file. `content status`
214
+ sizes that migration among the agent surfaces when the pin and the install
215
+ differ. `contentMigrate`, `parseMigrationSections`, `selectMigrationPath`,
216
+ `installedEngineVersion`, `pinnedEngineVersion` and `formatMigrateReport`
217
+ are exported from `explico/compiler/cli`.
218
+ - **`content bindings <lesson>` shows what a lesson's math binds before a
219
+ build.** Span by span: each glyph and its card with how it resolved
220
+ (`canonical`, `family`, `derived`, `base`, `operator`, or an explicit
221
+ wrap), every `\explain{key}{…}` whose bare symbol resolves to the same card
222
+ (a redundant wrap), and every unresolved glyph with where `content symbol`
223
+ finds it defined; the counts first. The same resolver and scope the
224
+ completeness gate uses. `contentBindings`, `spanBindings` and
225
+ `formatBindingsReport` are exported from `explico/compiler/cli`.
226
+ - **`content rule <ID>` prints one rule whole.** The catalog entry as this
227
+ course has it (kind, elements, enforcement, editions, on or off, waivable,
228
+ codes, settings in effect, summary) and the rule page's own section sliced
229
+ by heading, example included, about half a thousand tokens where opening
230
+ the page costs twenty thousand; an invariant by its slug, a retired rule
231
+ with its date and successor, an unknown ID lists the rules in effect and
232
+ exits 1. `contentRule`, `formatRuleReport`, `ruleSection` and
233
+ `authoringDocsDir` are exported from `explico/compiler/cli`.
234
+ - **A lesson can have a deck: its slide edition, `<lesson>.deck.mdx`**
235
+ (`docs/authoring/deck.md`). A deck sits beside its chapter, served at
236
+ `/<part>/<lesson>/slides/` and listed under the chapter in the sidebar, or
237
+ alone, as a slides-first lesson at the lesson's own URL. `##` opens a section
238
+ (a divider frame), `###` is a frame whose heading is its title,
239
+ `<Pause />` is a step, `<Notes>` holds the speaker notes and
240
+ `<Recall ref="eq-…" />` (`fig-`, `tab-`, `dia-`) shows the chapter's float with
241
+ the chapter's number; none needs an `import`. The engine writes the title
242
+ frame, the outcomes frame (`deck.outcomesFrame`) and the references frame, and
243
+ a legend line on each frame naming the symbols it introduces. A deck shows its
244
+ lesson's symbols: its math binds in the chapter's scope, its notation bundle
245
+ holds the chapter's, and a lab recalled onto it reads the lesson's scope
246
+ (`labMathScope`). Its own figures, tables and diagrams are unnumbered and an
247
+ equation it labels numbers (1), (2) within it; `Figure`, `Table` and
248
+ `Diagram` read that from the manifest (`components/deck/deck-context.ts`), so a
249
+ course's wrapper forwards nothing new. Area **D** joins the rule set: rule D1
250
+ (sections of frames, locked), rule D2 (a frame fits its canvas), rule D3 (a
251
+ frame makes one claim), rule D4 (notes on a frame with text), rule D5 (a frame
252
+ title fits one line), rule D6 (at most `maxSteps` steps, each adding), rule D7
253
+ (one picture per frame), rule D8 (recall, never retype), rule D9 (at most
254
+ `maxNewSymbols` new symbols) and rule D10 (generated title and outcomes
255
+ frames), with the new invariant `decks` (`deck-frontmatter`,
256
+ `deck-outside-part`, `recall-unresolved`, `pause-invalid`). A catalog entry
257
+ now says which edition of a lesson it is about (`editions`): V7, V8, S1, S6 and
258
+ S7 are about a chapter's page, and a `D` rule says the same of a frame, so a
259
+ course's own `X` rule may name its edition too. `build/rules-in-effect.md` is
260
+ the chapter's page, as before; a course with a deck also gets
261
+ `build/rules-in-effect-deck.md` (`content rules --edition deck`).
262
+ `content new deck <lesson>` drafts a deck from the chapter. The course setting
263
+ `deck` sets the canvas (`aspectRatio`: `16:9`, `16:10` or `4:3`), the legend
264
+ line, who sees the notes, the outcomes frame and the handout PDF.
265
+ `MANIFEST_SCHEMA_VERSION` is now v33 (additive): `decks`, `deck` and
266
+ `lessons[].editions`. `explico` depends on `github-slugger` 2.0.0 (the id
267
+ Astro gives a deck's page). On a chapter, Next and Previous still step
268
+ from chapter to chapter, past a chapter's slides row; on a deck's page they
269
+ step from deck to deck, a part's and a slides-first lesson's included, each
270
+ named "Slides: *title*" (`deckNeighbours` in `deck-context.ts`). The
271
+ glossary names a chapter once, not its deck as a second page, and a deck's
272
+ "On this page" names each frame by its title as it reads, math included.
273
+ - **A deck's page presents.** It reads as a study page, every frame on a
274
+ canvas of fixed size (`deck.aspectRatio`: 800×450, 720×450 or 600×450 CSS
275
+ pixels) scaled to the column, each in its final step with its notes under it.
276
+ Its Present button shows one frame at a time, full screen: → ↓ Space PageDown
277
+ step, ← ↑ PageUp Backspace step back, Home and End, a number and Enter jump,
278
+ `F` full screen, `N` the notes beside the frame, `P` a presenter window (the
279
+ frame, the next, the notes, a clock, kept in step through a
280
+ `BroadcastChannel`), `B` a black screen, Esc leaves; the address is
281
+ `#slide-N`. A step can drive a lab (`<Pause control="Launch angle" from="15"
282
+ to="75" seconds="5" />` moves its control), light a symbol up
283
+ (`highlight="key"`), and a frame titled `### … [morph]` morphs from the one
284
+ before through View Transitions. A figure on a frame scales its tools down
285
+ and narrows its picture to fit, to no less than 60 %. After the build, each deck page is
286
+ laid out in the course's Chromium and a frame that still runs past its canvas
287
+ fails the build: `frame-overflow` (rule D2; a warning without a browser,
288
+ an error in CI). The deck offers "Continue at slide N", for the tab and,
289
+ with the reader's yes to reading memory, on later visits
290
+ (`explico:decks:<base>`, `docs/privacy.md`). Print gives one frame per
291
+ page.
292
+ - **A deck downloads as slides, a handout, LaTeX, offline HTML and Marp
293
+ Markdown.** With `lessonExports.latex`, each deck compiles in beamer with
294
+ explico's theme: `downloads/<lesson>-slides.pdf`, one page per step, every
295
+ symbol on a frame linked to its entry on the notation frames that close the
296
+ deck; `downloads/<lesson>-slides-notes.pdf`, each frame in its final state
297
+ followed by its notes and its check's answers (with `deck.handoutPdf`, and
298
+ `deck.notes` not `off`); and their source, `downloads/<lesson>-slides-latex.zip`
299
+ (`main.tex`, `handout.tex`, `deck.tex`, `beamerthemeexplico.sty`). The build
300
+ fails when the PDF numbers a frame differently from the page, and warns when
301
+ a frame runs over its slide on paper. `explico.cls` now loads
302
+ `explico-common.sty` (fonts, colours, figures, diagrams, notation markers),
303
+ which the theme shares; a chapter's and a book's LaTeX folder carry it.
304
+ Beamer cannot write a tagged PDF, so a deck's PDFs are untagged. With
305
+ `standaloneHtml`, `downloads/<lesson>-slides.html` presents offline; with
306
+ `markdown`, `downloads/<lesson>-slides-markdown.zip` holds the deck in Marp's
307
+ dialect. The Download menu has a row for the deck on its page and its
308
+ chapter's (`deck` scope, a `handout` column). A part's and the course's book
309
+ keep a slides-first lesson's place with a short chapter (its outcomes and a
310
+ link to its slides), so later chapters keep their numbers. `decks[].since`
311
+ dates a deck (rule C5): its PDFs' date, title frame and cache key. The CI
312
+ package list (`texlive-packages.txt`) adds `beamer` and `translator`.
313
+ - **Every diagnostic line names its rule and page.** Text output reads
314
+ `info consistency:weak-local [N4 · notation.md#n4]: …`, and the JSON carries
315
+ `doc` next to `rule`, so finding what a code answers to needs no second
316
+ step. `formatCliDiagnostic` and `ownerTag` are exported from
317
+ `explico/compiler/cli`; the playground wrapper prints through them.
318
+ - **`content status` sizes the agent surfaces.** One line with the estimated
319
+ tokens of `AGENTS.md` (the course's, and the repository's one level up) and
320
+ of the rules-in-effect page (`agentSurfaces` in the JSON): a budget to
321
+ watch.
322
+ - **A page that changed since the reader last opened it says so under its
323
+ title**: "This page changed since you last opened it.", with a "What
324
+ changed" link to the course's `changelogHref` when it has one, and the
325
+ sidebar's accent dot beside it. It shows once, on the visit that finds the
326
+ change, only with the reader's yes to reading memory, and not with
327
+ `sidebarMarks.changed` off. `LayoutPageTitle.astro` renders it hidden on
328
+ every chapter and part page; the sidebar's script shows it.
329
+ - **[`docs/privacy.md`](docs/privacy.md)**, shipped in the package: every key
330
+ an explico site keeps in a reader's browser, when it is written, which of
331
+ them need the reader's yes, and what a course's own privacy notice should
332
+ add (its host, anything it loads from another site).
333
+ - **`content symbol <latex | key>` says where a symbol is already defined.**
334
+ The shared card, every `notation.local` entry with its lesson and every
335
+ gloss with its card, matched on the latex as written, a shared card's
336
+ `aliases` or the key, then the entries whose notation contains it as a
337
+ glyph (`v` in `v_0`), each with label, units, the first sentence of its
338
+ meaning and, for a shared card, the lessons that bind it. One call before
339
+ declaring a symbol, so an author references what exists instead of copying
340
+ it. Read-only; `--json` for the full report. `contentSymbol` and
341
+ `formatSymbolReport` are exported from `explico/compiler/cli`; a course
342
+ adds the verb to its `scripts/content.ts`.
343
+ - **`<CurriculumMap>` takes `graph`, `direction` and `warnAboveWidth`.** A
344
+ course with many competencies got a `dia-too-wide` warning (rule V8) on the
345
+ map's one competency graph and could not act on it. `graph="per-part"` draws
346
+ one diagram per part (`competency-graph-<part dir>`), each with the
347
+ competencies its lessons teach first and the earlier ones they build on as
348
+ boxes labelled with their part, and a last group for competencies no lesson
349
+ teaches; `direction` and `warnAboveWidth` reach every competency diagram.
350
+ The default, `graph="whole"`, renders exactly what it did.
351
+ - **The sidebar marks, for each reader, the chapters and part pages they
352
+ have not opened yet and those that changed since they last opened them.**
353
+ A quiet ring for "not opened yet", an accent dot for "changed since you
354
+ last opened it". A labelled row carries its mark in the panel's margin just
355
+ past the row's end, so a title keeps its width; a compact part's tile
356
+ carries it on its corner, and the tile's tooltip names it under the title;
357
+ a closed part (the phone's menu) carries its strongest mark after its
358
+ label; and with the panel folded away, its restore control carries a
359
+ change. A line under the chapter list says what the marks mean and offers
360
+ "Mark all as seen". "Changed" compares the page's rule C5 hash (the
361
+ current one, `pending ?? source`, cut to 12 hex digits) with the one the
362
+ reader saw, so an unreleased change on the live site counts and a release
363
+ that only stamps a version on a page does not. What a reader has seen is
364
+ view state kept in their browser through the `PreferenceStore`, under
365
+ `explico-page-marks:<base path>`, so courses that share an origin keep
366
+ their own; it is reading memory (above), so it is recorded, and marks
367
+ shown, only after the reader's yes; without storage nothing is marked. Each marked link names its
368
+ mark as its description (`aria-describedby`). The new `sidebarMarks`
369
+ setting (`unopened`, `changed`, both on by default) turns either off; the
370
+ manifest carries it as `sidebarMarks` (v32). `page-marks.ts` holds the
371
+ logic, `LayoutSidebar.astro` the wiring, `global.css` the marks.
372
+ - **`withNoneSeen` and `DECK_LESSON_CLASS`.** `page-marks.ts` names both
373
+ resets of a reader's record: `withNoneSeen()`, the record with no page
374
+ seen ("Mark all as unseen", below under Changed), beside `withAllSeen`.
375
+ `lesson-derivation.ts` exports `DECK_LESSON_CLASS` (`explico-deck-lesson`),
376
+ the class of a slides-first lesson's sidebar item, beside `DECK_ROW_CLASS`,
377
+ and `sidebarLessonSlug(item)`, the lesson a sidebar item lists (a chapter's
378
+ slug, or a slides-first lesson's item), which the manifest's chapter
379
+ numbering reads. `buildSidebar` takes `slidesFirst`, the lessons whose only
380
+ edition is a deck.
381
+ - **`sidebarEditions`, and two controls in the sidebar's foot that switch
382
+ it.** A lesson with a chapter and a deck is one sidebar row (below, under
383
+ Changed); the setting says which edition the row opens, `lead: "chapter"`
384
+ (default) or `"slides"`, and whether it lists the other, `other: "listed"`
385
+ (default) or `"hidden"` (`normalizeSidebarEditions`,
386
+ `SIDEBAR_EDITIONS_DEFAULTS` in `course-config.ts`; any other value fails
387
+ the build, naming the field). Each reader switches both under the chapter
388
+ list: "Lessons open as" Chapter or Slides, a pair of radios drawn as one
389
+ control, and "List the other edition too", a checkbox. Their choice is a
390
+ setting, like a compact part, kept in their browser whatever they answered
391
+ about reading memory, under `explico-sidebar-editions` (one key for the
392
+ courses on a site, listed in `docs/privacy.md`, reset by "Forget what this
393
+ course stored" with the other shared settings), and kept field by field, so
394
+ a field they never set follows the course's default. A course without a
395
+ deck shows no controls. The manifest carries the setting as
396
+ `sidebarEditions`, and `MANIFEST_SCHEMA_VERSION` is now v34.
397
+ `sidebar-editions.ts` holds the logic, `LayoutSidebar.astro` the wiring.
398
+ - **A deck's page shows which slides Present mode has shown the reader, and
399
+ presents from any of them** (`docs/plans/deck-navigation/`, phase 3, and
400
+ the "Seen slides and glyphs" decisions). Each frame Present mode shows, at
401
+ any step, in the room's window or the presenter's, is kept as it was. A
402
+ line above each frame, outside its canvas and in the page's own type
403
+ (`.deck-frame__line`), says "Slide N" and "Shown", "Not shown yet" (the
404
+ sidebar's ring) or "Changed since shown" (its dot), and ends on a button:
405
+ "Continue here", filled and always shown, on the frame to continue at;
406
+ "Present from here" on every other (`button.deck-frame__present`, named
407
+ "Present from here, slide N"), shown while the pointer is on the frame or
408
+ the keyboard's focus is in it, with an accent ring round the frame, and
409
+ always, as its icon, on a screen without hover. Under the title, the slide
410
+ map (`nav.deck-map`) shows every frame as a numbered square linking to it:
411
+ filled once shown, outlined before, dotted when changed since, the frame to
412
+ continue at ringed and the one in view underlined (`aria-current`). It
413
+ stays under the header while the deck scrolls (`data-stuck`), and a frame
414
+ a link lands on keeps its line clear of it (`--deck-map-height`). A deck
415
+ too long for the map's line keeps its first and last frames, the frames
416
+ before and after the one to continue at and round the one in view, as many
417
+ as the line has room for, with a gap for each run left out that links to
418
+ the frame in its middle (`components/deck/deck-map.ts`: `slideMap`,
419
+ `slideMapKeeping`). "Not shown yet" and "Changed since shown" are the
420
+ sidebar's two marks, which `sidebarMarks` turns off here too; with
421
+ `unopened` off, no frame says "Shown" either. Each frame names its state as
422
+ its description (`aria-describedby`). A line under Present counts them
423
+ ("14 slides not shown yet", "2 slides changed since shown") and offers
424
+ "Mark all as seen" and "Mark all as unseen" for the deck, with a
425
+ `role="status"` line that says what a button did. Every frame of
426
+ `decks[].frames` carries `id`, 12 hex digits of a SHA-256 over what it
427
+ shows (an authored frame's source, notes included, and the chapter's
428
+ number for each float it recalls; the title frame's title, course and
429
+ authors, not its version; the outcomes frame's outcomes; the references
430
+ frame's sources), stamped on the frame as `data-frame-id`, so a frame moved
431
+ by one added before it is still the frame the reader saw.
432
+ `MANIFEST_SCHEMA_VERSION` is now v35. `buildDecks` takes `course` (the
433
+ title frame's course and authors) and each lesson's `outcomes`. The record
434
+ under `explico:decks:<base>` is now `{ "v": 2, "decks": { slug: { at,
435
+ step, seen } } }`: the frame and the step Present mode stopped at, and the
436
+ frames it showed; a version 1 record (`{ slug: frame }`) reads as one with
437
+ no frame seen. "Continue at slide N" presents from the frame the reader
438
+ stopped at, unless they were shown its every step, then from the frame
439
+ after it, following that frame to its number now when frames were added
440
+ before it. Before the reader answers the reading-memory question, the
441
+ frames shown are kept, and marked, for the tab alone, as kept cards are;
442
+ "Remember" keeps them for later visits (`mergeDeckMemory`), and "Don't
443
+ remember" clears them, keeps only where each deck stopped for the tab, and
444
+ records no frame after. With `readingMemory: "off"` no frame is recorded and
445
+ nothing is marked. The question itself now names the slides it keeps. The
446
+ resume button, the lines, the map and the marks' line are the reader's own
447
+ (`data-explico-export-chrome`): Present mode, print and a downloaded deck
448
+ show none of them. `session/deck-memory.ts` holds the logic
449
+ (`decodeDeckMemory`, `encodeDeckMemory`, `withPosition`, `withShown`,
450
+ `withAllSeen`, `withNoneSeen`, `withEveryDeckSeen`, `withEveryDeckUnseen`,
451
+ `mergeDeckMemory`, `frameMark`, `resumeAt`, `DECK_MEMORY_EVENT`),
452
+ `deck-controller.ts` the wiring.
453
+ - **A part can have a deck: `<part>/index.deck.mdx`, served at
454
+ `/<part>/slides/`** (`docs/authoring/deck.md`, "A deck for a part";
455
+ `docs/plans/deck-navigation/`, phase 4). It sits beside the part's page,
456
+ sets only `title` and `description`, and each `##` section may name the
457
+ chapter it covers, `## Title [chapter: <name>]`, the marker stripped from
458
+ the divider. A chapter may run over several sections in a row, each naming
459
+ it, as a chapter's own deck does; the first one's divider is the chapter's
460
+ address in the deck (`#slide-N`). Its symbols bind in the scope of the part's chapters, a key
461
+ two of them define binding to the first in reading order; `<Recall ref>`
462
+ shows a covered chapter's float with that chapter's number, linked to it,
463
+ the section's own chapter first; `[[eq-…]]`, `[[fig-…]]` and `[[sec-…]]`
464
+ fall back to the one covered chapter that has the key; `<AssessmentSet />`
465
+ asks the checks of its section's chapter (the engine passes `lesson`, a new
466
+ prop); the outcomes frame lists every covered chapter's competencies; the
467
+ title frame falls back to the part's label. In the sidebar it is a row of
468
+ its own right after the part's Overview, `explico-deck-row
469
+ explico-part-deck`, the deck's glyph before "Slides" and a tile of its own in a
470
+ compact part; a chapter it covers that has no deck of its own carries
471
+ `data-deck-href`, and the sidebar's script makes that address the
472
+ chapter's other edition, "Slides: *title*", unmarked (`data-deck-section`):
473
+ a lesson's own deck wins. "Slides for this chapter" goes to that section
474
+ when the chapter has no deck; the part's page offers "Slides for this
475
+ part" and the deck's page "Open the part". It downloads as every deck does,
476
+ named after its part (`downloads/motion-slides.pdf`, `-notes.pdf`,
477
+ `-latex.zip`, `-markdown.zip`, `.html`), on its page, on its part's and,
478
+ as "Its slides", on each chapter it covers that has no deck of its own;
479
+ `deckFileStem` takes the owner's slug, and `deckOwnerSlug`, `deckSaveName`
480
+ (`exports/export-pages.ts`) and `deckCheckItems` (`latex/document.ts`) name
481
+ and fill it. Its page has a version of its own (rule C5), which counts the
482
+ number, captions and equation each recalled float has in its chapter. The
483
+ `decks` invariant gains `deck-chapter-unknown`, `deck-chapter-repeated` (a
484
+ chapter named again after a section of another chapter or of the deck's
485
+ own), `deck-chapter-outside-part`, `deck-marker-misplaced`,
486
+ `deck-check-no-chapter` and `deck-route-taken`. `content new deck <part>`
487
+ drafts it from the part's chapters (a section per chapter in track order, a
488
+ frame per section, a `<Recall>` per numbered float), and `content check`
489
+ takes a deck's page, `<part>/slides` or `<part>/<lesson>/slides`
490
+ (`findCheckTarget`). `MANIFEST_SCHEMA_VERSION` is now v36: `decks[]` gains
491
+ `part`, `covers` (every deck; empty on a lesson's), `recalledFrom` and
492
+ `frames[].chapter`, and `lessonId` is absent on a part's deck. In the
493
+ engine, `DeckFile` is `LessonDeckFile | PartDeckFile` (`scope`, `partDir`),
494
+ `NotationLessonInput.scopeLessonIds` lists the lessons a deck's page binds
495
+ in (`resolvePageReferenceId`, `pageScopeLessonIds` in
496
+ `reference/references.ts`; `scopeLessonsOf` in `page-notation.ts`), and
497
+ `lesson-derivation.ts` gains `isPartDeckSlug`, `partDeckDir`,
498
+ `partDeckRouteSlug`, `partDeckPageId` and `PART_DECK_CLASS`. A course with
499
+ no part's deck sees no change but the manifest's shape.
500
+ - **`EXPLICO_LATEX_CACHE=<directory>` reuses a compiled PDF whose source did
501
+ not change.** The LaTeX export recompiled every document on every build:
502
+ 43 LuaLaTeX compiles, 685 s on a two-core CI runner, for a course of 37
503
+ chapters. With the variable set, `latex/cache.ts` keys each document by a
504
+ SHA-256 over every file of its folder, the engine, the TeX installation
505
+ (`texFingerprint`: the engine's and latexmk's banners, the LaTeX kernel's
506
+ date), the epoch and `EXPLICO_LATEX_CACHE_SALT`, and keeps its PDF, `.aux`
507
+ and log. A reused document is checked like a compiled one: its numbers
508
+ against the website's, its log for characters no font set. A failed
509
+ compile stores nothing, an entry that does not read back whole is a miss,
510
+ and entries unused for a week are deleted. The summary line says
511
+ "7 compiled, 36 reused from …". Unset, the build is unchanged. With rule
512
+ C5 (above) a document's version line and date are its own, so a content
513
+ release, too, recompiles only the documents it changed.
514
+ - **`pnpm check:dist-imports`, run by `prepack`.** It parses every module in
515
+ `dist/` and fails on a relative import whose target the package does not
516
+ ship. The repository never loads `dist/`, so such a gap (below) passes the
517
+ build, the playground and the tests.
518
+
519
+ - **[`docs/diagram-scaling.md`](docs/diagram-scaling.md)**, shipped in the
520
+ package like `docs/katex-bracket-scaling.md`: how a diagram follows the
521
+ phone's text size (below, under Changed), why it is scaled through the HTML
522
+ canvas and never the SVG, the rules for changing it, and what to check on a
523
+ real iPhone, which the end-to-end suite (Chromium) cannot.
524
+
525
+ ### Changed
526
+
527
+ - **The header, the two sidebars and the reading column are told apart by
528
+ colour, and every slide on a deck's page has a visible edge and a drop
529
+ shadow** (`appearance`, above, at its defaults). Before, the light theme
530
+ drew all three columns white, and the dark theme gave the header and the
531
+ course sidebar one colour and the contents sidebar the article's. A dark
532
+ slide had no visible edge: its border was `hairline-shade`, which in the
533
+ dark theme is the page's own colour, and its shadow was black on
534
+ near-black. The part holding the open page is tinted with the sidebar's
535
+ own accent tint (`--ex-zone-rail-accent`), a step darker than
536
+ `accent-low`, so it differs from the grey by lightness as well as hue.
537
+ The course's title in the header is its accent a step toward the text
538
+ colour (`--ex-zone-title`), which keeps it at 4.5:1 on the tinted header.
539
+ `"zones": "none"` keeps the old frame.
540
+ - **A part's deck shares the part's row with its Overview.** It was a row of
541
+ its own under the Overview, listed whatever the sidebar's two controls
542
+ said, with a chapter's deck glyph and the tooltip "Slides". The sidebar's
543
+ script now makes the two one row, as a lesson's chapter and deck are, and
544
+ it follows "Lessons open as" and "List the other edition too" the same way,
545
+ Overview standing for the chapter: "Overview" with the deck as the icon at
546
+ its end, or "Slides for this part" with Overview's two sheets there, or
547
+ one of them alone (an unlisted one still shows while it is the open page).
548
+ In a part with no Overview the deck keeps a row of its own, unlisted only
549
+ while chapters lead with the other edition unlisted. Both links keep their
550
+ glyph in every state, and a compact part joins their tiles, both in the
551
+ accent. The deck's glyph is two frames, its own in front with an outlined
552
+ frame behind (`--explico-part-deck-glyph`, `deck.css`). Its tooltip names
553
+ the part, then on a quieter second line its chapters and slides ("Slides
554
+ for this part: Motion in the plane" / "Chapters 1–2 · 26 slides"), and a
555
+ chapter's section of it names its slides ("Slides 17–25 of the part's
556
+ slides"); the second line is the link's `aria-description` and a
557
+ `small.layout-edition-detail` in its name's span, shown only where the
558
+ span is a tooltip. `buildSidebar` adds `data-part` and `data-part-edition`
559
+ to the Overview's item and the deck's, `data-title` and `data-chapters`
560
+ (the covered chapters' ordinals, `1–2`, `1, 3`) to the deck's, and the new
561
+ `withPartDeckFrames` adds, once `buildDecks` has run, `data-frames` to the
562
+ deck's item and `data-deck-range` to each chapter that links into its
563
+ section (its frames, `2–16`). `sidebar-editions.ts` gains `partRow`,
564
+ `partEditionLabel`, `partDeckDetail`, `sectionDetail`,
565
+ `PART_EDITION_ATTRIBUTE`, `PART_ROW_CLASSES` and `EDITION_DETAIL_CLASS`;
566
+ `lesson-derivation.ts` gains `numberList`. Without scripts the sidebar is as
567
+ before. No schema bump: the attributes are additive.
568
+ - **A formula's "Symbols" list names every symbol the formula names.** The
569
+ cards an entry's `formula` wraps with `\explain{key}{…}` are listed with its
570
+ glosses, in formula order, each card linked to its own entry on the
571
+ glossary page (`cards` on `<NotationFormula>`; `data-notation-card` marks
572
+ one); the list used to show the glosses alone, so a formula read as
573
+ half-explained without hover and in the exports.
574
+ - **Rule N4's lints follow the tier model: a letter is declared once.**
575
+ `glyph-unique-in-corpus` now warns only when two *shared* cards claim one
576
+ glyph; a `notation.local` entry or a gloss that reuses a letter is the
577
+ declared exception and is never reported, so the acknowledgements courses
578
+ wrote for every reused index letter can go. N4's summary and its page say
579
+ which tier a symbol takes in one question — is this letter already
580
+ defined? — and `content symbol` answers it.
581
+ - **`content diagnostics` counts by default and lists on request.** The text
582
+ output is a severity line, then counts by code (with each code's rule and
583
+ page) and by file, most frequent first and the file list capped at 25;
584
+ `--all` lists every line as before, and `--code <code>`, `--lesson <id>` or
585
+ `--changed` list a part. The JSON report is the same shape it was, filtered
586
+ when a flag is given. `summarizeDiagnostics`, `filterDiagnostics` and
587
+ `formatDiagnosticsSummary` are exported from `explico/compiler/cli`.
588
+ - **`validate:content` passes on its first run.** It still rewrites
589
+ `build/resolution/*`, but it fails only on a report git tracks that it had
590
+ to rewrite (the committed-report contract a course that tracks `build/`
591
+ relies on) and lists the rest as refreshed (`refreshed` on
592
+ `syncResolutionReports`' result). A course whose `build/` is git-ignored no
593
+ longer fails and asks for a commit of files git ignores; a "run it twice"
594
+ instruction can go. `scripts/agent/verify` runs it once.
595
+ - **The LaTeX export starts the longest documents first**: the course, then
596
+ the parts, then the chapters, the largest folder first within each. The
597
+ course took longest and started last, so it ran alone at the end while the
598
+ other cores had nothing left to do. Output is unchanged.
599
+ - **The README and the wiring errors show `markdown.processor: unified({ … })`.**
600
+ Astro 7.3 deprecates the flat `markdown.remarkPlugins` / `rehypePlugins`
601
+ lists. `assertRemarkPluginsWired` still reads both surfaces; only its
602
+ messages and the README sketch name the processor form.
603
+ `docs/authoring/migrating.md` has the step to move a course's lists.
604
+ - **The Download button's tooltip names what its menu offers.** It said
605
+ "Download this page" everywhere, but the menu lists the scopes that contain
606
+ the page: this chapter, this part and the whole course on a chapter; this
607
+ part and the whole course on a part's page; the whole course on any other
608
+ page, which has no download of its own. The `title` now reads "Download this
609
+ chapter, this part or the whole course", "Download this part or the whole
610
+ course" or "Download the whole course", in the menu's own words. The
611
+ accessible name is still "Download this page", so a browser test that finds
612
+ the button by role and name keeps working.
613
+
614
+ - **The text size on touch screens runs from 50% to 100% in five geometric
615
+ steps**: 50, 59, 71, 84 and 100%, each 2^(1/4) (about 1.19) times the one
616
+ below, listed smallest first. It was 100, 90, 80 and 70%. `global.css` has
617
+ one `:root[data-text-size='…']` rule per step and
618
+ `TextSizeControl.astro` builds the list. A size the earlier release saved
619
+ (90, 80 or 70) is ignored: the page opens at 100% until the reader picks a
620
+ new one. `<html>` now also carries `--explico-text-scale`, the choice as a
621
+ number from 0.5 to 1, for what is drawn in px.
622
+
623
+ - **A diagram follows the text size as a whole**, boxes and text together, so
624
+ a wide diagram can be made to fit a phone: the phone menu's 50% takes a
625
+ 687 px diagram to 344 px. It is scaled the way print already scales it:
626
+ the SVG stays at its build-time size (WebKit does not scale a
627
+ `foreignObject`'s HTML with the `viewBox`) and the HTML canvas around it
628
+ takes a `transform`. `Diagram.astro` therefore has a new
629
+ `div.diagram-frame` between `.diagram-scroll` and `.diagram-canvas`; it
630
+ holds `--diagram-width` and `--diagram-height` (the canvas inherits them)
631
+ and takes the scaled size, which is what the scroller measures, so the
632
+ widen button ↔ appears or goes with the size
633
+ (`DiagramEnhancer.astro` watches the frame). Course CSS or tests that
634
+ select `.diagram-scroll > .diagram-canvas` as a child must use a
635
+ descendant selector. Only a diagram in the lesson (also when expanded)
636
+ scales; one in a preview card keeps its size and the card its 12 px text.
637
+ With no size chosen, and on a screen with hover, nothing changes.
638
+
639
+ - **The sidebar draws a chapter, a part and a deck differently.** A compact
640
+ part's chapter tile is one sheet with lines of text on it (it was two
641
+ plain sheets, as the part's Overview tile still is). A deck is a filled
642
+ slide frame with a play mark cut out of it, on its "Slides" row and its
643
+ tile; a slides-first lesson's row carries it before its title, as the
644
+ Overview row carries its sheets, and its tile shows it in place of a
645
+ sheet. The manifest's `sidebar` lists a slides-first lesson as
646
+ `{ slug, attrs: { class: "explico-deck-lesson" } }` rather than its bare
647
+ slug, so `deck.css` can find it; it keeps its place and its chapter
648
+ number. The chapter's and the deck's glyphs are one SVG mask each over
649
+ the link's own colour (`--explico-chapter-glyph` in `global.css`,
650
+ `--explico-deck-glyph` in `deck.css`), the part's two sheets a border and
651
+ a shadow, so each takes the link's colour hovered, open or covered; the
652
+ Overview's second sheet now follows the tile's fill in those states.
653
+ - **A chapter's tile and its deck's tile touch**, one pair in a compact
654
+ part: square where they meet, with one line between them that lights with
655
+ whichever tile is hovered, and accent around the open page's tile. The
656
+ sidebar's script makes the two one row (below), and `li[data-editions="pair"]`
657
+ joins them; without the script, `:has()` does, and a browser without it
658
+ keeps the gap.
659
+ - **A lesson is one row in the sidebar.** A lesson with a chapter and a deck
660
+ was two rows, the chapter and a "Slides" row under it. It is one row now:
661
+ the row opens the lead edition under the lesson's title, and the other
662
+ edition is an icon at the row's end, as `»` ends a part's header, or, in a
663
+ compact part, the second of two joined tiles, the lead first
664
+ (`docs/plans/deck-navigation/`, phase 2). The icon's accessible name,
665
+ and its tooltip, is "Slides: *title*" or "Chapter: *title*", so a browser
666
+ test that finds a sidebar link by a lesson's title without `exact: true`
667
+ now finds two. A lesson with one edition carries its glyph before its title
668
+ only when that edition does not lead: a slides-first lesson while chapters
669
+ lead, as before, and a chapter-only lesson while slides lead
670
+ (`explico-chapter-lesson`, a sheet). A hidden edition is listed while it is
671
+ the open page. The row's own link carries its mark at the row's end, as
672
+ before; the icon carries its own on its corner
673
+ (`a.layout-edition-other[data-page-mark]`, drawn by `i.layout-edition-mark`),
674
+ and the progress rail counts rows, the row holding the open page being the
675
+ reader's place. The two controls (above, under Added) and the marks' line
676
+ share the panel's foot, `.explico-sidebar-foot`, which draws the separator
677
+ the line drew. Without scripts the sidebar is what the compiler emits: each
678
+ chapter, then its "Slides" row. In the manifest's `sidebar`, every item of
679
+ a lesson names its edition, lesson and title (`data-edition`,
680
+ `data-lesson`, `data-title` in `attrs`), so a chapter is `{ slug, attrs }`
681
+ where it was a bare slug; `sidebarLessonSlug` reads an item's lesson as its
682
+ `data-lesson` when that is its own `slug`, and `buildSidebar` takes
683
+ `titleOf`, the lesson titles.
684
+ - **Pointing at a part's `»` lights its Overview row**, or its compact tile,
685
+ as a pointer on the row would: both open the part's page. Keyboard focus
686
+ on `»` counts.
687
+ - **The marks' line stays, and offers "Mark all as unseen" beside "Mark all
688
+ as seen".** The line under the chapter list is there whenever the reader's
689
+ record is kept, with a mark on screen or none, so either reset can be
690
+ undone; "Mark all as unseen" forgets every page but the open one, which is
691
+ open. What a button did is said in a `role="status"` line inside it
692
+ ("Every page is marked as seen." / "… as not opened yet."), where
693
+ "Mark all as seen" used to replace the line with that sentence and leave
694
+ no way back. A button with nothing to change is `aria-disabled`: it keeps
695
+ its place, and focus.
696
+ - **The sidebar's "Mark all as seen" and "Mark all as unseen" cover every
697
+ deck's frames too** (D9 of `docs/plans/deck-navigation/`), so a deck's
698
+ page shows them all seen, or none, at once and on its next load. In a
699
+ course with a deck, the status they leave says so: "Every page and slide
700
+ is marked as seen." and "Every page is marked as not opened yet, and every
701
+ slide as not shown yet." The marks' payload (`PageMarksPayload.decks`)
702
+ carries each deck's frame ids, and a button is in use while a deck's frame
703
+ has something to change, as a page does. A deck's page now holds a
704
+ "Present from here, slide N" button in every frame's line, so a browser
705
+ test that finds the deck's Present button by name adds `exact: true`.
706
+
707
+ ### Fixed
708
+
709
+ - **A hovered row in either sidebar shows its fill in the dark theme.** The
710
+ fill was `--sl-color-gray-6`, which is the dark course sidebar's own
711
+ colour, so a hovered chapter or part there changed only its text colour.
712
+ The fill is now each sidebar's colour with 10 % ink mixed in
713
+ (`--ex-row-hover`), whatever colour the sidebar is.
714
+ - **A single-language course no longer warns that Starlight's `i18n`
715
+ collection is missing or empty.** `explico/content-config` now exports the
716
+ collection with Starlight's schema and an empty English in-memory fallback;
717
+ authored JSON or YAML translations under `content/i18n/` replace it. The
718
+ playground also declares its canonical Astro `site`, clearing the sitemap
719
+ warning alongside its new `404.mdx`.
720
+ - **With a part's deck open, the chapter on screen lights its slides in the
721
+ sidebar, and the rail follows it.** Opened at a chapter's section
722
+ (`/motion/slides/#slide-17`) only the part's deck was lit, not the
723
+ chapter's slides icon that opened it, and the rail stopped at the part's
724
+ row. Each frame of a covered section now carries its chapter
725
+ (`data-chapter`, from `frameChapters` in `file.data.explicoDeck`), and the
726
+ deck's page tells the sidebar the chapter of the frame in view on the
727
+ study page, or shown in Present mode and the presenter
728
+ (`DECK_FRAME_EVENT`, `explico:deck-frame`, with `{ deck, chapter }`, and
729
+ `data-deck-chapter` on `<html>`). The sidebar lights that chapter's section
730
+ link with `aria-current="location"`, as the open page lights its own, and
731
+ the rail covers every row before it; the title and outcomes frames, which
732
+ belong to no chapter, light none.
733
+ - **"Continue at slide N" is offered whenever a reader left a deck past its
734
+ first frame, and presents from there.** Present mode writes `#slide-N`
735
+ into the address so a slide can be shared, and the offer was made only on
736
+ a page opened without one, once: after Esc, and after a reload of that
737
+ address, it was missing. `refreshResume` in `deck-controller.ts` now
738
+ renders it as the page opens and again whenever Present mode ends,
739
+ whatever the address says: a button (`data-deck-resume`) beside Present,
740
+ where it was a link to the frame, that presents from the frame to continue
741
+ at (above). Once the reader was shown all of the last frame it reads
742
+ "Present again" and presents from the first.
743
+ It is the reader's own (`data-explico-export-chrome`), so a deck's
744
+ standalone HTML and Markdown never carry it. A link to `#slide-N` still
745
+ opens the study page at that frame.
746
+ - **Opening a page no longer writes its sections' state to the browser.**
747
+ `wirePersistedDetails` meant to save only a section the reader folded or
748
+ opened, but restoring a section's default state fired a `toggle` of its
749
+ own, which it saved too: every page with collapsible sections left one
750
+ `disclosure:<path>:<section>` entry per section on a mere visit, a list of
751
+ the pages a reader opened. Only a state the reader moved a section to is
752
+ saved now.
753
+ - **A notation entry's body honours `fixedIndex` (rule N23).** Its glyph table
754
+ dropped the flag that a lesson's keeps, so in the glossary `r_2` took the
755
+ card of the family `r_m` although `m` was marked `fixedIndex: true`. The
756
+ entry's body and a lesson now agree; the error names the card that fixed the
757
+ index. The render's message for an entry's body no longer suggests
758
+ `notation.local`, which an entry does not have.
759
+ - **`astro build` reports a notation entry's error at the file's own line.**
760
+ Astro's glob loader renders an entry from its body alone, so the line in
761
+ `entry.md:1:43` (the message, and the loader's `Failed to parse Markdown file`
762
+ header) counted from the end of the frontmatter. `remark-notation.mjs` now
763
+ finds where the body sits in the file (`source-lines.mjs`, read only while a
764
+ failure or warning is reported) and moves the location, so the build and
765
+ `validate:content` agree. A lesson is compiled whole and was already right.
766
+ - **An unresolved-glyph error quotes the math it is in** (`… "w", in $w_3$.`),
767
+ in `astro build` and in `validate:content`, so a symbol can be searched for
768
+ whatever the line says. The sentence before `Introduce the symbol …` gains
769
+ `, in $…$`; a test matching the text up to the token is unaffected.
770
+ - **The published package loads again.** 0.13.0 and 0.13.1 shipped
771
+ `dist/compiler/exports/{latex,markdown}/serialize.js` importing
772
+ `dist/components/float-name.js`, which no one built, so `astro build`,
773
+ `astro dev` and `astro check` stopped with "Unable to load your Astro
774
+ config". `float-name.ts` (no Astro, no DOM beyond the `Element` that
775
+ `floatOf` takes) moved from `src/components/` to `src/reference/`, which
776
+ tsup compiles. It was never a documented author-facing export; a course
777
+ that patched the module in (`patches/explico@0.13.1.patch`) deletes the
778
+ patch.
779
+ - **`migrating.md`'s 0.13.0 → 0.13.1 section said the manifest schema stays
780
+ v29.** It is v31.
781
+ - **A figure no longer fails on `e`, `\pi`, `i` or `d`.** `renderLabMath`
782
+ leaves a plain base-library atom without a card, as a lesson's math does, but
783
+ `assertFigureSymbolsBound` / `unboundFigureSymbols` (`figure-symbols.ts`)
784
+ then failed the build on it ("figure symbols without a notation card"). The
785
+ check now exempts the same library (`isBaseLibraryGlyph`,
786
+ `base-library.mjs`), in a power too (`e^{i\pi}`). An index (`e_i`, rule N23),
787
+ a decorated (`\hat{e}`, rule N22) or a styled (`\mathbf{e}`) letter is
788
+ another symbol and is still reported.
789
+ - **The build reports every page with a figure symbol that has no card, or a
790
+ notation trigger with no popup entry, at once.** `astro:build:done` threw on
791
+ the first page and never reached the later ones or the exports. It now checks
792
+ all pages and throws once with the whole list (`assertNoUnboundFigureSymbols`
793
+ is new in `figure-symbols.ts`; `assertFigureSymbolsBound` is unchanged). The
794
+ standalone, part, course and LaTeX exports still wait for a clean check.
795
+ - **`lab-katex-direct` is reported per lesson.** A helper that imports `katex`
796
+ was reported once, against the first lesson that reached it, so
797
+ `content check <lesson>` could not tell whether another lesson was clean. It
798
+ is now reported once for each lesson whose imports reach it, with that
799
+ lesson's own chain, and a lesson that reaches none is clean. The code and
800
+ severity are unchanged. `validate:content` prints one error per lesson while
801
+ several share a helper.
802
+ - **A card only a lab template uses now reaches the page.** The package serves
803
+ `explico/reference/*` from `dist/`, but `LessonFooter.astro` imports the
804
+ TypeScript source of `page-notation`, so the two copies had separate
805
+ registries. A key bound by `labMathScope` or `renderLabMath` in a template
806
+ (and a derived N23 card it registered) was never in the card payload, and the
807
+ hover opened nothing. The registries now live on `globalThis` under
808
+ `Symbol.for('explico.reference.page-notation')`, one per process.
809
+ - **Rule N23 no longer asks for the set of a letter that is not an index.**
810
+ `index-range` named `b` in `\nabla_{b_n}` (the base of the carded symbol
811
+ `b_n`, whose own index `n` is checked in its own right) and `g` in
812
+ `\mathbb{E}_{Z \sim g}` (a density in a condition), and asked for a `range`
813
+ on the card of `b_n` as "the set of the running index b". A letter that sits
814
+ inside a larger symbol with a card of its own, and every letter of a
815
+ subscript slot that holds a relation (`\sim`, `=`, `\in`, …), is no longer a
816
+ running index. A letter with a card of its own, and one inside an author's
817
+ `\explain`, is checked as before.
818
+ - **A family whose index is juxtaposed letters now reaches its instances.** The
819
+ entry `x_{ij}` (`K_{ij}`, `r_{ik}`) gave its card to the exact form only, so
820
+ `x_{i1}`, `x_{12}` and `K_{ji}` failed rule N4 as unresolved and the message
821
+ said the entry "explains its own instances only". A slot of two or more
822
+ lowercase letters is now a double index: the family matches a run of as many
823
+ letters and digits (`x_{i1}`, `x_{12}`, `x_{ji}`), as `x_{i,j}` always did,
824
+ and never `x_i`, `x_{ijk}` or `x_{i+1}`. A page that has both `x_{ij}` and
825
+ `x_k` as entries now fails `ambiguous-base-fallback` on `x_{12}`, which both
826
+ fit; wrap that instance in `\explain`.
827
+ - **An `\explain` inside a family instance's index no longer unbinds the
828
+ family.** `\beta_{\explain{count}{K}}` and `x_{\explain{iter}{k}}` left the
829
+ base unresolved, because the match was dropped for enclosing the author's
830
+ wrap. A symbol that holds an `\explain` in its own subscript now keeps its
831
+ card, and the two wraps nest: `\explain{beta-j}{\beta_{\explain{count}{K}}}`.
832
+ A match that only straddles a wrap's braces is still dropped.
833
+ - **A wrapped family instance is not re-bound inside.** On a page with the
834
+ family `\sigma_k` and a plain `\sigma`, `\explain{component-sd}{\sigma_1}`
835
+ became `\explain{component-sd}{\explain{population-sd}{\sigma}_1}`, so hovering
836
+ the σ opened the population standard deviation. The wrap names the indexed
837
+ symbol, so its base takes no other entry's card; this holds under a power
838
+ (`\sigma_1^2`), for a two-letter family and for a font-styled base. A wrap
839
+ around a symbol whose key has another base still binds the glyphs inside.
840
+ - **An active prose link no longer falls below AA contrast.** 0.13.1 blended
841
+ the highlight of every active notation target (`mix-blend-mode: multiply`
842
+ in the light theme, `screen` in the dark one), but the blend exists for a
843
+ symbol in math, whose KaTeX boxes paint over the glyph to their left. On a
844
+ prose term link (`a.notation-term`), `multiply` darkened the tint over a
845
+ glossary row's panel and the engine's own link colour dropped from 4.75:1
846
+ to 4.4:1, which axe reports as `color-contrast`. The blend is now scoped to
847
+ `.katex-html`; a prose link keeps the opaque tint it had before 0.13.1. A
848
+ course that added `a.notation-term.is-notation-active { color: … }` to pass
849
+ axe deletes it.
850
+ - **`unresolved-glyph` names the card that fixed a family's index.** Marking
851
+ the card of `m` `fixedIndex: true` turns the family `r_m` into a single
852
+ symbol (rule N23), so `r_2` stops being its instance. The error then blamed
853
+ "a bare r" and offered `\explain{periodic-rate}{r}`, which is the wrong
854
+ repair. For a subscripted symbol that a family would have explained, it now
855
+ says "`r_2` would be an instance of `periodic-rate`, but `m` is marked
856
+ `fixedIndex` on `compounding-frequency`", and to give that card a `range`
857
+ if `m` runs over a set. The same holds for an entry that carries
858
+ `fixedIndex` itself, and for an index entry that fixes the subscript. A
859
+ truly bare `r` keeps its message. The error `astro build` throws for the
860
+ same symbol says it too. `index-range`'s offer of `fixedIndex: true` now
861
+ says that a family indexed by that letter then explains none of its
862
+ instances. `resolveMathGlyphs` returns `instance` and `fixedLenders` on such
863
+ an unresolved item, and `math-glyphs.mjs` exports `fixedLenderMessage`.
864
+
865
+ - **The `[?]` About panel opens on screen in the phone menu.** The menu's
866
+ copy of the butto
867
+ ### Removed
868
+
869
+ - **`gloss-wants-promoting`.** It fired on three identical glosses and never
870
+ did in any course; `meaning-repeated` reports the same copy at two. A
871
+ `lint-ignore.yml` line for it can be deleted.
872
+ n starts its row, and the panel hung from the button's end,
873
+ so it ran off the screen's start edge and past the end of the menu. Below
874
+ 50rem it is now a sheet fixed under the header, centred, at most 21rem
875
+ wide.
876
+
877
+ - **The `[?]` button is round in the phone menu.** The sidebar's rules for a
878
+ part's header (`.layout-navigation-content summary`) are unlayered and
879
+ reached it, making it a rounded square with the `?` at its start; they now
880
+ skip `.about-toggle`.
881
+
882
+ - **A hover peek no longer overwrites a collapsed section's saved state.** A
883
+ collapsed section peeks (its content floats open) when the pointer is on its
884
+ title, and a peek has to set `details.open = true` to render, which fired the
885
+ native `toggle` that `wirePersistedDetails` saves as "open". A reader who
886
+ collapsed a section and reloaded with the pointer still on its title (the
887
+ browser sends the pointer's `pointerenter` after the reload) found it open
888
+ from then on: `open` was stored over `closed`. A peek now carries
889
+ `data-explico-temporary-open`, which `persisted-disclosure.ts` already skips,
890
+ from its first `open = true` until the `toggle` its closing fires; a click
891
+ that commits the peek removes it. `CollapsibleSections.astro`'s module
892
+ comment already said a peek "never persists anything".
893
+
894
+ ## [0.13.1] - 2026-10-07
895
+
896
+ ### Fixed
897
+
898
+ - **A hovered symbol in an equation no longer hides the glyph to its left.**
899
+ The highlight (`[data-notation-key]:hover` / `.is-notation-active` inside
900
+ `.katex-html`, and the same symbol inside a card's formula) was an opaque
901
+ `accent-low` background with a 0.12em ring, and KaTeX paints its boxes in
902
+ source order, so hovering the `y` of `dy` covered the right half of the
903
+ `d`. The highlight now blends with what is behind it
904
+ (`mix-blend-mode: multiply` in the light theme, `screen` in the dark one),
905
+ which tints the page and leaves the ink as it was. Forced-colors mode
906
+ resets the blend to `normal`.
907
+
17
908
  ## [0.13.0] - 2026-10-07
18
909
 
19
910
  ### Breaking
@@ -3432,7 +4323,9 @@ under **Breaking** below; a PATCH never breaks.
3432
4323
  system with its completeness gate, the curriculum-graph validator, the
3433
4324
  Astro/Starlight UI, the LMS / session / analytics seams, and the `content` CLI.
3434
4325
 
3435
- [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.13.0...HEAD
4326
+ [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.14.0...HEAD
4327
+ [0.14.0]: https://github.com/danieltuzes/explico/compare/v0.13.1...v0.14.0
4328
+ [0.13.1]: https://github.com/danieltuzes/explico/compare/v0.13.0...v0.13.1
3436
4329
  [0.13.0]: https://github.com/danieltuzes/explico/compare/v0.12.0...v0.13.0
3437
4330
  [0.12.0]: https://github.com/danieltuzes/explico/compare/v0.11.1...v0.12.0
3438
4331
  [0.11.1]: https://github.com/danieltuzes/explico/compare/v0.11.0...v0.11.1