explico 0.8.0 → 0.11.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 (230) hide show
  1. package/CHANGELOG.md +1239 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +46 -2
  5. package/dist/compiler/cli.js +70 -4
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +68 -5
  8. package/dist/compiler/course-config.d.ts +3 -194
  9. package/dist/compiler/course-config.js +78 -5
  10. package/dist/compiler/exports/standalone-html.d.ts +6 -1
  11. package/dist/compiler/knowledge-version.js +1 -1
  12. package/dist/compiler/lesson-derivation.d.ts +27 -10
  13. package/dist/compiler/lesson-derivation.js +23 -8
  14. package/dist/compiler/logging.d.ts +4 -1
  15. package/dist/compiler/logging.js +4 -1
  16. package/dist/compiler/manifest.d.ts +204 -31
  17. package/dist/compiler/manifest.js +270 -55
  18. package/dist/compiler/resolution-report.d.ts +6 -1
  19. package/dist/course-config-Dsnri9Tk.d.ts +464 -0
  20. package/dist/reference/base-accents.d.mts +26 -0
  21. package/dist/reference/base-accents.mjs +31 -0
  22. package/dist/reference/base-roman-labels.d.mts +2 -2
  23. package/dist/reference/base-roman-labels.mjs +2 -2
  24. package/dist/reference/consistency.d.ts +14 -2
  25. package/dist/reference/consistency.js +108 -4
  26. package/dist/reference/delimiter-size.d.ts +3 -2
  27. package/dist/reference/diagrams-validate.d.ts +7 -2
  28. package/dist/reference/diagrams-validate.js +9 -1
  29. package/dist/reference/equations-validate.d.ts +8 -5
  30. package/dist/reference/equations-validate.js +1 -0
  31. package/dist/reference/equations.d.mts +60 -31
  32. package/dist/reference/equations.mjs +83 -43
  33. package/dist/reference/examples-validate.d.ts +31 -0
  34. package/dist/reference/examples-validate.js +48 -0
  35. package/dist/reference/figures-validate.d.ts +1 -1
  36. package/dist/reference/figures-validate.js +4 -1
  37. package/dist/reference/folds-validate.d.ts +33 -0
  38. package/dist/reference/folds-validate.js +32 -0
  39. package/dist/reference/gate-abbreviations.d.ts +3 -1
  40. package/dist/reference/gate-abbreviations.js +1 -0
  41. package/dist/reference/gate-course-rules.d.ts +32 -0
  42. package/dist/reference/gate-course-rules.js +85 -0
  43. package/dist/reference/gate-heading-length.d.ts +5 -2
  44. package/dist/reference/gate-heading-length.js +45 -21
  45. package/dist/reference/gate-heading-structure.d.ts +41 -0
  46. package/dist/reference/gate-heading-structure.js +113 -0
  47. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  48. package/dist/reference/gate-math-punctuation.js +251 -0
  49. package/dist/reference/gate-math.d.ts +22 -6
  50. package/dist/reference/gate-math.js +42 -8
  51. package/dist/reference/gate-part-pages.d.ts +50 -0
  52. package/dist/reference/gate-part-pages.js +118 -0
  53. package/dist/reference/gate-prose-notation.d.ts +21 -11
  54. package/dist/reference/gate-prose-notation.js +25 -7
  55. package/dist/reference/katex-delimiters.mjs +20 -7
  56. package/dist/reference/keyed-float-validate.d.ts +27 -10
  57. package/dist/reference/keyed-float-validate.js +15 -1
  58. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  59. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  60. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  61. package/dist/reference/knowledge-checks.d.mts +3 -1
  62. package/dist/reference/knowledge-checks.mjs +9 -4
  63. package/dist/reference/lab-math-scope.d.ts +1 -0
  64. package/dist/reference/lab-math-slot.d.ts +34 -0
  65. package/dist/reference/lab-math-slot.js +6 -0
  66. package/dist/reference/math-glyphs.d.mts +80 -4
  67. package/dist/reference/math-glyphs.mjs +572 -37
  68. package/dist/reference/page-notation.d.ts +6 -1
  69. package/dist/reference/raw-floats-validate.js +1 -1
  70. package/dist/reference/raw-floats.mjs +1 -1
  71. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  72. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  73. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  74. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  75. package/dist/reference/remark-asides.d.mts +3 -0
  76. package/dist/reference/remark-asides.mjs +180 -0
  77. package/dist/reference/remark-memo.d.mts +3 -1
  78. package/dist/reference/remark-memo.mjs +42 -11
  79. package/dist/reference/remark-notation.d.mts +60 -10
  80. package/dist/reference/remark-notation.mjs +387 -36
  81. package/dist/reference/render-lab-math.d.ts +4 -26
  82. package/dist/reference/render-lab-math.js +4 -4
  83. package/dist/reference/sections-validate.d.ts +27 -0
  84. package/dist/reference/sections-validate.js +41 -0
  85. package/dist/reference/sections.d.mts +97 -0
  86. package/dist/reference/sections.mjs +306 -0
  87. package/dist/reference/tables-validate.d.ts +1 -1
  88. package/dist/reference/types.d.ts +17 -1
  89. package/dist/reference/unit-words.d.ts +37 -0
  90. package/dist/reference/unit-words.js +170 -0
  91. package/dist/rules/catalog.d.ts +176 -0
  92. package/dist/rules/catalog.js +983 -0
  93. package/dist/rules/course-rules.d.ts +115 -0
  94. package/dist/rules/course-rules.js +292 -0
  95. package/dist/rules/course-rules.schema.json +138 -0
  96. package/dist/rules/in-effect.d.ts +53 -0
  97. package/dist/rules/in-effect.js +142 -0
  98. package/dist/rules/rules.schema.json +1072 -0
  99. package/dist/rules/settings.d.ts +3 -0
  100. package/dist/rules/settings.js +290 -0
  101. package/dist/rules/waivers.d.ts +53 -0
  102. package/dist/rules/waivers.js +132 -0
  103. package/dist/session/PreferenceStore.d.ts +5 -0
  104. package/dist/session/PreferenceStore.js +9 -0
  105. package/docs/VERSIONING.md +2 -2
  106. package/docs/authoring/README.md +232 -40
  107. package/docs/authoring/agents.md +55 -9
  108. package/docs/authoring/components.md +178 -22
  109. package/docs/authoring/migrating.md +1096 -2
  110. package/docs/authoring/notation.md +576 -322
  111. package/docs/authoring/prose.md +551 -0
  112. package/docs/authoring/retired.md +35 -0
  113. package/docs/authoring/structure-rules.md +422 -40
  114. package/docs/authoring/structure.md +445 -62
  115. package/docs/authoring/versioning.md +20 -28
  116. package/docs/authoring/visualizations.md +142 -20
  117. package/package.json +2 -2
  118. package/src/compiler/cli.ts +116 -6
  119. package/src/compiler/collections.ts +111 -7
  120. package/src/compiler/course-config.ts +273 -34
  121. package/src/compiler/knowledge-version.ts +1 -1
  122. package/src/compiler/lesson-derivation.ts +54 -15
  123. package/src/compiler/logging.ts +8 -2
  124. package/src/compiler/manifest.ts +573 -104
  125. package/src/components/BlockHeading.astro +113 -0
  126. package/src/components/CourseContents.astro +221 -0
  127. package/src/components/CurriculumMap.astro +5 -6
  128. package/src/components/FloatIndex.astro +200 -0
  129. package/src/components/HoverPanel.astro +431 -79
  130. package/src/components/PartBridge.astro +27 -0
  131. package/src/components/PartOverview.astro +401 -0
  132. package/src/components/PartScope.astro +27 -0
  133. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  134. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  135. package/src/components/assessments/AssessmentSet.astro +23 -26
  136. package/src/components/block-heading.ts +146 -0
  137. package/src/components/card-arrow.ts +243 -0
  138. package/src/components/card-drag.ts +631 -0
  139. package/src/components/card-jump.ts +45 -0
  140. package/src/components/card-manager.ts +1589 -0
  141. package/src/components/card-memory.ts +177 -0
  142. package/src/components/card-placement.ts +504 -0
  143. package/src/components/card-side.ts +336 -0
  144. package/src/components/card-travel.ts +197 -0
  145. package/src/components/citation/CitationLayer.astro +77 -76
  146. package/src/components/diagram/Diagram.astro +161 -316
  147. package/src/components/disclosure/course-disclosure.ts +24 -0
  148. package/src/components/disclosure/hover-preview.ts +122 -42
  149. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  150. package/src/components/disclosure/print-as-shown.ts +252 -0
  151. package/src/components/examples/CompactExample.astro +3 -1
  152. package/src/components/examples/CompactExamples.astro +101 -94
  153. package/src/components/figure/Figure.astro +174 -274
  154. package/src/components/figure/FloatCaption.astro +87 -0
  155. package/src/components/figure/figure-state.ts +58 -0
  156. package/src/components/figure/figure-tools.ts +87 -0
  157. package/src/components/float-name.ts +101 -0
  158. package/src/components/fold/Fold.astro +125 -0
  159. package/src/components/fold/fold-id.ts +40 -0
  160. package/src/components/memo/Memo.astro +162 -231
  161. package/src/components/memo/ShortNote.astro +6 -6
  162. package/src/components/notation/NotationFormula.astro +5 -1
  163. package/src/components/notation/NotationGlossary.astro +3 -4
  164. package/src/components/notation/NotationLayer.astro +169 -162
  165. package/src/components/notation/NotationMath.astro +10 -1
  166. package/src/components/reference/FloatRefLayer.astro +485 -423
  167. package/src/components/starlight/AboutPanel.astro +5 -4
  168. package/src/components/starlight/CollapsibleSections.astro +207 -93
  169. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  170. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  171. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  172. package/src/components/starlight/LayoutHeader.astro +298 -19
  173. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  174. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  175. package/src/components/starlight/LayoutSidebar.astro +229 -11
  176. package/src/components/starlight/LessonExportActions.astro +53 -72
  177. package/src/components/starlight/LessonFooter.astro +26 -4
  178. package/src/components/starlight/section-parts.ts +15 -2
  179. package/src/components/starlight/section-summary.ts +181 -0
  180. package/src/components/table/Table.astro +95 -76
  181. package/src/components/table/TableOverflowExpander.astro +65 -28
  182. package/src/content-config.ts +40 -4
  183. package/src/reference/base-accents.mjs +31 -0
  184. package/src/reference/base-roman-labels.mjs +2 -2
  185. package/src/reference/consistency.ts +176 -5
  186. package/src/reference/delimiter-size.ts +3 -2
  187. package/src/reference/diagrams-validate.ts +17 -1
  188. package/src/reference/equations-validate.ts +10 -5
  189. package/src/reference/equations.mjs +83 -43
  190. package/src/reference/examples-validate.ts +97 -0
  191. package/src/reference/figures-validate.ts +6 -1
  192. package/src/reference/folds-validate.ts +65 -0
  193. package/src/reference/gate-abbreviations.ts +1 -0
  194. package/src/reference/gate-course-rules.ts +134 -0
  195. package/src/reference/gate-heading-length.ts +66 -35
  196. package/src/reference/gate-heading-structure.ts +193 -0
  197. package/src/reference/gate-math-punctuation.ts +381 -0
  198. package/src/reference/gate-math.ts +69 -10
  199. package/src/reference/gate-part-pages.ts +217 -0
  200. package/src/reference/gate-prose-notation.ts +59 -12
  201. package/src/reference/katex-delimiters.mjs +20 -7
  202. package/src/reference/keyed-float-validate.ts +45 -11
  203. package/src/reference/keyed-jsx-floats.mjs +73 -20
  204. package/src/reference/knowledge-checks-validate.ts +1 -1
  205. package/src/reference/knowledge-checks.mjs +9 -4
  206. package/src/reference/lab-math-slot.ts +36 -0
  207. package/src/reference/math-glyphs.mjs +572 -37
  208. package/src/reference/raw-floats-validate.ts +1 -1
  209. package/src/reference/raw-floats.mjs +1 -1
  210. package/src/reference/rehype-heading-anchors.mjs +116 -17
  211. package/src/reference/rehype-table-header-labels.mjs +21 -3
  212. package/src/reference/remark-asides.mjs +180 -0
  213. package/src/reference/remark-memo.mjs +42 -11
  214. package/src/reference/remark-notation.mjs +387 -36
  215. package/src/reference/render-lab-math.ts +16 -31
  216. package/src/reference/sections-validate.ts +81 -0
  217. package/src/reference/sections.mjs +306 -0
  218. package/src/reference/tables-validate.ts +2 -1
  219. package/src/reference/types.ts +17 -0
  220. package/src/reference/unit-words.ts +233 -0
  221. package/src/rules/catalog.ts +1298 -0
  222. package/src/rules/course-rules.ts +456 -0
  223. package/src/rules/in-effect.ts +235 -0
  224. package/src/rules/settings.ts +448 -0
  225. package/src/rules/waivers.ts +249 -0
  226. package/src/session/PreferenceStore.ts +15 -0
  227. package/src/styles/global.css +1471 -163
  228. package/src/styles/surfaces.css +182 -0
  229. package/src/components/hover-panel.ts +0 -339
  230. package/src/components/popup-stack.ts +0 -345
package/CHANGELOG.md CHANGED
@@ -14,6 +14,1241 @@ under **Breaking** below; a PATCH never breaks.
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.11.0] - 2026-09-28
18
+
19
+ ### Breaking
20
+
21
+ - **Figures and diagrams no longer compact.** A figure or a diagram is laid
22
+ out as LaTeX lays it out, in every state: its picture, then its caption
23
+ line (docs/plans/figures-and-short-captions.md, Part 1). The caption
24
+ line's compaction toggle (`.float-caption__toggle`), the `<details>`
25
+ around a figure's or a diagram's body (`.keyed-figure__details`,
26
+ `.diagram__details`, `.float-compact`) and
27
+ `components/disclosure/float-compaction.ts` are gone, and print never
28
+ notes a compacted figure. To let a reader fold a float away, put it in a
29
+ collapsible section.
30
+ - **`disclosure.diagrams` is removed.** A course that still sets it fails
31
+ the build (`normalizeDisclosure` throws) with the edit to make: delete the
32
+ key. `disclosure.figures` stays, and now sets whether a lab's tools start
33
+ open — as `defaultOpen` and `rememberState` on a `<Figure>` now do for one
34
+ figure's tools (below).
35
+ - **`<Diagram defaultOpen>` and `<Diagram rememberState>` are removed.**
36
+ Either fails `pnpm validate:content` (`dia-removed-prop`), and a diagram
37
+ that a wrapper component renders fails when it renders.
38
+ - **The manifest schema is now v24**: `disclosure` loses `diagrams` and
39
+ gains `folds`; `diagnostics.folds` is new (below). A course script that
40
+ lists the diagnostic groups it blocks on (`compile-manifest.ts`) should
41
+ add `folds`.
42
+
43
+ ### Added
44
+
45
+ - **Rule S9** (guide; docs/plans/figures-and-short-captions.md, Part 7): a
46
+ float's short caption names it, and its long caption goes on from that
47
+ name instead of restating it; a figure holds no running text, and a lab's
48
+ instructions go in its tools (`structure-rules.md#s9`). With the short
49
+ caption printed as the caption line's title, `components.md` now asks
50
+ for one on every figure, table and diagram.
51
+ - **`FloatIndex.astro`: the "Figures and tables" page**
52
+ (docs/plans/figures-and-short-captions.md, Part 6) — LaTeX's
53
+ `\listoffigures` and `\listoftables` in one. It lists every numbered
54
+ figure, table and diagram with an `id`, by part and chapter (chapters
55
+ numbered as their sections are), each as its number, linked to its anchor,
56
+ and its short caption — or its long caption, muted, without one, or the
57
+ number alone when the manifest has neither. The sidebar's Reference group
58
+ lists "Figures and tables" when the course has a top-level
59
+ `content/docs/figures-and-tables.mdx` (`buildSidebar` takes
60
+ `{ floatIndex }`).
61
+ - **A lab's tools: `<Figure>`'s `tools` slot**
62
+ (docs/plans/figures-and-short-captions.md, Part 3). The default slot is
63
+ the picture; the `tools` slot — an instruction line, the sliders and
64
+ fields, the live equation — renders after the caption line, in a tray of
65
+ its own (a thin border, no tint) inside a `<details>`. Only the tools
66
+ fold: a fold button ⌃ / ⌄ ends the caption line (`.float-tool`, "Hide the
67
+ controls of Figure 2.1.1"), and folded tools leave a "Hidden here: 2
68
+ sliders, 1 equation" line that unfolds them
69
+ (`components/figure/figure-tools.ts`). They start open unless the course's
70
+ `disclosure.figures` or the figure's `defaultOpen` says otherwise, and the
71
+ reader's choice is remembered per page (`rememberState`). Print and export
72
+ show the figure as LaTeX would: the tools never print, and
73
+ `prepareDisclosures` leaves a `details[data-print-omit]` alone. A figure
74
+ without the slot has no button and no tray. `global.css` sets a lab's
75
+ tools island in the tray: `.keyed-figure__tools .lab-shell` brings no
76
+ margin, a `.lab-tools` root drops its last child's, and `.lab-instruction`
77
+ is the tools' first line, what to do.
78
+ - **The caption line's live end, and `setFigureState`.** A `<Figure>`'s
79
+ caption line ends with an empty `span.float-caption__state[data-figure-state]`,
80
+ outside `.float-caption__text`, where a lab writes the case its plot shows
81
+ — `setFigureState(element, runs)` in `components/figure/figure-state.ts`,
82
+ with runs of text, italic and subscript as data, never HTML. It prints
83
+ with the caption. `FloatCaption.astro` takes `live` for it, and renders
84
+ its button row only when the slot holds a button.
85
+ - **`<Fold>`: collapsible content with no heading, number or marker**
86
+ (`components/fold/Fold.astro`; docs/plans/figures-and-short-captions.md,
87
+ Part 2). It folds like a memo — the section chevron, "Hidden here: 1
88
+ diagram" while folded, the reader's choice remembered per page, opened by a
89
+ deep link or find-in-page, printed as shown — but it is the plain tier (no
90
+ frame or tint), puts no letter in the text, has no `[[…]]` key, is not in
91
+ "On this page" and takes no outline number. Its bar, semibold at
92
+ body-text size, is its `label` or, without one, the numbers and short
93
+ captions of the numbered floats inside ("Diagram 2.2.1 Inputs to the
94
+ launch-angle search", `components/float-name.ts`). A bar that names the
95
+ floats is their name: their caption lines inside the fold drop the bold
96
+ short-caption title, which would repeat it. This is how a figure or a diagram becomes
97
+ collapsible outside a collapsible section, now that floats do not fold by
98
+ themselves.
99
+ - **`disclosure.folds`** (`{ defaultOpen }`, closed by default) sets whether
100
+ a fold starts open; `defaultOpen` on one `<Fold>` overrides it, and
101
+ `rememberState={false}` keeps the default on every visit.
102
+ - **`fold-unnamed`** (error, the keyed-floats invariant;
103
+ `reference/folds-validate.ts`): a `<Fold>` with no `label` and no numbered
104
+ `<Figure>`, `<Table>` or `<Diagram>` inside fails `validate:content`, and
105
+ the component throws on it when it renders.
106
+ - A fold is transparent to numbering, like a memo: the equations and floats
107
+ in one keep their places in the lesson's sequence. The scans read one list,
108
+ `TRANSPARENT_COMPONENTS` in `reference/equations.mjs`.
109
+ - **Explanation cards can sit beside the article.** On a window at least
110
+ 72rem wide, a card's new Side button docks it in a side column to the
111
+ right of the article, level with its trigger's line
112
+ (docs/plans/hover-card-consistency.md, Part 2; `components/card-side.ts`).
113
+ For a reader:
114
+ - Docked cards are 18–23rem wide, never cover the navigation or the "On
115
+ this page" rail, stack top down without overlapping, and scroll with the
116
+ page; one taller than the window scrolls inside. Side again, a click on
117
+ the card's trigger, × or Escape work as on a pinned card; a click on
118
+ another occurrence of its item moves the card level with that line.
119
+ - The room comes from sliding the article left, only while cards are
120
+ docked and only as far as they need, never past the navigation. When
121
+ there is not enough room even then, the Side button says "Collapse a
122
+ side panel to make room"; below 72rem it is hidden.
123
+ - "Keep the article left", at the top of the side column, keeps the
124
+ article on the left after the last docked card closes. The reader's
125
+ choice is remembered across pages and visits.
126
+ - Pointing at another occurrence of a pinned or docked card's item, or at
127
+ a docked card itself, draws an arrow from the card to that occurrence;
128
+ it fades 1.5 s after both are left (`components/card-arrow.ts`).
129
+ - Moving into or out of the side and the article's slide animate in
130
+ 100 ms, and not at all under reduced motion; neither applies in print.
131
+ - A card with no free spot next to its trigger now goes to the side
132
+ column at its line, where there is room, before it would cover another
133
+ card.
134
+ - `HoverPanel.astro` renders the Side button (`data-card-side`) before Pin,
135
+ and a note line (`data-card-note`) under the header row.
136
+ - **Explanation cards can be dragged.** A grip at the start of every card's
137
+ header moves the card anywhere between the sidebars, or into the side
138
+ column (docs/plans/hover-card-consistency.md, Part 3;
139
+ `components/card-drag.ts`). For a reader:
140
+ - Only the grip drags, so text in the card stays selectable; a drag starts
141
+ after 4 px and keeps the card open. Dropped anywhere else, the card
142
+ stays there: it scrolls with the page, follows its trigger when the text
143
+ above reflows, and is never dropped over the navigation or the "On this
144
+ page" rail. Dropped within 32 px of its spot next to its trigger, it
145
+ goes back there, pinned.
146
+ - Where the window has a side column, the docking area is tinted during a
147
+ drag. A card whose left edge comes within 32 px of the article's edge
148
+ snaps: a dashed line marks the edge, a dashed outline shows where the
149
+ card will land, and "Release to pin to the side" says what a drop does.
150
+ Released, it docks at the height it was dropped at, the article sliding
151
+ first when the column needs room. It lets go again only past 48 px;
152
+ holding Alt (Option) turns snapping off.
153
+ - The grip is a button, "Move card": arrow keys move the card 8 px (48 px
154
+ with Shift) and Home puts it back next to its trigger. Escape during a
155
+ drag cancels it; the next Escape closes the card.
156
+ - Pointing at, or clicking, another occurrence of a moved card's item
157
+ flashes the card and draws its arrow; the card stays where it is. A
158
+ moved card, like a docked one, hides its Pin button.
159
+ - `HoverPanel.astro` renders the grip (`data-card-grip`) at the start of
160
+ the header row; a moved card carries `data-card-mode="moved"`.
161
+ - **A page remembers its cards.** The cards a reader keeps (pinned, docked
162
+ or moved) come back where they were when the reader returns to the page:
163
+ through a link, Back or a reload, in another tab, or after the browser
164
+ restarts, including a card opened from inside another. Hovered cards are
165
+ not kept, and closing a page's last card forgets the page
166
+ (`components/card-memory.ts`, in local storage).
167
+ - `PreferenceStore` gains an optional `remove(key)`; the browser and memory
168
+ stores implement it. A store without it still fits the interface.
169
+
170
+ ### Changed
171
+
172
+ - **A float's short caption shows** (docs/plans/figures-and-short-captions.md,
173
+ Part 5):
174
+ - it opens the caption line of a figure, table or diagram as a bold title,
175
+ `b.float-caption__title`, with a closing period unless it has its own
176
+ punctuation — "**Sampled flight data.** Position, …". It sits outside
177
+ `.float-caption__text`, so every reader of the long caption still reads
178
+ it alone. `FloatCaption.astro` takes `short`;
179
+ - a folded section's "Hidden here" line names each numbered float with a
180
+ short caption, number in bold, before the counts of everything else:
181
+ "Hidden here: **Figure 2.1.1** Projectile trajectory explorer, 2
182
+ paragraphs" (`section-summary.ts`: `collectHiddenContent`,
183
+ `hiddenContentParts`; `countHiddenContent` is gone). A `<Fold>` whose bar
184
+ names its floats keeps counting them;
185
+ - every chain icon copies a named link: the confirmation says "Link copied
186
+ · Figure 2.1.1 · Projectile trajectory explorer" (a section's, check's
187
+ or example set's title, a memo's short caption), and where the browser
188
+ has `ClipboardItem`, a `text/html` link reading "Figure 2.1.1,
189
+ Projectile trajectory explorer" or "§ 1.2.1 Velocity over time" goes on
190
+ the clipboard beside the URL. The confirmation bubble wraps inside a cap
191
+ instead of running past the column.
192
+ - **A figure's preview card copies its picture as drawn.** A hydrated
193
+ island in the picture (a lab's plot) becomes plain markup in the copy, so
194
+ it never hydrates again inside the card, its controls are disabled, and
195
+ every id in the copy is prefixed, as a diagram's already were. A lab whose
196
+ plot is not drawn yet — or is on another page, drawn only in the browser —
197
+ leaves the card to its caption and number. A lab no longer needs
198
+ `previewable={false}`.
199
+ - **A diagram's widen button ↔ is a boxed button at the end of its caption
200
+ line**, the `.float-tool` a table's caption has, shown while the graph is
201
+ wider than the column; `.diagram-toolbar` and `.diagram-expand-button`
202
+ are gone. `FloatCaption.astro` wraps its default slot in the caption's
203
+ `.float-tools` row, rendered only when the slot is filled, and names the
204
+ float in `data-float-name`; a caption line keeps room for its button row
205
+ only while a button shows.
206
+ - **The side panels sit above explanation cards.** The navigation and the
207
+ "On this page" rail, open or peeking while collapsed, now cover a card
208
+ that reaches them, instead of the card covering them; a reader who needs
209
+ a panel's room collapses it. The site header stays above both. While a
210
+ table or diagram is expanded, the panels drop back under it as before.
211
+ - **A slimmer site header.** It is 3rem tall at every width (Starlight's
212
+ default is 3.5rem, and 4rem from 50rem up), with the site title at
213
+ `--sl-text-xl`, the logo sized to the height, and a 2rem search box.
214
+ Everything placed under the header follows `--sl-nav-height`; a course
215
+ that sizes its own header content should check it still fits.
216
+ - **Every explanation card now works the same way.** Notation symbols and
217
+ `[[key]]` terms, citations, abbreviations, and references to equations,
218
+ tables, figures, diagrams and sections used to run on four engines with
219
+ four sets of rules; one card manager (`components/card-manager.ts`) now
220
+ runs them all (docs/plans/hover-card-consistency.md, Part 1). For a reader:
221
+ - Hovering or keyboard-focusing a trigger opens its card, and the card
222
+ stays open while the pointer travels to it, even from a symbol at the
223
+ top of a tall equation. A symbol crossed on the way no longer takes
224
+ over; resting on it for a moment does.
225
+ - A click (or a tap) on any trigger pins its card and a second click
226
+ closes it. It never navigates: that is the card's own link, now worded
227
+ "Go to …" ("Go to the full definition", "Go to the reference list",
228
+ "Go to equation (1.4)"). A reference whose target is already fully on
229
+ screen still highlights it in place and navigates, and a standalone
230
+ export's cross-page references stay plain links. Diagram references pin
231
+ like the others.
232
+ - Clicks inside a card never close it, so text in a card can be selected
233
+ and copied; a click elsewhere never closes a pinned card. Every card
234
+ has a Pin toggle and a × in its header, and a pinned card shows an
235
+ accent border. Abbreviation cards can be pinned; the citation card's own
236
+ Pin button is gone.
237
+ - Every layer shows several cards at once, one per item; opening a card
238
+ closes only the hovered cards outside its chain, never a pinned one. A
239
+ symbol inside a card opens its own card as a child of it, across layers.
240
+ - Cards are placed clear of each other and of the equation their symbol
241
+ sits in, never at the top of the window, and scroll with the page.
242
+ - Escape closes one card per press, the newest first, across all layers.
243
+ - A `[[key]]` term no longer shows the browser's tooltip on top of its
244
+ card; the `title` stays in the HTML for readers without JavaScript.
245
+ - The collapsed-content peeks (a memo marker's preview, a collapsed
246
+ section's) keep their behaviour but close on the cards' terms: after a
247
+ short grace, not the moment the pointer steps off them.
248
+ - `HoverPanel.astro` keeps its props but renders a `<template>` that each
249
+ layer clones per card, with a header row (a `heading` slot, the Pin toggle
250
+ and ×) above its default slot.
251
+ - **A card's "Go to …" link says where it lands.** It ends in ↑ or ↓ for a
252
+ place on the same page (above or below the middle of the view), → for
253
+ another page of the course, and ↗ only for a citation's source, the one
254
+ link that leaves the site; every one used to end in ↗. A float reference
255
+ to another lesson names it: "Go to equation (1.1.6) in Position and
256
+ Coordinate Systems →" (`components/card-jump.ts`, `[data-jump-arrow]`).
257
+ - **Smaller headings, one even step apart.** From 50em up h1–h5 are 36, 28,
258
+ 22, 18 and 16 px (Starlight's 42, 35, 29, 24, 18); below it 30, 24, 20,
259
+ 17 and 16 (35, 29, 24, 20, 18). An h2 fitted some 25 characters of a
260
+ lesson column. The space above a section (`1.5em`), a folded bar and the
261
+ section chevron follow the same `--sl-text-h*` tokens, set in
262
+ `global.css`.
263
+ - **Knowledge-check and worked-example headings.** The kind's icon leads
264
+ the title, and the kind with the number ("KNOWLEDGE CHECK 1.2.1",
265
+ "EXAMPLE 1.2.1") is a small label in the card's top-right corner, so the
266
+ title keeps the whole line and the number no longer reads as a section
267
+ number. The heading's accessible name is unchanged. Markup:
268
+ `.block-badge` holds the word and `.block-heading__number`, and the icon
269
+ is `.block-heading__icon`, before `.block-heading__title` (it was
270
+ `.block-badge__icon`, inside the badge); course CSS on the badge's inside
271
+ moves with it.
272
+ - **A `[card=meta]` section is unnumbered**, with every heading inside it,
273
+ like References: logistics — what a chapter builds on, how to read it —
274
+ is outside the outline, and the sections after it keep their numbers
275
+ (`sectionNumbers`' `unnumbered` in `reference/sections.mjs`, read with
276
+ `headingCardTier` from `reference/rehype-heading-anchors.mjs`, in both the
277
+ manifest's walk and the page's).
278
+ - **A table's buttons sit together at the top right of its caption line.**
279
+ A widen button ↔, shown when the table is wider than the column, does
280
+ what the column at the table's right edge does (that column stays, and
281
+ so do the collapse buttons on both sides of a widened table); the fold
282
+ button next to it is a chevron, ⌃ / ⌄, where it was ▴ / ▾. Both use the
283
+ new boxed `.float-tool` style in a `.float-tools` row (`global.css`); the
284
+ fold toggle keeps `.keyed-table__pagination-toggle` but is no longer a
285
+ `.table-overflow-expand-btn`.
286
+ - **Less empty space around a lesson.** Accent and meta cards pad 1rem at
287
+ the top and bottom and up to 1.25rem at the sides (they were up to 1.5rem
288
+ all round); the first block under a lesson's title adds no margin to the
289
+ content panel's padding; Previous / Next sit side by side down to a 26rem
290
+ column, with 1.125rem titles, where they stacked below 36rem; and the
291
+ 3rem Starlight keeps above its "last updated" and edit links is gone when
292
+ a course shows neither.
293
+ - **The notation panel's help text is inside the panel**, under its title,
294
+ shown when the panel opens, instead of three lines under the closed one.
295
+ - **A knowledge check's score** is empty until the first answer, then
296
+ reads "2 of 3 correct" under the questions, so it never pushes down the
297
+ question just answered. The "3 questions" line above an unanswered check
298
+ is gone; the list shows the questions.
299
+
300
+ ### Removed
301
+
302
+ - `components/hover-panel.ts` (`createHoverPanel`, `claimActiveSurface`,
303
+ `releaseActiveSurface`) and `components/popup-stack.ts`, internal engines
304
+ outside the public surface in `docs/VERSIONING.md`. A layer registers a
305
+ `CardSource` with `registerCardSource` instead, and an overlay that must
306
+ clear the hovered cards calls `closeTransientCards()`.
307
+
308
+ ### Fixed
309
+
310
+ - A folded section, memo or fold that the browser opens by itself — to show
311
+ a find-in-page match or a linked fragment inside it — now opens for real:
312
+ its bar reads open, "Hidden here" goes, and its body shows even after the
313
+ reader folded it (the fold animation's `max-height: 0` stayed behind). The
314
+ reader's choice is saved as open, as a click would save it.
315
+ - A heading's chain icon, including a knowledge check's, a worked
316
+ example's and a memo's, now says "Copy link" on hover, as a float's
317
+ already did: a click on it puts the link on the clipboard. The title
318
+ comes from `HeadingAnchorEnhancer.astro` and only where a click copies:
319
+ on a page without it, or in a browser without the Clipboard API, the icon
320
+ still only navigates and has no title.
321
+ - On a phone, or any touch screen, a knowledge check's and a worked
322
+ example's badge icon was drawn over the first letter of its word
323
+ ("⊞AMPLE"): the heading's hanging indent reached into the badge.
324
+ - A long table's "… N more rows …" label is centred on the part of the table
325
+ on screen. It was centred on the whole table, columns scrolled out of view
326
+ included, so on a table wider than the column it sat far to the right.
327
+ - A worked example's closing display equation kept its bottom margin
328
+ inside the card: `CompactExample.astro`'s scoped `:last-child` rule never
329
+ matched slotted content.
330
+ - A reference to an equation, table or figure inside a closed memo or a
331
+ folded section now opens its preview card. The fold clips its content but
332
+ keeps its box on screen, so the reference counted it as visible: it stayed
333
+ a plain link with nothing to show, and a click jumped into the closed memo.
334
+ While the card is open, the fold's bar is marked (`.float-ref-fold-hint`),
335
+ so the reader sees where the target lives.
336
+ - A figure's preview card shows the figure: an SVG sized by its `viewBox`
337
+ alone had no width of its own in the card and collapsed to nothing, leaving
338
+ only the caption. The card's content now takes the card's width, and a
339
+ figure at least 22rem of it.
340
+
341
+ ## [0.10.0] - 2026-09-27
342
+
343
+ ### Breaking
344
+
345
+ - **Float numbers are chapter-scoped: `Figure 2.1` is now `Figure 2.2.1`.**
346
+ A display equation, `<Table>`, `<Figure>`, `<Diagram>` and knowledge check
347
+ is numbered `‹part›.‹chapter›.‹n›` and every kind restarts in each chapter,
348
+ the LaTeX book convention — `2.1` would read as "part 2, chapter 1" once
349
+ sections carry `part.chapter.section` numbers. `[[…]]` references need no
350
+ edit: they print from the manifest. What changes for a course is every
351
+ number a reader sees, the positional anchor of an *unkeyed* float
352
+ (`#eq-2-4` → `#eq-2-2-4`), and any e2e test or prose that quotes a number.
353
+ `numbering.floats: "part"` in `course.settings.json` keeps the old
354
+ `‹part›.‹n›` scheme.
355
+ - **The manifest schema is now v20**: `numbering` carries the course's
356
+ resolved numbering settings; `lessons[].headings` lists every top-level
357
+ heading with its anchor (`slug`), `text`, `level` and, when numbered, its
358
+ `number`; `lessons[].numbering` gains `prefix`, and under the default chapter
359
+ scope its per-kind counts, which used to carry a part's running total, are
360
+ all 0. `remarkNotation` reads all of it off the manifest (`sections` and
361
+ `sectionNumbering` join its options), gives each heading its anchor and
362
+ `data-sec-number`, and fails the build when a page's top-level headings are
363
+ not the ones the manifest numbered.
364
+ - **Six overlapping rules are folded into four; their IDs are retired.** Rules
365
+ N1 (the normal distribution is `\mathcal{N}`), N6 (probability is
366
+ `\mathbb{P}`) and N7 (the differential is `\mathrm{d}`) are now lines in
367
+ **rule N2**, which becomes "a reserved spelling means only what it is
368
+ reserved for, and every other styled letter is a symbol of its own" and
369
+ carries the table as a course setting, `rules.N2.reserved` (default: the four
370
+ number systems, `\mathbb{P}`, `\mathcal{N}(\mu, \sigma^2)`, `\mathrm{d}`)
371
+ — so a course edits its conventions in `course.settings.json` instead of
372
+ overriding a rule page, and an agent reads the effective table off
373
+ `build/rules-in-effect.md`. Rule N3 (write a multi-letter label as
374
+ `\mathrm{…}`) is folded into **rule N10**, which becomes "a multi-letter
375
+ roman label is written `\mathrm{…}`, and it is universal or it is explained"
376
+ and is now `partial`: the format half never had a check of its own — rule
377
+ N4's gate reports a bare `PV` as unresolved one-letter symbols. Rule N16
378
+ (alternate delimiter shapes for stylistic nesting) is folded into **rule
379
+ N17**, which becomes "a delimiter's shape carries its meaning and never
380
+ changes to show nesting: a nest is sized by the engine", also `partial`. The
381
+ preference for alternating shapes is **reversed**, not just moved: this
382
+ engine sizes a nest automatically, and a `[` inside a grouping nest reads as
383
+ an interval, an expectation or a matrix first. N16's "in AI-generated
384
+ content" scope is gone with it — a rule holds for whoever typed the page.
385
+ Rule C4 (a release moves `[Unreleased]` and bumps `about.version` together)
386
+ is folded into **rule C3**, whose `knowledge-changelog-missing` was already
387
+ the check that fired on a half-finished release; C3 is now `partial`. Each
388
+ retired ID keeps its number so an older commit message still resolves, but
389
+ nothing else: `RuleEntry.retired` is **gone**, and a retired rule leaves the
390
+ catalog for a new compact list (`RETIRED`, read by `retiredRules()` /
391
+ `retiredRule(id)`) holding only `{ id, title, date, into?, why }`. It has no
392
+ rule page, no row in the index, no entry in the settings schema and no line on
393
+ `build/rules-in-effect.md`, so an author or an agent that is drafting never
394
+ reads it; the one place it is written out is the generated table on the new
395
+ `docs/authoring/retired.md`, which is where a migration guide links.
396
+ `gen-rules.mjs` writes and `--check`s that table alongside the index. A course
397
+ with `rules.N1` / `rules.N3` / `rules.N6` / `rules.N7` / `rules.N16` /
398
+ `rules.C4` in its settings still gets a `config-retired-rule` warning — now
399
+ naming the successor and the date — and should move the block to the surviving
400
+ rule (only N2 takes settings).
401
+ - **Rule N9 catches a unit written as a word.** "a time of 4 seconds", "2
402
+ meters per second squared", "30 degrees above the horizontal" and
403
+ "$x = 3$ meters" now fail `pnpm validate:content` with the new code
404
+ `spelled-unit` (owned by rule N9), in lesson bodies and assessment
405
+ `prompt` / `explanation`; the message gives the math to write instead
406
+ (`$4\ \text{s}$`, `$2\ \text{m/s}^{2}$`, `$30^{\circ}$`,
407
+ `$x = 3\ \text{m}$`). The unit words live in `src/reference/unit-words.ts`
408
+ (SI-style units and their common prefixes; not calendar time or
409
+ currency). Flagged: a unit word after a number (digits or words, plain or
410
+ typeset), before or after `per`, or with `square` / `cubic` / `squared` /
411
+ `cubed`. A unit named on its own ("measured in meters"), "degrees of
412
+ freedom" and `second-order` pass. Rule N9's old carve-out for a
413
+ spelled-out unit in flowing prose is gone. A course with a violation
414
+ rewrites it, waives a defining sentence with
415
+ `{/* explico-allow N9: reason */}`, or lists unit words it keeps as words
416
+ in the new `rules.N9.allow` setting. `proseOnly` takes
417
+ `{ keepMath: true }`; `gateProseNotation` / `gateProseNotationInAssessments`
418
+ take `{ allowSpelledUnits }`.
419
+ - **Rule N12 is build-enforced: math never ends in the sentence's own
420
+ punctuation.** `gate-math-punctuation.ts` (`gateMathPunctuation`,
421
+ `gateMathPunctuationInAssessments`) reads every `$…$` / `$$…$$` span in a
422
+ lesson body and in assessment `prompt` / `explanation`. It fails
423
+ `pnpm validate:content` with `math-trailing-punctuation` when a `.`, `,`,
424
+ `;` or `:` is the last visible thing in the span, or in a row before a
425
+ `\\`. It looks through spacing, `\label{…}` / `\tag{…}`, row breaks,
426
+ `\end{…}` and `\right.`, and into a closing `\text{…}` (`5\ \text{m,}`). A
427
+ trailing decimal point (`5.`) and a typed `...` fail too; rule N12 no
428
+ longer lists a decimal point as an exception. An upright dotted
429
+ abbreviation that is notation passes: the shipped `a.e.`, `a.s.`, `c.c.`,
430
+ `const.`, `h.c.`, `i.i.d.`, edited with the new `rules.N12.allow` /
431
+ `rules.N12.deny` settings. A course with a violation fixes the math, waives
432
+ the place with `{/* explico-allow N12: reason */}`, or turns rule N12 off
433
+ in `rules`. See `docs/authoring/notation.md#n12`.
434
+ - **A font-styled letter is its own symbol (rule N4).** `\mathbf{x}`,
435
+ `\boldsymbol{\beta}`, `\bm{x}`, `\mathcal{N}`, `\mathscr{L}`,
436
+ `\mathfrak{g}`, `\mathbb{E}`, `\mathsf{x}` and `\mathtt{x}` no longer
437
+ borrow the notation card of the plain letter on the page, and a styled
438
+ base-library letter (`\mathbf{e}`, `\boldsymbol{\pi}`) no longer passes as
439
+ Euler's number or π. Each needs its own entry (`latex: \mathbf{x}`) or an
440
+ `\explain{key}{\mathbf{x}}` wrap, or rule N4's completeness gate fails with
441
+ `unresolved-glyph` — in lesson bodies, notation formulas, assessments and
442
+ labs alike; the message says the styled letter is a different symbol.
443
+ `\mathit` / `\mathnormal` still share the plain letter's card, and
444
+ `\mathbb{N,Z,R,C}` stay reserved. The wrap for `\boldsymbol`, `\bm`,
445
+ `\bold`, `\Bbb`, `\frak`, `\mathsf`, `\mathtt` and `\mathnormal` now
446
+ lands around the command (`\explain{k}{\boldsymbol{\beta}}`), not inside
447
+ it.
448
+
449
+ - **Every float is named by one caption pair: a long `caption` and an
450
+ optional `short` caption** (LaTeX's `\caption[short]{long}`). The other
451
+ name props are removed outright, with no alias period; each now fails the
452
+ build with the edit to make:
453
+ - `<Figure title="…">` → `short="…"` (the title bar is gone; the long
454
+ `caption` stays required).
455
+ - `<Diagram label="…">` → `caption="…"`, now shown below the graph; add
456
+ `short` for a long caption. A `<Diagram title>` — never a prop, but the
457
+ rule S4 example used it — fails the same way.
458
+ - `<Memo title="…" description="…">` → `caption="…"` (required: the memo's
459
+ bar and the head of its preview), plus `short="…"` (optional: the
460
+ marker's tooltip). A memo with no title is fine; one with no caption
461
+ fails.
462
+ - `<CompactExamples summary="…">` → `short="…"`.
463
+
464
+ `pnpm validate:content` reports a `<Figure title>` as `fig-removed-prop`
465
+ and a `<Diagram label>` / `title` as `dia-removed-prop` (errors, owned by
466
+ the keyed-floats invariant; `tab-removed-prop` is registered for the same
467
+ shared check); `remarkMemo` fails a `<Memo title>` / `description`; each
468
+ component throws on its removed prop when it renders.
469
+ - **A figure and a diagram put their number in a caption line below the
470
+ body, and compacting keeps that line.** The line reads `Figure 2.2.1`
471
+ (a link to the figure), a chain icon, and the long caption; the compaction
472
+ toggle sits beside it (in the gutter on a wide screen, at the start of the
473
+ line on a narrow or touch one), and a compacted float hides only its body —
474
+ its number, anchor and caption stay (a collapsed figure used to hide its
475
+ number). A diagram's number leaves its title bar for the same line, and its
476
+ widen control moves to the end of it; the curriculum map's competency graph
477
+ gets the same caption line. The float's `<details>` keeps a
478
+ native `<summary>` that shows only under `(scripting: none)`. Selectors that
479
+ moved: `.keyed-figure__titlebar`, `.keyed-figure__summary`,
480
+ `.keyed-figure__title-anchor`, `.diagram__titlebar`, `.diagram__summary`
481
+ and `.diagram__title-anchor` are gone; `.diagram__number` is now a link;
482
+ the caption line is `figcaption.float-caption` with `.float-caption__toggle`,
483
+ `.float-caption__number`, `.float-caption__anchor` and `.float-caption__text`
484
+ (`src/components/figure/FloatCaption.astro`,
485
+ `src/components/disclosure/float-compaction.ts`).
486
+ - **A memo's marker is a superscript letter**, `a`, `b`, … per page, in
487
+ italics — the footnote convention — where it was a bracketed `[1]` that read
488
+ as a citation. The same letter is the memo's permalink on its bar;
489
+ `data-memo-number` and the `number` prop `remarkMemo` injects carry the
490
+ letter (`memoLetter(n)` is exported from `remark-memo.mjs`).
491
+ - **The manifest schema is now v21**: `lessons[].equations` / `.tables` /
492
+ `.figures` / `.diagrams` entries, and the `labels` of `equations` /
493
+ `tables` / `figures` / `diagrams`, carry the float's `short` caption and,
494
+ for a `<Table>` / `<Figure>` / `<Diagram>`, its long `caption`, each when
495
+ the source states it as a literal (`ManifestLessonFloat`).
496
+ - **Rule S1 measures the short caption, not a title.** It caps every
497
+ heading, the `short` prop of a `Figure`, `Diagram`, `Table`, `Memo` and
498
+ `CompactExamples`, a display equation's `\caption{…}`, and a `ShortNote`'s
499
+ `title`; a long `caption` is never measured. A course whose `Figure title`
500
+ passed S1 moves it to `short`, where it is measured the same way.
501
+
502
+ - **Every collapse default is one `disclosure` block: `CourseConfig.disclosure`
503
+ replaces `CourseConfig.disclosureDefaults`.** It takes the course settings'
504
+ `disclosure` block in its own shape — `sections` / `subsections` /
505
+ `knowledgeChecks` as `{ collapsible, defaultOpen }`, `notation` /
506
+ `references` / `memos` / `figures` / `diagrams` / `workedExamples` as
507
+ `{ defaultOpen }`, `tables` as `{ truncateAfter }` — and
508
+ `normalizeDisclosure` (with `DISCLOSURE_DEFAULTS`) replaces
509
+ `normalizeDisclosureDefaults`. A course passes the block straight through
510
+ (`course.config.ts` no longer splits it off) and reads it back in
511
+ `astro.config.mjs` for `rehypeHeadingAnchors` (`defaultCollapsible:
512
+ courseConfig.disclosure`) and `remarkCitation` (`defaultOpen`); the old
513
+ `disclosureDefaults: { notation, references }` mapping goes. **The manifest
514
+ schema is now v22**: `manifest.disclosure` (`NormalizedDisclosure`, every
515
+ default applied) replaces `manifest.disclosureDefaults`.
516
+ - **A `<Memo>` is a `######`-rank section.** Its bar is an `<h6>` (the
517
+ `#memo-…` anchor, `role="none"`, carrying the caption and, as generated
518
+ content, the letter) in a heading wrapper `CollapsibleSections` folds like
519
+ any section — gutter chevron, hover peek, "Hidden here: …" line, animation,
520
+ persistence under `section:memo-…`, a chain icon that copies "Link copied ·
521
+ Memo a". `.memo-block` is now the frame (it was the `<details>`); the
522
+ `<details>`, `.memo-block__summary`, the letter link `[data-memo-self-link]`
523
+ and the separate chain link `[data-memo-anchor-link]` are gone. The marker
524
+ in the prose still opens it, goes there and folds it again. It is not a
525
+ heading of the lesson: not in the outline, "On this page" or the heading
526
+ rules.
527
+
528
+ - **A knowledge check and a set of worked examples draw their own heading;
529
+ the author writes only the topic.** Both render the new `BlockHeading.astro`
530
+ `<h2>`: the kind's badge (an icon and "Knowledge check" or "Example"), the
531
+ number, then the author's title — the assessment file's `title`, the set's
532
+ `short`. A check's heading was `Knowledge check 1.2.1 — ‹title›` in one
533
+ span (`.assessment-heading-number` is gone), so an assessment title that
534
+ repeats the kind ("Velocity check") now reads twice: write the topic alone.
535
+ **`<CompactExamples>` requires `id` and `short`**: `short` no longer
536
+ defaults to "Worked examples", and a set without either fails
537
+ `pnpm validate:content` (`ex-missing-prop`) and its own render, as
538
+ `summary` does (`ex-removed-prop`, now reported at validation too).
539
+ **A set is a `##` section**, folded by `CollapsibleSections` like a memo —
540
+ chevron, peek, "Hidden here: 2 worked examples", print-as-shown — instead of
541
+ its own `<details>`: `details.compact-examples`,
542
+ `.compact-examples__summary` and `.compact-examples__title` are gone
543
+ (`<compact-examples class="compact-examples">` is the accent card, the
544
+ heading is `#ex-‹id›`), and `hint` has no default ("Open and choose an
545
+ example." described the old disclosure; the folded bar now says what is
546
+ inside). Its heading is part of the outline:
547
+ it ends the section before it, and rule S6 and rule S7 read it as a `##`
548
+ (rule S7 already counted it as a heading), so a `###` → set → `####` is now
549
+ a `heading-skip`. `ex-` joins the reserved reference prefixes: a notation
550
+ key written `[[ex-…]]` is read as a set reference.
551
+ - **The manifest schema is now v23**: `examples` (the `<CompactExamples>`
552
+ number table, same shape as `figures`), `lessons[].examples`
553
+ (`ManifestLessonFloat`, `short` the title), `lessons[].numbering.examples`,
554
+ `diagnostics.examples`; `lessons[].knowledgeChecks` entries gain `title`;
555
+ `lessons[].titledBlocks` (`ManifestTitledBlock`: each check and set with its
556
+ heading's `anchor`, `title` and the top-level heading it follows, `after`);
557
+ and `blockLabels`. A course's own `compile-manifest.ts` that lists the
558
+ diagnostics groups must add `examples`, or an `ex-…` error does not fail
559
+ `validate:content` (the page build still fails on it).
560
+
561
+ ### Added
562
+
563
+ - **Outline numbering.** A lesson's top-level headings are numbered
564
+ `‹part›.‹chapter›.‹section›[.‹subsection›…]` — `1.1.2`, `1.1.2.1` — down to
565
+ `####`, printed before the title on a wide screen and in print, and on a
566
+ collapsed section's bar at every width; an open section on a narrow screen
567
+ shows none. The number is CSS generated content from `data-sec-number`, so
568
+ the heading's text, its anchor and the "On this page" entry are unchanged.
569
+ Configured by the new `numbering` block in `course.settings.json`
570
+ (`CourseConfig.numbering`): `sections.show` (default `true`),
571
+ `sections.depth` (`2`–`6`, default `4`), `floats` (`"chapter"`, the
572
+ default, or `"part"`) and `aliases` (default `true`); a course's settings
573
+ schema needs the block to offer it in the form editor
574
+ (`playground/content/course.settings.schema.json` has it). A heading
575
+ inside a component (`<Memo>`, `<CompactExample>`) is not part of the
576
+ outline. The manifest decides each top-level heading's anchor — the same
577
+ slug Astro gives a plain-text heading — and the page takes it from there,
578
+ so a heading with inline math now gets a slug from its source (`$R$` → `r`)
579
+ instead of from KaTeX's output.
580
+ - **`[[sec-slug]]` cites a section**, and `[[lesson/slug#sec-slug]]` one on
581
+ another page: it prints `§ 1.1.2` (the title in quotes when the heading has
582
+ no number, or the course hides them) and gets the float-reference hover card,
583
+ with the section's title and first paragraph. `sec-` joins the reserved
584
+ reference prefixes. A reference to no top-level heading is a
585
+ `sec-ref-resolves` error (owned by the `keyed-floats` invariant, reported in
586
+ `diagnostics.math`), and the build fails on it too. `content context
587
+ --json` lists the lesson's `sections` — anchor, text, level and number — so
588
+ a drafter can cite one without deriving its slug.
589
+ - **Number aliases.** With `numbering.aliases` on, a fragment a reader types in
590
+ number form — `#sec-1.1.2`, `#fig-2.2.1`, `#eq-1.1.4`, `#tab-…`, `#dia-…`,
591
+ `#check-…` — lands on the element that shows that number on the page, and
592
+ the address bar switches to its named anchor (`KeyedFloatEnhancer` takes an
593
+ `aliases` prop; `LessonFooter` passes the manifest's). Inbound only: the
594
+ site links and copies named anchors alone. A knowledge check's heading
595
+ carries `data-check-number` for this.
596
+ - **A heading's chain icon copies its link.** One click puts the page URL with
597
+ the heading's named anchor on the clipboard (the page's canonical URL when it
598
+ declares one) and shows "Link copied · § 1.1.2" under the title, announced
599
+ through a polite live region; the click still navigates and highlights, and
600
+ a refused clipboard leaves exactly that. On a narrow screen the icon is
601
+ always visible, smaller and muted, since it is where the section number
602
+ surfaces there.
603
+ - **Container tiers.** `explico/styles/surfaces.css` defines the style
604
+ primitives once — `--ex-surface-*` (`none`, `tint-neutral`,
605
+ `tint-accent`), `--ex-edge-*` (a 1px border, a 3px left rule),
606
+ `--ex-depth-*` (`flat`, `raised`) and one `--ex-radius-*` per boxed tier —
607
+ for both themes, and composes them into `.ex-tier-meta` (grey, bordered,
608
+ flat), `.ex-tier-accent` (accent tint, bordered, shadowed) and
609
+ `.ex-tier-aside` (a left rule; `.ex-tier-aside--tinted` adds the accent
610
+ tint). No class is the **plain** tier. `global.css` imports it, so a
611
+ course's `customCss` needs no second entry. Tiers are fixed per kind; there
612
+ is no setting.
613
+ - **`[card]` / `[card=meta]` heading markers.** Appended to a heading like
614
+ `[collapsed]`, they put the whole section — heading and body, down to the
615
+ next heading of the same or a higher rank — in one accent or meta card. A
616
+ heading takes one marker of each kind, in either order
617
+ (`### Explore the range [collapsed] [card]`). `rehype-heading-anchors.mjs`
618
+ strips them (from the text, the anchor and the manifest's slug alike) and
619
+ writes `data-card="accent"|"meta"` on the heading wrapper;
620
+ `CollapsibleSections.astro` draws the card, after collapsing the section
621
+ when it is collapsible too. Inside a card, the collapse chevron sits in the
622
+ card's padding (`--ex-section-gutter`).
623
+ - **Starlight asides render in an explico course: `remarkAsides`**
624
+ (`explico/reference/remark-asides.mjs`). Starlight turns `:::note` /
625
+ `:::tip` / `:::caution` / `:::danger` into asides only under its own
626
+ `src/content/docs/`, so in a course's `content/docs/` they rendered as a
627
+ bare `<div>`. Add `remarkAsides` last in `markdown.remarkPlugins`: it hands
628
+ each one to Starlight's `<Aside>` (imported into the page), so Starlight
629
+ still draws the icon, the translated default title and its check of a named
630
+ `{icon="…"}`. In a `.md` lesson, which has no JSX, the aside is written out
631
+ with no icon. `icon=""` — on a directive or on an `<Aside>` — drops the
632
+ icon, which Starlight itself cannot do.
633
+ - **Rule S8 (guide): prefer `<Memo>` over a Starlight aside** when the
634
+ content needs an anchor, a caption or collapsing. Review-only; on by
635
+ default (`rules.S8`).
636
+ - **Rules S6 and S7: heading discipline**, both errors, both new in
637
+ `src/reference/gate-heading-structure.ts` (`gateHeadingStructure`). Rule S6
638
+ (`heading-skip`): headings descend one level at a time — a lesson body
639
+ starts at `##` (the page title is its one `#`, so a `#` in the body fails),
640
+ and `##` → `####` fails; going back up is free. Rule S7
641
+ (`heading-adjacent`): a heading is followed by body content, never directly
642
+ by another heading, and `<AssessmentSet />` and `<CompactExamples>` count as
643
+ headings since each opens with a title of its own — so `## Try it` straight
644
+ above a worked-example set fails. Both read a lesson's outline, the same
645
+ top-level headings the section numbers count (`scanOutlineBlocks`, new in
646
+ `explico/reference/sections.mjs`); a heading inside a component, and a
647
+ memo's bar, are not in it. Both are waivable at one place
648
+ (`{/* explico-allow S7: reason */}` above the heading) and switchable
649
+ (`rules.S6.enabled`, `rules.S7.enabled`). `docs/authoring/structure.md`
650
+ gains "Structure from the bottom up": run-in labels and description lists,
651
+ not deeper headings, for parallel items near the bottom of the outline.
652
+ - **Kind badges** (docs/plans/titles-and-containers.md Q11). A knowledge
653
+ check's and a worked-example set's heading opens with a badge — the kind's
654
+ icon and word, small caps on the accent tint — then the number, then the
655
+ author's topic, all one line of text that wraps word by word. The new
656
+ `blockLabels` setting (`CourseConfig.blockLabels`, `"icon-word"` by default,
657
+ or `"icon"` for the icon alone with the word kept for a screen reader)
658
+ chooses the form; `normalizeBlockLabels` and `BLOCK_LABELS_DEFAULT` are
659
+ exported from `explico/compiler/course-config`.
660
+ - **Worked-example sets are numbered and citable.** A `<CompactExamples
661
+ id="…">` is `Example ‹part›.‹chapter›.‹n›`, counted on its own like every
662
+ float kind (`numbering.floats` applies), anchored `#ex-‹id›`. `[[ex-id]]` and
663
+ `[[lesson/slug#ex-id]]` print `Example 1.1.1` and get the reference hover
664
+ card — the set's title and its examples' names. `remarkNotation` numbers
665
+ the set and passes `number` / `anchorId` to the component, and fails the
666
+ page on an unresolved reference; `validateExamples`
667
+ (`explico/reference/examples-validate`) reports `ex-invalid-key`,
668
+ `ex-duplicate-key`, `ex-ref-resolves`, `ex-key-unused` (info),
669
+ `ex-removed-prop` and `ex-missing-prop`, owned by the `keyed-floats`
670
+ invariant. The number-form alias `#ex-1.1.1` resolves, the chain icon says
671
+ "Link copied · Example 1.1.1", `content status` counts example sets and
672
+ `content context --json` lists a lesson's `examples`.
673
+ - **"On this page" lists a knowledge check and a worked-example set** (audit
674
+ §8.7) where each stands, by its title — as a section is listed without its
675
+ number — with the badge's icon before it: `LayoutPageSidebar.astro` rebuilds
676
+ Starlight's list with the manifest's `titledBlocks` in it
677
+ (`tocWithTitledBlocks` in `explico/components/block-heading`), for the
678
+ desktop and the mobile list alike, and marks those entries
679
+ `data-block-kind` for the icon. Starlight's scroll-spy marks them like any
680
+ heading.
681
+ - **`[[check-…]]` previews its check** like every other reference: a hover
682
+ card with the check's title and its number of questions, a highlight when
683
+ the check is already on screen, and a pinned card instead of a jump when it
684
+ is not (the card's link goes there). It is styled like `[[sec-…]]` now.
685
+ - **A course can add rules of its own** — `courseRules` in
686
+ `course.settings.json`, a list the settings form editor adds to and removes
687
+ from. IDs are `X1`…`X99`: the `X` area is permanently reserved for a course,
688
+ so the engine never ships one and no upgrade collides with a classroom's own
689
+ numbering. A rule is `{ id, title, summary?, elements?, enabled?, severity?,
690
+ forbid?, unchecked?, doc? }`. With a `forbid` pattern it is **enforced**: each
691
+ match is a diagnostic carrying the rule's ID as its code
692
+ (`[X1] content/docs/…:42: …`), at `severity` `error` (fails
693
+ `pnpm validate:content`) or `warning`. Patterns are matched against the same
694
+ "is this really prose" surface rule N9 scans — code, math, `[[key]]`,
695
+ `import` lines, JSX tags and MDX comments blanked first — in one of three
696
+ scopes: `paragraph` (the default; a block between blank lines, right-trimmed,
697
+ so `^`/`$` are its ends), `prose` (the whole lesson body) or `heading`.
698
+ Without a pattern it is a **guide**, listed for an author or an agent to
699
+ follow; `enabled: false` parks it in the page's "Off" list, where an agent
700
+ neither applies nor flags it. A course rule is waivable at one place like any
701
+ other located rule (`{/* explico-allow X1: reason */}`), and a stale waiver
702
+ still warns. A bad definition fails the build with the setting's path (a
703
+ regular expression that does not compile or that matches the empty string, an
704
+ ID outside `X`, a duplicate ID, an unknown key, a `severity` with no pattern
705
+ to report). `rules.X1` is refused with a message pointing at `courseRules`.
706
+ Implemented by `explico/rules/course-rules` (`resolveCourseRules`,
707
+ `courseRuleEntry`, `courseRulesJsonSchema`) and
708
+ `explico/reference/gate-course-rules` (`gateCourseRules`); `pnpm build` writes
709
+ `dist/rules/course-rules.schema.json`, which a course points at with
710
+ `"courseRules": { "$ref": "../node_modules/explico/dist/rules/course-rules.schema.json" }`.
711
+ See `docs/authoring/README.md` § Rules of your own.
712
+ - **The rule catalog as data** — `explico/rules/catalog` exports `CATALOG`
713
+ (one entry per authoring rule and per always-on invariant), `ownerOf(code)`,
714
+ `ruleById`, `enforcementOf`. Every diagnostic code the engine emits now has
715
+ exactly one owning entry; `tests/rules/catalog.test.ts` scans `src/` and
716
+ fails on a code no entry claims, or one claimed twice. `content diagnostics`
717
+ / `content warnings` / `content check` carry the owner as `rule` in `--json`
718
+ and prefix the console line with it for a lettered rule
719
+ (`[N9 prose-notation] …`). Six rules name checks that already ran but had
720
+ no rule page: rule S3 (`raw-table-markdown`, `raw-table-html`), rule S4
721
+ (`diagram-unkeyed`, `diagram-nested`), rule S5
722
+ (`convention-single-definition`), rule N19 (`notation-units`,
723
+ `units-vocab`), rule N20 (`notation-source-locator`), rule N21
724
+ (`gloss-name-shape`). Design: `docs/plans/rule-catalog.md`.
725
+ - **Per-place waivers.** `{/* explico-allow <ID>[, <ID>]: <reason> */}` on
726
+ its own line waives the named rules for the next block (to the next blank
727
+ line). A rule whose checks all report a line — rule N9, rule N14, rule S1,
728
+ rule N17 — has the covered findings dropped; a guide rule's waiver is the
729
+ recorded reason and nothing more. `waiver-malformed` / `waiver-invalid`
730
+ (a locked rule, an invariant, a corpus-level rule, an unknown ID) are
731
+ errors; `waiver-unused` (a checked rule's waiver that silenced nothing) is
732
+ a warning; all three are `manifest.diagnostics.waivers`, owned by the new
733
+ `waivers` invariant (`explico/rules/waivers`: `collectWaivers`,
734
+ `applyWaivers`, `isWaivable`). The rules-in-effect page marks each waivable
735
+ rule and shows the syntax. `proseOnly` (rule N9 / rule N14 scans) now blanks
736
+ MDX comments, so a waiver's reason may quote the text it excuses. A course's
737
+ own `scripts/compile-manifest.ts` must print `diagnostics.config` and
738
+ `diagnostics.waivers` and fail on the latter's errors — the playground's
739
+ shows the two loops — and can print math findings through `diagnosticLine`
740
+ to get the `[N9 prose-notation]` rule prefix.
741
+ - **The rules-in-effect page and `content rules`.** `pnpm validate:content`
742
+ writes `build/rules-in-effect.md` (git-ignored, regenerated every run): the
743
+ rules this course has on, grouped by element, each with its enforcement,
744
+ diagnostic codes, settings in effect and a one-line summary, then the
745
+ always-on invariants, the rules turned off, and the locked ones — about a
746
+ hundred lines, so an agent reads it instead of the rule set. `pnpm content
747
+ rules [--element <e>[,<e>]] [--enforcement script|partial|guide|locked]
748
+ [--json]` prints the same page filtered (`explico/rules/in-effect`:
749
+ `renderRulesInEffect`, `rulesInEffect`, `writeRulesInEffect`;
750
+ `explico/compiler/cli`: `contentRules`, `parseRulesFilter`).
751
+ `docs/authoring/agents.md` now sends an agent there first and no longer
752
+ restates the `P` rules.
753
+ - **A rule turned off stops running.** With `rules.<ID>.enabled: false`
754
+ the manifest withholds every diagnostic that rule owns (its codes, per the
755
+ catalog) from every `diagnostics` group, and the whole-rule gates (rule N9,
756
+ rule N14, rule S1, rule S2) are not run at all; `remarkNotation` honours
757
+ rule S3 / rule S4 for its render-time raw-float warnings. Switchable:
758
+ N5, N8, N9, N10, N14, N17, N19, N20, N21, S1–S7, C3 — and every guide
759
+ rule, which then leaves the rules-in-effect page. `manifest.rules`
760
+ publishes `disabled` and each rule's effective settings.
761
+ `tests/rules/one-rule-off.test.ts` holds each switch to exactly its own
762
+ codes. `MathGateDiagnostic.code` is now required.
763
+ - **Per-rule settings: `CourseConfig.rules`.** One block per rule ID, holding
764
+ only what differs from the defaults — `{ "P5": { "enabled": false }, "S1":
765
+ { "maxLength": 72 }, "N14": { "allow": ["GDP"] } }`. `resolveRules`
766
+ (`explico/rules/settings`) validates it against the catalog at the start
767
+ of `compileManifest` and fails the build on an unknown rule or key, a
768
+ value outside its spec, or `enabled: false` on a locked rule (N4, N13,
769
+ N15). Rule S1 gains `maxLength` (default 65), rule S2 `bridgeMinWords` /
770
+ `bridgeMaxWords` / `scopeMinWords` / `scopeMaxWords` (30 / 150), rule V5
771
+ `seeds` (10), rule V6 `minLabels` (3); rules N8, N10, N14 keep their
772
+ `allow` / `deny` lists, now under `rules.N8` / `rules.N10` / `rules.N14`.
773
+ `pnpm build` writes `dist/rules/rules.schema.json` — the `rules` block as
774
+ JSON Schema, properties ordered by element with an `x-element` hint — so a
775
+ course's `course.settings.schema.json` just points at it:
776
+ `"rules": { "$ref": "../node_modules/explico/dist/rules/rules.schema.json" }`.
777
+ The settings form editor (`tools/vscode-settings-editor/`) inlines such a
778
+ file `$ref` and groups the rules by element. `enabled: false` is recorded
779
+ now and takes effect in the next change (the gates); the manifest schema
780
+ is now v18 (`diagnostics.config`).
781
+ - **The rule index is generated.** `scripts/agent/gen-rules.mjs` writes the
782
+ table in `docs/authoring/README.md` from the catalog — each row now shows
783
+ the rule's element(s), its settings defaults, and its enforcement with the
784
+ emitting module and function linked — plus an "Always enforced
785
+ (invariants)" table. `--check` (in `scripts/agent/verify` and CI) fails on
786
+ a stale table, a missing `## <ID>` heading, a bold title that differs from
787
+ the catalog, or an `_Enforcement._` paragraph that does not name every
788
+ module the catalog lists for that rule. Rules N1, N2, N3, N6, N7 are
789
+ now stated as Review-only: their earlier "build-enforced" claim rested on
790
+ the completeness gate (rule N4), which checks that a glyph resolves, not
791
+ which spelling was chosen.
792
+ - **A part can have its own page.** `content/docs/<dir>/index.mdx` is now a
793
+ recognised, first-class page: the URL a part's directory already implies
794
+ (`/<part>/`), which until now was a 404 that nothing linked and nothing
795
+ could link. It is not a chapter — `isLessonSlug` has always excluded
796
+ `<dir>/index`, so the page teaches nothing, numbers nothing, and never
797
+ enters a track or the curriculum graph — and it is opt-in one part at a
798
+ time. See `docs/authoring/structure.md` § Part pages.
799
+ - **`<PartOverview>`** (`explico/components/PartOverview.astro`) renders that
800
+ page's body from the manifest: the part's chapters in reading order with
801
+ their descriptions and float counts, beside a rail carrying the competency
802
+ outcomes the part delivers and the parts it builds on and leads to. The
803
+ authored `chain` prop states the part's argument, one step per line, because
804
+ no manifest field knows why one chapter follows another; `rail={false}`
805
+ keeps the reading list alone.
806
+ - **The sidebar reaches a part page two ways.** A part whose overview page
807
+ exists gains an `Overview` row leading its group — in the served HTML, so it
808
+ is keyboard-operable, crawlable and present without JavaScript, and marked
809
+ `explico-part-overview` so it carries its own icon (two stacked sheets,
810
+ filled against a chapter's outlined page in compact mode, and hollowed out
811
+ again on the part's own page where the row is already an accent tile).
812
+ `LayoutSidebar.astro` then adds a `»` control at the end of the group
813
+ header, so the label keeps the collapse it already owned and neither gesture
814
+ is retrained.
815
+ - **`studyMinutes`** is an optional lesson frontmatter field — `40`, or
816
+ `[30, 40]`. `manifest.lessons[].studyMinutes` carries it in one shape and
817
+ `manifest.parts[].studyMinutes` totals a part whose chapters *all* state
818
+ one, so a half-annotated course reports no misleading sum. It is the
819
+ reader-facing half of the long-dropped `estimatedMinutes`, back now that
820
+ part and course pages have somewhere to add it up.
821
+ - **`<CourseContents>`** (`explico/components/CourseContents.astro`) lists
822
+ every part and chapter for a landing page, each part heading linking its
823
+ overview page, with per-part and whole-course study totals. A hero that
824
+ links only chapter one no longer hides the rest of the course, and the list
825
+ is the manifest's, so it cannot go stale.
826
+ - **Both rails show how far the reader has come.** The navigation rail
827
+ colours the line beside every chapter already passed — reading order is the
828
+ sidebar's own — and marks a part whole when all of its chapters are behind
829
+ the reader; in compact mode the chapter tiles carry it instead. The contents
830
+ rail does the same down one page, following Starlight's scroll-spy. Without
831
+ JavaScript both are simply uncoloured.
832
+
833
+ - **`LayoutPageTitle.astro`** puts the part's name, linked, above every
834
+ chapter title — the one route to a part page that does not depend on the
835
+ sidebar, and the answer to "where am I" for a reader who arrived from a
836
+ search result. Wire it as Starlight's `PageTitle` component override.
837
+ - **`CoursePart.overviewLabel`** renames that sidebar row per part, for a
838
+ course where `Overview` reads wrong.
839
+ - **Rule N18: a multi-step derivation is one aligned chain.** One step per
840
+ line in an `aligned` block, `&` directly before each relation so the signs
841
+ stand in one column, and any justification at the end of its step's line
842
+ after `\qquad`, as `\text{…}`. A justification names the fact it uses;
843
+ another display equation is linked with `[[eq-key]]` in the sentence that
844
+ introduces the chain, because math cannot carry a link and rule N5 forbids
845
+ hand-typed numbers. Review-only (`docs/authoring/notation.md#n18`).
846
+ - **Rule V6: every quantitative axis shows at least three labelled values.**
847
+ One value pins a point and two cannot tell a linear axis from a log one;
848
+ three show the offset, the unit, and the spacing. Covers hand-drawn SVG,
849
+ Observable Plot, islands, and printed pgfplots figures; a categorical axis
850
+ shows its categories, and a figure without axes is exempt. Review-only
851
+ (`docs/authoring/visualizations.md#v6`). The playground's
852
+ polynomial-shape memo figure now labels both axes.
853
+ - **Prose rules P2–P9: controlled scientific English, adapted from
854
+ ASD-STE100.** A lesson's main text puts clarity before variety and
855
+ entertainment. `prose.md` now defines _main text_ (everything a reader must
856
+ read) and _aside_ (a `<ShortNote>` or `<Memo>` body), and adds rule P2 (one
857
+ canonical term per concept, repeated; a notation entry's `label` is that
858
+ term), rule P3 (one meaning per term; no technical term in its everyday
859
+ sense), rule P4 (use the established term and define it where the argument
860
+ first needs it), rule P5 (literal main text: no idioms, metaphors,
861
+ colloquialisms, buzzwords, or entertainment devices; analogies go in an
862
+ aside), rule P6 (industry jargon only in an optional "Industry jargon"
863
+ aside that states its approximate meaning, context of use, and lack of a
864
+ standard definition), rule P7 (one claim per sentence, every logical link
865
+ stated), rule P8 (relation words match the math exactly), and rule P9 (a
866
+ display equation's lead-in sentence says what it gives and where it comes
867
+ from). All review-only (`docs/authoring/prose.md`).
868
+ - **Rule S2: a part page links the earlier parts to this one, then states
869
+ its own scope.** Every `content/docs/<dir>/index.mdx` carries a
870
+ `<PartScope>` paragraph on what the part covers, and every part after the
871
+ first also carries a `<PartBridge>` paragraph before it, on how the earlier
872
+ parts lead in. Both components are new (`explico/components/PartBridge.astro`,
873
+ `explico/components/PartScope.astro`) and render their Markdown child as-is.
874
+ Build-enforced by `gate-part-pages.ts` at `pnpm validate:content`: each slot
875
+ appears once, holds one paragraph, and falls inside a word range. The range
876
+ is `rules.S2` (`bridgeMinWords`, `bridgeMaxWords`, `scopeMinWords`,
877
+ `scopeMaxWords`), 30–150 words for any end left out. A part
878
+ without an overview page is unaffected
879
+ (`docs/authoring/structure-rules.md#s2`).
880
+ - **A closed section says so on every screen.** Under its title it now
881
+ shows what the fold holds — "Hidden here: 2 paragraphs, 1 equation"
882
+ (subsections, paragraphs, equations, figures, tables, diagrams, lists,
883
+ code blocks, memos, worked examples, questions) — which folds away once
884
+ the section opens. On a narrow or touch screen the chevron moves into the
885
+ title line and stays visible in both states (`›` closed, `⌄` open), and a
886
+ closed title adds "(tap to expand)" — "(click to expand)" with a mouse.
887
+ All of it is `aria-hidden` and part of the tap target; the heading alone
888
+ still names the disclosure. Nothing to author: every collapsible section
889
+ gets it.
890
+ - **Rule N22: an accent or mark on a symbol is an operator.** A decoration —
891
+ an accent (`\hat`, `\widehat`, `\bar`, `\tilde`, `\dot`, `\vec`, …),
892
+ `\overline` / `\underline`, or a superscript `'`, `^*`, `^\dagger` — is
893
+ on the course's `rules.N22.allow` list (shipped empty,
894
+ `src/reference/base-accents.mjs`), explained by an operator notation entry
895
+ (`latex: \hat{}`, `{}'`, `{}^{*}`), or part of a whole-form entry
896
+ (`latex: \hat{\beta}`), which wins for that exact form. New Tier-3
897
+ warning `accent-explained` (`consistency.ts` `checkAccentExplained`). The
898
+ resolver reports each decoration in `resolveMathGlyphs(…).decorations`
899
+ (`wholeForm`, `operator`); `collectDecorations`, `decorationOfEntry`,
900
+ `operatorEntryLatex`, `operatorDisplayLatex` and `DECORATION_NAMES` are
901
+ exported from `math-glyphs.mjs`. See `docs/authoring/notation.md#n22`.
902
+ - **Lesson frontmatter `notation.suspend` (rule N22).** A page can declare
903
+ that a decoration is not an operator there
904
+ (`- operator: "'"`, `reason: …`). Every decorated form of it on that page
905
+ then needs its own entry or a whole-form `\explain`, or the build fails
906
+ with `suspended-operator-unresolved` (the gate and the render path both
907
+ check lesson-body math; assessments, labs and notation formulas keep the
908
+ operator reading). New diagnostics `operator-suspended` (info),
909
+ `operator-suspension-reason` (warning, no reason given) and
910
+ `operator-shadowed` (info, a whole form over an operator the page
911
+ explains). An unknown decoration, one suspended twice, or a
912
+ `notation.local` operator entry for a suspended decoration fails the load.
913
+ `resolveMathGlyphs` takes `{ suspended }` as a third argument; an item
914
+ in `unresolved` may carry `suspended` or `font`.
915
+ - **Hovering an accent explained by an operator entry shows the operator's
916
+ card; the letter under it keeps its own.** The operator's `\explain` wraps
917
+ the decorated form around the letter's. A table header built from both
918
+ reads "Estimate of regression coefficient". An operator entry is drawn
919
+ over a placeholder square (`\dot{\square}`) wherever its symbol is shown
920
+ on its own.
921
+ - **The Page notation panel names each suspended decoration** and the reason
922
+ ("On this page, ′ is not an operator. …"): `NotationLayer` takes
923
+ `suspended`, and `LessonFooter` passes the page's `notation.suspend`.
924
+
925
+ - **An equation takes a short caption: `\caption{…}` inside its `$$…$$`**,
926
+ next to its `\label{eq:…}`. It is stripped before KaTeX exactly like the
927
+ label (and so by every math gate), heads the `[[eq-…]]` preview card, and
928
+ names the copy action: the copy button and the click-to-copy tooltip read
929
+ "Copy LaTeX — Range formula". An equation takes no long caption. A
930
+ `\caption{}` in inline math, two in one equation, or an empty one fails
931
+ the page build. `EQ_CAPTION_PATTERN` and `equationCaption(latex)` are
932
+ exported from `equations.mjs`; `stripEquationLabels` now strips
933
+ `\caption{…}` too; the equation's container carries `data-eq-short`.
934
+ - **A `short` caption on `<Table>`, `<Figure>` and `<Diagram>`**, carried as
935
+ `data-tab-short` / `data-fig-short` / `data-dia-short`: the reference
936
+ preview card (`FloatRefLayer.astro`) heads with it, for every kind that has
937
+ one.
938
+ - **A float's chain icon copies its link**, the way a heading's does: one
939
+ click puts the page URL with the float's named anchor on the clipboard and
940
+ says "Link copied · Figure 2.2.1" (`a[data-copy-link]`, handled by
941
+ `HeadingAnchorEnhancer.astro`), then navigates and highlights as before.
942
+ A table's caption line gains the chain icon too, after its number.
943
+ - A notation formula's copy button, and its click-to-copy tooltip, read
944
+ "Copy LaTeX — ‹entry label›" (`data-copy-title` on the formula).
945
+
946
+ - **Collapse defaults for every kind**, in the `disclosure` settings block,
947
+ each at the value it used to have hardcoded: `knowledgeChecks`
948
+ (`{ collapsible: true, defaultOpen: true }`), `memos` (closed), `figures`
949
+ (open), `diagrams` (open), `workedExamples` (closed) and
950
+ `tables.truncateAfter` (6). A `defaultOpen` prop still overrides one
951
+ `<Figure>` / `<Diagram>`, and is new on `<Memo>` and `<CompactExamples>`;
952
+ `rememberState` is new on `<CompactExamples>`.
953
+ - **Worked examples remember the reader's choice**, like every other
954
+ collapsible (`section:ex-‹id›`).
955
+ - `section-parts.ts` markers for a block that is a section of its own:
956
+ `data-section-to-end` on its heading wrapper (its section is the rest of its
957
+ container) and `data-section-contained` on its container (it never ends a
958
+ section around it). `CollapsibleSections` honours
959
+ `data-remember-state="false"` on a heading wrapper.
960
+
961
+ ### Changed
962
+
963
+ - **Every container is restyled into a tier** (reader-visible; no markup a
964
+ course writes changes). No container has a top rule any more, and the shadow
965
+ never grows with heading depth.
966
+ - `<Figure>`: a card with an accent top rule, a tint and a shadow → **plain**
967
+ (no box). A `.lab-shell` loses the same card, inside a figure or not.
968
+ - Knowledge check: a grey card around the questions → **accent**, one card
969
+ around the heading and the questions (the check's heading wrapper carries
970
+ `data-card="accent"`; `.assessment-shell` draws no box of its own).
971
+ - `<CompactExamples>`: a grey card → **accent**.
972
+ - The notation panel, a part page's rail, the notation glossary's entries
973
+ and the curriculum map's cards: grey cards → **meta**. The About popover:
974
+ an accent top rule on the page background → **meta**, keeping its overlay
975
+ shadow.
976
+ - `<Memo>`: a bordered box with a 4px accent left rule → **aside**, tinted:
977
+ the 3px rule and the tint, no box; its `<details>` no longer draws
978
+ Starlight's own 2px rule inside it. An opened `<ShortNote>`: a 2px rule →
979
+ the same 3px rule.
980
+ - Hover cards (a reference preview, a citation, a notation explanation) lose
981
+ their accent top rule and keep their overlay shadow.
982
+ - Starlight asides → **aside**: `note` and `tip` in the course accent,
983
+ `caution` and `danger` keeping an orange and a red rule, tint and title;
984
+ Starlight's icon per type stays. A Starlight `<Card>` → **meta**.
985
+ - References were already plain and are unchanged.
986
+
987
+ - **A table's caption line is the one every float uses**: `Table 2.2.1`, the
988
+ chain icon, then the caption, at the caption size a figure uses (it was
989
+ body size) and with no " — " between number and caption. Minimized, a table
990
+ still keeps its caption line and its header row (decision Q6).
991
+ - **A knowledge check folds by the course's `disclosure.knowledgeChecks`**
992
+ instead of a hardcoded "collapsible, open"; a figure, a diagram, a memo and
993
+ a worked-example set take their first state from their `disclosure` key; the
994
+ notation panel reads `manifest.disclosure.notation`.
995
+ - **A knowledge check's number follows the section numbers' rules**, as a
996
+ worked-example set's does: shown on a wide screen, in print and on a folded
997
+ bar, hidden on a narrow screen, and not at all when
998
+ `numbering.sections.show` is off — then `data-check-number` is not written
999
+ (so no `#check-…` alias resolves) and `[[check-…]]` / `[[ex-…]]` print
1000
+ `Knowledge check “‹title›”` / `Example “‹title›”` instead of a number. A
1001
+ float's number stays in its caption at every width.
1002
+ - **A truncated table shows up to `truncateAfter` rows, half from each end**
1003
+ (3 and 3 under the default 6, as before).
1004
+ - **A lab carries no title or caption of its own**; the `<Figure>` around it
1005
+ names the whole. The engine's `.lab-chart-figure` styles are gone (a lab
1006
+ draws its chart in a plain `.lab-chart`), and the "print as shown" note for
1007
+ a compacted figure or diagram follows its caption line.
1008
+ - **Rule V3 no longer asks an author for accessibility work.** An interactive
1009
+ figure needs no accessible name or description (`ariaLabel` /
1010
+ `ariaDescription` / `ariaHidden` on the plot) and no text alternative (a
1011
+ `<details>` block of assumptions and representative values): the
1012
+ `<Figure>`'s caption is its accessible name, since the figure renders a real
1013
+ `<figure>` and `<figcaption>`. Rule V3 keeps the hydrated island, the
1014
+ `labMathScope` / `renderLabMath` / `\slot{}` seam and labelled controls — it
1015
+ is narrowed, not switched off, since switching it off would drop the seam
1016
+ too. What stays automatic is unchanged: the hidden labels
1017
+ `rehype-table-header-labels.mjs` writes into an all-math table header, the
1018
+ names the engine gives its own buttons and links, and the axe checks on the
1019
+ engine's markup. `docs/authoring/README.md` § Accessibility states the
1020
+ posture: accessibility is the engine's job, and nothing asks a course author
1021
+ to write any. A course's existing `aria*` options may stay.
1022
+
1023
+ - **`build/rules-in-effect.md` and `content rules` list the course's own rules**
1024
+ beside the engine's, under their element, tagged `course`, with the settings
1025
+ file (or the rule's own `doc`) as the link; the count line says how many are
1026
+ the course's, and a disabled one joins the "Off" list. `content rules --json`
1027
+ gains `source: "engine" | "course"` per rule. A **retired** rule is now left
1028
+ off the page and out of `--json` entirely — it has been folded into another
1029
+ and keeps its ID only so old commit messages resolve. A string-array setting
1030
+ prints its items in backticks rather than as JSON, so
1031
+ `rules.N2.reserved`'s LaTeX reads as the author wrote it (`\mathbb{N} — the
1032
+ natural numbers`, not `"\\mathbb{N} …"`).
1033
+ - **The manifest schema is now v19**: `rules.course` carries the course's own
1034
+ rules as validated, in file order, so the rules-in-effect page and the CLI
1035
+ never re-read the settings file. A finding from one is in
1036
+ `diagnostics.math` with the rule's ID as its `code`, and
1037
+ `MathGateDiagnostic.context` gains `'course'` — the one context whose
1038
+ severity the course chooses.
1039
+ - **Every math-gate finding carries a `code`.** Rule N9 findings are
1040
+ `prose-notation`, rule N14 `abbreviation-unexplained`, rule S1
1041
+ `title-length`; an unresolved glyph is `unresolved-glyph`; a resolver
1042
+ failure keeps the resolver's own code (`unparenthesized-function`,
1043
+ `invalid-latex`, …) and any other compile crash is `math-compile-error`.
1044
+ They were code-less before, so the CLI labelled them by context
1045
+ (`math:prose`, `math:structure`); `code` in `content … --json` output is
1046
+ now `math:<code>` for all of them, and `MathGateDiagnostic.code` is
1047
+ required in the type.
1048
+ - **Print follows the screen ("print as shown").** A printed or
1049
+ PDF-saved lesson keeps every disclosure the reader controls — section,
1050
+ figure, diagram, memo, short note, worked-example set, question, the
1051
+ notation list — as the reader left it: open prints in full, closed prints
1052
+ its title and "(content collapsed — full text: `url`)", linking the item's
1053
+ permalink on the deployed site (`canonicalBaseUrl`). Until now print
1054
+ expanded everything. Rows past a long table's fold and an open example
1055
+ set's other tabs still print in full, since the reader never chose to hide
1056
+ them, and the Markdown export still expands everything. A course without
1057
+ `lessonExports.print` gets the same behaviour from the browser's own print.
1058
+ The shared logic is `explico/components/disclosure/print-as-shown.ts`.
1059
+ - **The part caret leads its label instead of trailing the row.** `>` before
1060
+ the name it opens, `v` when open, which is how a disclosure triangle reads
1061
+ and where the eye looks for it; the `»` takes the end of the row.
1062
+ - **Both sidebars answer a pointer with a filled row**, not only a colour
1063
+ change — on a list of twenty chapters, a text-colour shift is easy to lose.
1064
+ The part header answers too: it is a control, and it was the one row in the
1065
+ panel that looked inert. It also carries a standing tint while the open page
1066
+ belongs to it, so the three states stay distinct — a solid accent row for
1067
+ the current chapter, a tint for the part holding it, a neutral fill that
1068
+ comes and goes with the pointer.
1069
+ - **Manifest schema is now v17** (additive): `parts` carries every part with
1070
+ its ordinal, directory link, chapters in reading order, and overview page
1071
+ where one exists — so a page or a breadcrumb reads a part instead of
1072
+ re-deriving one from lesson slugs. `sidebar[].items` is now one mixed list
1073
+ of lesson slugs and `{ label, link, attrs? }` entries (`SidebarItem`)
1074
+ rather than one kind per group; `SidebarGroup` is a single interface
1075
+ instead of a union, which widens what a consumer may receive and narrows
1076
+ nothing. `lessons[].studyMinutes` and `parts[].studyMinutes` come with it.
1077
+ - **A whole-form notation match (`\hat{\beta}`, `b'`, `\theta^{*}`) now
1078
+ wraps the decoration too**, so its hover target includes the accent or
1079
+ prime: `\explain{k}{\hat{\beta}}`, not `\hat{\explain{k}{\beta}}`. The
1080
+ rendered HTML of an existing whole form changes accordingly.
1081
+ - **A marked entry no longer lends its card to the bare letter.** `latex: b'`
1082
+ or `\theta^{*}` used to make a bare `b` or `\theta` elsewhere on the page
1083
+ bind to it (the parameterised-symbol fallback meant for `t_k` → `t`). A
1084
+ mark makes a different quantity (rule N22), so the bare letter now needs
1085
+ its own entry.
1086
+
1087
+ ### Deprecated
1088
+
1089
+ - **`CourseConfig.operators`, `romanLabels`, `abbreviations`** — the same
1090
+ `allow` / `deny` blocks now live at `rules.N8`, `rules.N10`, `rules.N14`
1091
+ (rule N8, rule N10, rule N14). The old keys are folded in with a
1092
+ `config-deprecated-key` warning; setting both the old key and its
1093
+ replacement is an error. They go away in a later MINOR. The unreleased
1094
+ `partPages` block became `rules.S2` (`bridgeMinWords` … `scopeMaxWords`)
1095
+ before it shipped, with no alias.
1096
+
1097
+ ### Fixed
1098
+
1099
+ - **Grey boxes had no background in the dark theme.** The knowledge-check,
1100
+ worked-examples, notation-panel, glossary and curriculum-map cards and the
1101
+ knowledge-check feedback read `--sl-color-gray-7`, which Starlight defines
1102
+ for its light theme only; the tier tokens name a colour for each theme.
1103
+ - **Rule S1 measures a heading as the reader sees it**: its `[collapsed]` /
1104
+ `[card]` markers no longer count toward `rules.S1.maxLength`.
1105
+
1106
+ - **An equation's copy button shows its "Copy LaTeX" tooltip.** The
1107
+ equation-wide copy hint's `mouseout` handler removed every
1108
+ `title="Copy LaTeX"`, the button's own included, so the first time the
1109
+ pointer left the button (or crossed onto its icon) the tooltip was gone
1110
+ for good. It now removes only the hints it added itself, on both a lesson
1111
+ equation and a notation formula.
1112
+ - **A collapsed section is visible on a phone.** Its chevron sat left of the
1113
+ title in the content gutter, which a phone doesn't have, so it rendered
1114
+ off-screen and the section looked like a plain heading. On desktop the
1115
+ same fixed offset put the glyph on the sidebar's border line; it is now
1116
+ centred in the gutter at every width, level with the title's first line.
1117
+ - **The copy-link icon beside a section title shows on touch screens.** It
1118
+ was hidden unconditionally (Starlight hides it only where a hover can
1119
+ reveal it), yet stayed a live tap target — so a tap near the end of a
1120
+ title changed the URL instead of opening the section.
1121
+ - **Labs hydrate under `astro dev` again.** `katex-delimiters.mjs` imported
1122
+ `node:fs` at the top level and rewrote KaTeX's bundle on import; a lab
1123
+ island reaches it through `render-lab-math.ts`, so the dev server evaluated
1124
+ it in the browser, where Vite's `node:fs` stub throws — and the island
1125
+ never hydrated. The Node imports are now lazy and the rewrite installs only
1126
+ outside a browser. Builds and server rendering are unchanged: they still
1127
+ get the N17 sizing and still fail loudly when it can't be installed.
1128
+ - **A build no longer warns about `katex-delimiters.mjs`.** A lab island that
1129
+ imported `slotPlaceholder` from `render-lab-math.ts` pulled the server-only
1130
+ KaTeX chain into the browser graph, so Vite warned that `node:fs` and
1131
+ `node:module` were externalized. The island-facing half (`slotPlaceholder`,
1132
+ `RenderedLabMath`) now also lives in `explico/reference/lab-math-slot`,
1133
+ which imports nothing. `render-lab-math` still re-exports both, so no
1134
+ import breaks, but an island should switch to `lab-math-slot` to drop the
1135
+ warning. The SSR warning about the rewritten bundle's data-URL `import()`
1136
+ is silenced with `/* @vite-ignore */`.
1137
+ - **Printed square roots keep their radical sign.** The print stylesheet's
1138
+ `svg { height: auto }` reached KaTeX's own SVGs and collapsed the `\sqrt`
1139
+ radical (and stretchy arrows and accents) to zero height.
1140
+ - **`<Diagram>` formulas render in Safari and every iOS browser.** WebKit
1141
+ paints HTML inside an SVG `foreignObject` that gets its own render layer —
1142
+ every KaTeX `position: relative` span — without the SVG's internal
1143
+ transforms. So each node formula was drawn at the SVG origin, piled onto
1144
+ the first node, while its box and plain-text label stayed in place. Nodes
1145
+ are now positioned with `x`/`y` on the `<rect>` and `<foreignObject>`
1146
+ instead of a `transform` on their `<g>`. Printing had the same fault
1147
+ through the `viewBox` scale that fits a diagram to the page: WebKit
1148
+ dropped the formulas from a printed or PDF-saved lesson. Where CSS can
1149
+ divide lengths, the print stylesheet now keeps the SVG at its build-time
1150
+ size and scales the HTML canvas with a CSS `transform` instead. Browsers
1151
+ without that CSS support keep the `viewBox` scaling. No course changes.
1152
+ - **Lesson display equations are no longer clipped at the bottom.** A
1153
+ display equation scrolls sideways inside a box that hides vertical
1154
+ overflow, and that box had no padding, so it ended exactly at KaTeX's
1155
+ metric strut. Glyph ink overshoots those metrics by up to about 0.1em, which
1156
+ cut off the bottom of a denominator's `g` or a stretched `\right]`.
1157
+ `.sl-markdown-content .katex-display` now has `0.25em` block padding, so
1158
+ each lesson equation takes about 8px more vertical space. No course changes.
1159
+ - Rules N8 and N10 skipped every labelled display equation: the
1160
+ `\label{eq:…}` made the span fail KaTeX's parse, and the scans treated
1161
+ that as "nothing here". `consistency.ts` now strips the label first, so an
1162
+ unexplained `\operatorname{…}` or `\mathrm{…}` in a labelled equation is
1163
+ reported.
1164
+ - `x^\prime` failed the completeness gate as an unresolved `\prime`. A
1165
+ prime is a mark on the symbol before it, never a symbol.
1166
+ - An accent's glyph box, a full line tall, took every hover on the letter
1167
+ under it: pointing at the `x` of `\hat{\explain{k}{x}}` showed nothing,
1168
+ or the accent's card. The glyph box now lets the pointer through, and a
1169
+ strip across the top of the decorated form is the accent's hit area.
1170
+
1171
+ ## [0.9.0] - 2026-09-19
1172
+
1173
+ ### Added
1174
+
1175
+ - **The reader sets each desktop panel's width.** The navigation sidebar and
1176
+ the "On this page" rail each carry a drag handle on their inner edge
1177
+ (`.layout-edge-resizer`, `role="separator"`, also arrow keys plus
1178
+ `Home`/`End` from the keyboard). The navigation's width is written to
1179
+ Starlight's own `--sl-sidebar-width`, so the header's title column, the
1180
+ article's inline start and the `.main-pane` formula all follow it with no
1181
+ second source of truth; the contents rail, which has no such token, takes
1182
+ `--layout-contents-width`, honoured only while `data-contents-resized` says
1183
+ the reader has set one. Both persist per reader through the existing
1184
+ `PreferenceStore` seam (`explico:layout:navigation-width`,
1185
+ `explico:layout:contents-width`) and are dropped again outside the layout
1186
+ they belong to, so a desktop width never follows the page down to a phone.
1187
+ - **Dragging a panel below three characters of width collapses it** — `3ch`
1188
+ measured in the panel's own font, so a course that changes `--sl-font` gets
1189
+ the threshold its own text implies. The panel's control then reopens it at
1190
+ the width the reader last chose, not at the sliver the collapsing drag
1191
+ happened to pass through.
1192
+ - **A peek now waits out `--layout-peek-delay` (40ms) before it opens.** The
1193
+ rails sit on the two edges of the window, so a pointer on its way to the
1194
+ scrollbar, the tab strip, or across the article crosses one without meaning
1195
+ anything by it, and the panel swung open behind it every time. The CSS keeps
1196
+ `:hover` as the trigger and reads a new `data-navigation-peek-armed` /
1197
+ `data-page-sidebar-peek-armed` attribute as the gate in front of it, so
1198
+ peeking still has one description rather than two. Nothing is lost without
1199
+ JavaScript: a panel is only ever collapsed because that same script
1200
+ collapsed it.
1201
+ - **A panel narrowed past its own labels scrolls sideways** rather than
1202
+ wrapping every chapter title into a column of single words. The contents
1203
+ rail's scrollbar, which Starlight hides outright (`scrollbar-width: none`),
1204
+ comes back as `thin` while that panel is reader-sized.
1205
+
1206
+ ### Changed
1207
+
1208
+ - **The collapse controls are rectangles docked into the corner the header
1209
+ and the panel's separator line make**, reading `<<<` / `>>>` for the
1210
+ direction their panel travels, instead of circled carets floating near that
1211
+ corner. Both panels' controls take the same block start, so the pair reads
1212
+ as one row across the page; both are narrow enough to leave the first
1213
+ chapter's own expand caret alone; and each panel now reserves that corner
1214
+ (`--layout-edge-toggle-reserve`) instead of running its first row under the
1215
+ control. New tokens: `--layout-edge-toggle-width`,
1216
+ `--layout-edge-toggle-height`, `--layout-edge-toggle-gap` (one number for
1217
+ both of the control's edges, `2px`), `--layout-edge-toggle-radius` (applied
1218
+ to the one corner free of them) and `--layout-navigation-width`.
1219
+ - **While a collapsed panel peeks open over the article, its control grows to
1220
+ meet the separator rather than travelling to it.** Hovering the rail is both
1221
+ what opens the peek and what a reader does on the way to clicking the
1222
+ control, so moving the control at that moment takes it out from under the
1223
+ pointer that was coming for it — 260px away, across the panel. Its outer
1224
+ edge, and the glyph inside it, now stay exactly where the pointer found
1225
+ them while its inner end reaches the separator the open panel puts there.
1226
+ The glyph sits in a box the width of the resting control, pinned to that
1227
+ outer edge, so it does not shift at all.
1228
+ - Layout preferences moved from the `credit-playground:layout:*` storage keys
1229
+ to `explico:layout:*`. The old keys are still read once as a fallback, so a
1230
+ returning reader keeps the layout they left.
1231
+
1232
+ ### Fixed
1233
+
1234
+ - **A compact chapter's lesson tooltip is no longer cut off at the navigation
1235
+ panel's edge.** That panel is a scroll container (`overflow-y: auto`, which
1236
+ forces `overflow-x` to match), so an absolutely positioned tooltip could not
1237
+ leave it: any lesson title longer than the panel is wide simply ended there.
1238
+ A showing tooltip is now `position: fixed`, placed from its anchor's own
1239
+ viewport rect and kept on screen at either edge, with its cap raised to
1240
+ `min(22rem, 45vw)`. It is still the link's accessible name in the markup —
1241
+ only where it is painted changed.
1242
+
1243
+ ### Breaking
1244
+
1245
+ - `--layout-edge-toggle-size` is gone, replaced by the separate
1246
+ `--layout-edge-toggle-width` and `--layout-edge-toggle-height`. A course
1247
+ that overrode the old token to resize the control sets the two new ones.
1248
+ - The control's inner markup is `span.layout-edge-glyph` (with `-hide` and
1249
+ `-restore` variants) rather than `svg.layout-edge-icon`. A course that
1250
+ styled `.layout-edge-icon` retargets it.
1251
+
17
1252
  ## [0.8.0] - 2026-09-19
18
1253
 
19
1254
  ### Breaking
@@ -891,7 +2126,10 @@ under **Breaking** below; a PATCH never breaks.
891
2126
  system with its completeness gate, the curriculum-graph validator, the
892
2127
  Astro/Starlight UI, the LMS / session / analytics seams, and the `content` CLI.
893
2128
 
894
- [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.8.0...HEAD
2129
+ [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.11.0...HEAD
2130
+ [0.11.0]: https://github.com/danieltuzes/explico/compare/v0.10.0...v0.11.0
2131
+ [0.10.0]: https://github.com/danieltuzes/explico/compare/v0.9.0...v0.10.0
2132
+ [0.9.0]: https://github.com/danieltuzes/explico/compare/v0.8.0...v0.9.0
895
2133
  [0.8.0]: https://github.com/danieltuzes/explico/compare/v0.7.0...v0.8.0
896
2134
  [0.7.0]: https://github.com/danieltuzes/explico/compare/v0.6.0...v0.7.0
897
2135
  [0.6.0]: https://github.com/danieltuzes/explico/compare/v0.5.0...v0.6.0