explico 0.8.0 → 0.11.1

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