explico 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/CHANGELOG.md +1137 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +4 -3
  4. package/dist/compiler/astro.js +45 -15
  5. package/dist/compiler/cli.d.ts +26 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +9 -1
  8. package/dist/compiler/collections.js +55 -16
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +14 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +87 -6
  51. package/dist/compiler/manifest.js +174 -31
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +2 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +2 -2
  61. package/dist/reference/base-operators.mjs +2 -2
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figures-validate.d.ts +1 -1
  88. package/dist/reference/gate-abbreviations.d.ts +1 -1
  89. package/dist/reference/gate-abbreviations.js +1 -1
  90. package/dist/reference/gate-course-rules.d.ts +4 -3
  91. package/dist/reference/gate-course-rules.js +9 -4
  92. package/dist/reference/gate-heading-length.d.ts +2 -0
  93. package/dist/reference/gate-heading-length.js +7 -5
  94. package/dist/reference/gate-heading-structure.js +26 -0
  95. package/dist/reference/gate-indices.d.ts +35 -0
  96. package/dist/reference/gate-indices.js +299 -0
  97. package/dist/reference/gate-math.d.ts +32 -2
  98. package/dist/reference/gate-math.js +68 -12
  99. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  100. package/dist/reference/gate-mdx-expression.js +29 -0
  101. package/dist/reference/gate-part-pages.d.ts +1 -1
  102. package/dist/reference/gate-prop-text.d.ts +28 -0
  103. package/dist/reference/gate-prop-text.js +106 -0
  104. package/dist/reference/gate-prose-notation.d.ts +3 -1
  105. package/dist/reference/gate-prose-notation.js +4 -2
  106. package/dist/reference/gate-retired-keys.d.ts +35 -0
  107. package/dist/reference/gate-retired-keys.js +58 -0
  108. package/dist/reference/gloss.d.ts +2 -0
  109. package/dist/reference/katex-measure.d.ts +17 -0
  110. package/dist/reference/katex-measure.js +223 -0
  111. package/dist/reference/keyed-float-validate.js +12 -0
  112. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  113. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  114. package/dist/reference/lab-math-scope.d.ts +1 -0
  115. package/dist/reference/lab-math-scope.js +13 -2
  116. package/dist/reference/math-glyphs.d.mts +94 -2
  117. package/dist/reference/math-glyphs.mjs +851 -46
  118. package/dist/reference/mixed-content.d.ts +8 -3
  119. package/dist/reference/mixed-content.js +4 -2
  120. package/dist/reference/page-notation.d.ts +34 -4
  121. package/dist/reference/page-notation.js +54 -2
  122. package/dist/reference/references.d.ts +11 -1
  123. package/dist/reference/references.js +6 -0
  124. package/dist/reference/registry.js +104 -10
  125. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  126. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  127. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  128. package/dist/reference/remark-citation.mjs +0 -0
  129. package/dist/reference/remark-notation.mjs +84 -20
  130. package/dist/reference/render-lab-math.d.ts +2 -0
  131. package/dist/reference/render-lab-math.js +17 -5
  132. package/dist/reference/sections-validate.d.ts +28 -3
  133. package/dist/reference/sections-validate.js +31 -4
  134. package/dist/reference/sections.d.mts +24 -6
  135. package/dist/reference/sections.mjs +92 -17
  136. package/dist/reference/tables-validate.d.ts +1 -1
  137. package/dist/reference/tables-validate.js +24 -1
  138. package/dist/reference/text-measure.d.ts +10 -0
  139. package/dist/reference/text-measure.js +41 -0
  140. package/dist/reference/text-widths.d.ts +10 -0
  141. package/dist/reference/text-widths.js +1238 -0
  142. package/dist/reference/types.d.ts +64 -8
  143. package/dist/rules/catalog.js +191 -30
  144. package/dist/rules/in-effect.d.ts +1 -1
  145. package/dist/rules/rules.schema.json +79 -24
  146. package/dist/rules/settings.d.ts +1 -1
  147. package/dist/rules/settings.js +7 -0
  148. package/dist/session/PreferenceStore.d.ts +18 -1
  149. package/dist/session/PreferenceStore.js +29 -1
  150. package/docs/authoring/README.md +32 -21
  151. package/docs/authoring/agents.md +38 -7
  152. package/docs/authoring/components.md +111 -6
  153. package/docs/authoring/migrating.md +707 -0
  154. package/docs/authoring/notation.md +281 -29
  155. package/docs/authoring/retired.md +1 -0
  156. package/docs/authoring/structure-rules.md +10 -41
  157. package/docs/authoring/structure.md +53 -2
  158. package/docs/authoring/visualizations.md +144 -18
  159. package/package.json +12 -7
  160. package/src/compiler/astro.ts +57 -17
  161. package/src/compiler/cli.ts +143 -35
  162. package/src/compiler/collections.ts +92 -19
  163. package/src/compiler/content-schema.ts +108 -0
  164. package/src/compiler/content-schemas.ts +391 -0
  165. package/src/compiler/course-config.ts +86 -15
  166. package/src/compiler/exports/combined-html.ts +669 -0
  167. package/src/compiler/exports/download-index.ts +36 -0
  168. package/src/compiler/exports/export-chapters.ts +233 -0
  169. package/src/compiler/exports/export-pages.ts +44 -0
  170. package/src/compiler/exports/html-tree.ts +62 -0
  171. package/src/compiler/exports/latex/capture.ts +651 -0
  172. package/src/compiler/exports/latex/compile.ts +168 -0
  173. package/src/compiler/exports/latex/document.ts +479 -0
  174. package/src/compiler/exports/latex/explico.cls +415 -0
  175. package/src/compiler/exports/latex/numbering.ts +74 -0
  176. package/src/compiler/exports/latex/serialize.ts +787 -0
  177. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  178. package/src/compiler/exports/latex-export.ts +255 -0
  179. package/src/compiler/exports/markdown/serialize.ts +632 -0
  180. package/src/compiler/exports/markdown-export.ts +329 -0
  181. package/src/compiler/exports/standalone-html.ts +39 -70
  182. package/src/compiler/exports/static-tree.ts +944 -0
  183. package/src/compiler/exports/zip.ts +88 -0
  184. package/src/compiler/lesson-derivation.ts +13 -8
  185. package/src/compiler/manifest.ts +299 -28
  186. package/src/compiler/render-diagnostics.ts +111 -0
  187. package/src/components/HoverPanel.astro +204 -6
  188. package/src/components/PartOverview.astro +12 -9
  189. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  190. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  191. package/src/components/assessments/AssessmentSet.astro +14 -1
  192. package/src/components/block-heading.ts +38 -0
  193. package/src/components/card-arrow.ts +18 -4
  194. package/src/components/card-carry.ts +406 -0
  195. package/src/components/card-drag.ts +1 -30
  196. package/src/components/card-fetch.ts +87 -0
  197. package/src/components/card-manager.ts +1222 -38
  198. package/src/components/card-memory.ts +5 -1
  199. package/src/components/card-placement.ts +211 -13
  200. package/src/components/card-side.ts +281 -31
  201. package/src/components/citation/CitationLayer.astro +45 -40
  202. package/src/components/diagram/Diagram.astro +123 -44
  203. package/src/components/figure/Figure.astro +11 -0
  204. package/src/components/figure/FloatCaption.astro +4 -17
  205. package/src/components/float-toolbar.ts +274 -0
  206. package/src/components/memo/Memo.astro +21 -10
  207. package/src/components/memo/ShortNote.astro +4 -0
  208. package/src/components/notation/NotationGlossary.astro +22 -17
  209. package/src/components/notation/NotationLayer.astro +321 -79
  210. package/src/components/notation/NotationMath.astro +8 -1
  211. package/src/components/notation/render-notation-math.ts +51 -5
  212. package/src/components/readable-text.ts +25 -0
  213. package/src/components/reference/FloatRefLayer.astro +162 -89
  214. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  215. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  216. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  217. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  218. package/src/components/starlight/LessonExportActions.astro +384 -310
  219. package/src/components/starlight/LessonFooter.astro +47 -9
  220. package/src/components/starlight/PrintController.astro +28 -0
  221. package/src/components/starlight/TextSizeControl.astro +137 -0
  222. package/src/components/starlight/print-controller.ts +380 -0
  223. package/src/components/starlight/section-summary.ts +13 -5
  224. package/src/components/table/Table.astro +13 -21
  225. package/src/components/table/table-truncation.ts +15 -0
  226. package/src/content-config.ts +20 -312
  227. package/src/curriculum/validation.ts +72 -24
  228. package/src/reference/base-abbreviations.mjs +2 -2
  229. package/src/reference/base-operators.mjs +2 -2
  230. package/src/reference/base-roman-labels.mjs +4 -3
  231. package/src/reference/citation-format.mjs +74 -0
  232. package/src/reference/consistency.ts +47 -61
  233. package/src/reference/curriculum-alignment.ts +4 -41
  234. package/src/reference/derived-entries.ts +68 -0
  235. package/src/reference/derived-notation.mjs +121 -0
  236. package/src/reference/diagram-fit.ts +104 -0
  237. package/src/reference/diagram-layout.ts +182 -51
  238. package/src/reference/diagrams-validate.ts +2 -1
  239. package/src/reference/equations-validate.ts +41 -2
  240. package/src/reference/equations.mjs +120 -22
  241. package/src/reference/examples-validate.ts +1 -0
  242. package/src/reference/figure-fit.ts +139 -0
  243. package/src/reference/figure-pictures.ts +96 -0
  244. package/src/reference/figures-validate.ts +2 -1
  245. package/src/reference/gate-abbreviations.ts +1 -1
  246. package/src/reference/gate-course-rules.ts +22 -7
  247. package/src/reference/gate-heading-length.ts +25 -7
  248. package/src/reference/gate-heading-structure.ts +40 -1
  249. package/src/reference/gate-indices.ts +470 -0
  250. package/src/reference/gate-math.ts +102 -13
  251. package/src/reference/gate-mdx-expression.ts +45 -0
  252. package/src/reference/gate-prop-text.ts +145 -0
  253. package/src/reference/gate-prose-notation.ts +19 -5
  254. package/src/reference/gate-retired-keys.ts +101 -0
  255. package/src/reference/gloss.ts +2 -0
  256. package/src/reference/katex-measure.ts +384 -0
  257. package/src/reference/keyed-float-validate.ts +22 -0
  258. package/src/reference/keyed-jsx-floats.mjs +11 -14
  259. package/src/reference/lab-math-scope.ts +15 -1
  260. package/src/reference/math-glyphs.mjs +851 -46
  261. package/src/reference/mixed-content.ts +11 -2
  262. package/src/reference/page-notation.ts +100 -3
  263. package/src/reference/references.ts +14 -0
  264. package/src/reference/registry.ts +154 -8
  265. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  266. package/src/reference/rehype-heading-anchors.mjs +11 -1
  267. package/src/reference/remark-citation.mjs +0 -0
  268. package/src/reference/remark-notation.mjs +84 -20
  269. package/src/reference/render-lab-math.ts +29 -5
  270. package/src/reference/sections-validate.ts +67 -3
  271. package/src/reference/sections.mjs +92 -17
  272. package/src/reference/tables-validate.ts +40 -2
  273. package/src/reference/text-measure.ts +63 -0
  274. package/src/reference/text-widths.ts +133 -0
  275. package/src/reference/types.ts +75 -8
  276. package/src/rules/catalog.ts +205 -29
  277. package/src/rules/settings.ts +10 -0
  278. package/src/session/PreferenceStore.ts +53 -0
  279. package/src/styles/global.css +291 -75
@@ -174,6 +174,16 @@ export function resolveRules(config: CourseConfig): ResolvedRules {
174
174
  });
175
175
  }
176
176
 
177
+ if (config.conventions !== undefined) {
178
+ diagnostics.push({
179
+ code: 'config-deprecated-key',
180
+ severity: 'warning',
181
+ message:
182
+ 'CourseConfig `conventions` is no longer read: rule S5, its only reader, was retired into rule N4. ' +
183
+ 'Delete the block. See docs/authoring/retired.md.',
184
+ });
185
+ }
186
+
177
187
  const enabled = new Map<string, boolean>();
178
188
  const settings = new Map<string, Readonly<Record<string, unknown>>>();
179
189
 
@@ -11,6 +11,8 @@
11
11
  * cookie-backed store) and nothing else changes. `createBrowserPreferenceStore`
12
12
  * is the "versioned local-storage adapter" the architecture doc anticipates —
13
13
  * kept deliberately tiny, and the only place `git grep localStorage` matches.
14
+ * `createSessionPreferenceStore` is its per-tab twin, for state one tab keeps
15
+ * of its own (the cards it carries, `card-carry.ts`).
14
16
  */
15
17
 
16
18
  export interface PreferenceStore {
@@ -54,6 +56,57 @@ export function createBrowserPreferenceStore(): PreferenceStore {
54
56
  };
55
57
  }
56
58
 
59
+ /**
60
+ * A `sessionStorage`-backed store: one tab's own state, which a reload, Back
61
+ * and Forward keep and another tab never sees. Wrapped like the local one.
62
+ */
63
+ export function createSessionPreferenceStore(): PreferenceStore {
64
+ return {
65
+ get(key) {
66
+ try {
67
+ return window.sessionStorage.getItem(key);
68
+ } catch {
69
+ return null;
70
+ }
71
+ },
72
+ set(key, value) {
73
+ try {
74
+ window.sessionStorage.setItem(key, value);
75
+ } catch {
76
+ // Storage unavailable or full — the tab still works, it just forgets.
77
+ }
78
+ },
79
+ remove(key) {
80
+ try {
81
+ window.sessionStorage.removeItem(key);
82
+ } catch {
83
+ // Storage unavailable — there is nothing stored to forget.
84
+ }
85
+ },
86
+ };
87
+ }
88
+
89
+ /**
90
+ * The store's one inline twin: the source of a script that copies a stored
91
+ * preference onto the root element as `attribute` before the page first
92
+ * paints. Every other reader of a preference is a module script, which runs
93
+ * after that paint: fine for a panel's state, but a page drawn at one text
94
+ * size and then redrawn at another visibly jumps. Only a value in `values`
95
+ * is copied; anything else stored is ignored. It is built here so that
96
+ * browser storage is still touched in this file alone.
97
+ */
98
+ export function inlineRootAttributeScript(
99
+ key: string,
100
+ attribute: string,
101
+ values: readonly string[],
102
+ ): string {
103
+ return (
104
+ `try{var v=window.localStorage.getItem(${JSON.stringify(key)});` +
105
+ `if(${JSON.stringify(values)}.indexOf(v)>=0)` +
106
+ `document.documentElement.setAttribute(${JSON.stringify(attribute)},v)}catch(e){}`
107
+ );
108
+ }
109
+
57
110
  /** An in-memory store — the no-op-equivalent seam for SSR or tests. */
58
111
  export function createMemoryPreferenceStore(): PreferenceStore {
59
112
  const map = new Map<string, string>();
@@ -188,6 +188,28 @@ body {
188
188
  padding-inline: 2px;
189
189
  }
190
190
 
191
+ /* An equation wider than the column scrolls inside it, with no scrollbar on
192
+ a phone: the edge it is cut at fades, so the reader sees the formula goes
193
+ on (`data-overflow`, set by `KeyedFloatEnhancer.astro`). The mask fades the
194
+ ink, whatever the background; focus and print show the equation whole. */
195
+ .sl-markdown-content .katex-display[data-overflow='end'] {
196
+ mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
197
+ }
198
+ .sl-markdown-content .katex-display[data-overflow='start'] {
199
+ mask-image: linear-gradient(to left, #000 calc(100% - 2.5rem), transparent);
200
+ }
201
+ .sl-markdown-content .katex-display[data-overflow='start end'] {
202
+ mask-image: linear-gradient(to right, transparent, #000 2.5rem, #000 calc(100% - 2.5rem), transparent);
203
+ }
204
+ .sl-markdown-content .katex-display[data-overflow]:focus-visible {
205
+ mask-image: none;
206
+ }
207
+ @media print {
208
+ .sl-markdown-content .katex-display[data-overflow] {
209
+ mask-image: none;
210
+ }
211
+ }
212
+
191
213
  /* Focusable only while it overflows (`KeyedFloatEnhancer.astro`), so it can
192
214
  be scrolled from the keyboard. */
193
215
  .sl-markdown-content .katex-display:focus-visible {
@@ -264,6 +286,24 @@ body {
264
286
  margin-block-start: 0;
265
287
  }
266
288
 
289
+ /* The reader's text size on a touch screen (`TextSizeControl.astro`): the
290
+ root font size, which every `rem` follows, so the text, the math, the
291
+ spacing and the cards scale together. Screen only: print keeps its own
292
+ sizes, and a screen with hover has the browser's zoom instead. */
293
+ @media screen and (hover: none) {
294
+ :root[data-text-size='90'] {
295
+ font-size: 90%;
296
+ }
297
+
298
+ :root[data-text-size='80'] {
299
+ font-size: 80%;
300
+ }
301
+
302
+ :root[data-text-size='70'] {
303
+ font-size: 70%;
304
+ }
305
+ }
306
+
267
307
  .lab-chart {
268
308
  overflow-x: auto;
269
309
  min-height: 18rem;
@@ -276,6 +316,14 @@ body {
276
316
  background: var(--sl-color-bg);
277
317
  }
278
318
 
319
+ /* A lab shows the build's copy of its default plot (its children, in an
320
+ `<astro-slot>`) until it has drawn its own into `.lab-chart-live`. The
321
+ copy goes the moment the live plot exists, so the two never stack, not
322
+ even for the frame before the island drops it (rule V3). */
323
+ .lab-chart:has(> .lab-chart-live > svg) > astro-slot {
324
+ display: none;
325
+ }
326
+
279
327
  .cds-leg-chart:empty {
280
328
  display: none;
281
329
  }
@@ -1372,7 +1420,7 @@ details.layout-part-has-open > summary > .group-label {
1372
1420
  end. It rises above the raised header, or the header's box takes every
1373
1421
  tap and the navigation never opens on a phone. Starlight hides the
1374
1422
  button from 50rem up, so the rule needs no media query. */
1375
- starlight-menu-button > button {
1423
+ .sl-menu-button {
1376
1424
  z-index: calc(var(--sl-z-index-navbar) + 2);
1377
1425
  }
1378
1426
 
@@ -1417,13 +1465,111 @@ starlight-menu-button > button {
1417
1465
  z-index: var(--sl-z-index-toc);
1418
1466
  }
1419
1467
 
1420
- /* S9: "Keep the article left", fixed at the top of the side column just
1421
- under the header, over the cards scrolling past it. `card-side.ts` sets
1422
- its `left`. */
1423
- .explico-keep-left {
1468
+ /* The row of card controls (`card-side.ts`, S9, S11, K1): at the top of the
1469
+ side column, "Article left", "Carry cards" and, once a card has been
1470
+ resized, the width menu; with no side column in use, "Carry cards" alone
1471
+ at the article's top right while a card is kept (D7). Fixed just under
1472
+ the site header, over the cards scrolling past; `card-side.ts` sets its
1473
+ `left` or `right`. It wraps rather than reaching over the rail. */
1474
+ .explico-side-controls {
1424
1475
  position: fixed;
1425
1476
  z-index: var(--sl-z-index-navbar);
1426
1477
  top: calc(var(--sl-nav-height) + 0.5rem);
1478
+ display: flex;
1479
+ flex-wrap: wrap;
1480
+ align-items: center;
1481
+ gap: 0.4rem;
1482
+ }
1483
+
1484
+ .explico-side-controls[data-at='article'] {
1485
+ justify-content: flex-end;
1486
+ }
1487
+
1488
+ .explico-side-controls[hidden] {
1489
+ display: none;
1490
+ }
1491
+
1492
+ /* S11: the width menu, a pill like the switch beside it. */
1493
+ .explico-side-width {
1494
+ display: inline-flex;
1495
+ align-items: center;
1496
+ gap: 0.35rem;
1497
+ padding: 0.2rem 0.35rem 0.2rem 0.65rem;
1498
+ border: var(--ex-edge-border);
1499
+ border-radius: 999px;
1500
+ font-size: var(--sl-text-xs);
1501
+ line-height: 1.4;
1502
+ color: var(--sl-color-gray-2);
1503
+ background: var(--sl-color-bg);
1504
+ box-shadow: var(--sl-shadow-sm);
1505
+ }
1506
+
1507
+ .explico-side-width[hidden] {
1508
+ display: none;
1509
+ }
1510
+
1511
+ .explico-side-width select {
1512
+ border: 0;
1513
+ padding: 0 0.2rem;
1514
+ font: inherit;
1515
+ color: var(--sl-color-text);
1516
+ background: transparent;
1517
+ cursor: pointer;
1518
+ }
1519
+
1520
+ .explico-side-width:focus-within {
1521
+ outline: 3px solid var(--sl-color-accent);
1522
+ outline-offset: 2px;
1523
+ }
1524
+
1525
+ /* K7: a card's "Opened at" link lands on the trigger the card was opened
1526
+ at, and lights it a moment (`card-manager.ts`, `is-card-origin`). */
1527
+ .is-card-origin {
1528
+ border-radius: 0.2em;
1529
+ animation: explico-card-origin 1.2s ease-out;
1530
+ }
1531
+
1532
+ @keyframes explico-card-origin {
1533
+ from {
1534
+ background-color: var(--sl-color-accent-low);
1535
+ box-shadow: 0 0 0 0.2em var(--sl-color-accent-low);
1536
+ }
1537
+ to {
1538
+ background-color: transparent;
1539
+ box-shadow: 0 0 0 0.2em transparent;
1540
+ }
1541
+ }
1542
+
1543
+ @media (prefers-reduced-motion: reduce) {
1544
+ .is-card-origin {
1545
+ animation: none;
1546
+ background-color: var(--sl-color-accent-low);
1547
+ }
1548
+ }
1549
+
1550
+ /* K5: cards carried in from another page and held at the window's bottom
1551
+ edge never leave it, so they would cover the page's last stretch — its
1552
+ Next and Previous links. The page grows by their stack, so its end
1553
+ scrolls clear of them (`card-manager.ts` sets the room). */
1554
+ @media screen {
1555
+ body {
1556
+ padding-block-end: var(--explico-carried-room, 0px);
1557
+ }
1558
+ }
1559
+
1560
+ /* S11: while a docked card reaches over the rail, the rail goes under the
1561
+ card layer, so the card and its edge stay whole; the header covers both. */
1562
+ :root[data-card-over-rail] .right-sidebar {
1563
+ z-index: calc(var(--sl-z-index-navbar) - 1);
1564
+ }
1565
+
1566
+ :root[data-card-resizing],
1567
+ :root[data-card-resizing] * {
1568
+ cursor: col-resize !important;
1569
+ user-select: none;
1570
+ }
1571
+
1572
+ .explico-side-switch {
1427
1573
  display: inline-flex;
1428
1574
  align-items: center;
1429
1575
  gap: 0.45rem;
@@ -1440,20 +1586,20 @@ starlight-menu-button > button {
1440
1586
  cursor: pointer;
1441
1587
  }
1442
1588
 
1443
- .explico-keep-left[hidden] {
1589
+ .explico-side-switch[hidden] {
1444
1590
  display: none;
1445
1591
  }
1446
1592
 
1447
- .explico-keep-left:hover {
1593
+ .explico-side-switch:hover {
1448
1594
  color: var(--sl-color-text);
1449
1595
  }
1450
1596
 
1451
- .explico-keep-left:focus-visible {
1597
+ .explico-side-switch:focus-visible {
1452
1598
  outline: 3px solid var(--sl-color-accent);
1453
1599
  outline-offset: 2px;
1454
1600
  }
1455
1601
 
1456
- .explico-keep-left-switch {
1602
+ .explico-side-switch-knob {
1457
1603
  position: relative;
1458
1604
  flex: none;
1459
1605
  width: 1.75em;
@@ -1463,7 +1609,7 @@ starlight-menu-button > button {
1463
1609
  transition: background-color 100ms ease;
1464
1610
  }
1465
1611
 
1466
- .explico-keep-left-switch::after {
1612
+ .explico-side-switch-knob::after {
1467
1613
  content: '';
1468
1614
  position: absolute;
1469
1615
  top: 0.125em;
@@ -1475,33 +1621,33 @@ starlight-menu-button > button {
1475
1621
  transition: transform 100ms ease;
1476
1622
  }
1477
1623
 
1478
- .explico-keep-left[aria-pressed='true'] .explico-keep-left-switch {
1624
+ .explico-side-switch[aria-pressed='true'] .explico-side-switch-knob {
1479
1625
  background: var(--sl-color-accent);
1480
1626
  }
1481
1627
 
1482
- .explico-keep-left[aria-pressed='true'] .explico-keep-left-switch::after {
1628
+ .explico-side-switch[aria-pressed='true'] .explico-side-switch-knob::after {
1483
1629
  transform: translateX(0.75em);
1484
1630
  }
1485
1631
 
1486
1632
  @media (prefers-reduced-motion: reduce) {
1487
- .explico-keep-left-switch,
1488
- .explico-keep-left-switch::after {
1633
+ .explico-side-switch-knob,
1634
+ .explico-side-switch-knob::after {
1489
1635
  transition: none;
1490
1636
  }
1491
1637
  }
1492
1638
 
1493
1639
  @media (forced-colors: active) {
1494
- .explico-keep-left-switch {
1640
+ .explico-side-switch-knob {
1495
1641
  border: 1px solid ButtonText;
1496
1642
  }
1497
1643
 
1498
- .explico-keep-left[aria-pressed='true'] .explico-keep-left-switch {
1644
+ .explico-side-switch[aria-pressed='true'] .explico-side-switch-knob {
1499
1645
  background: Highlight;
1500
1646
  }
1501
1647
  }
1502
1648
 
1503
1649
  @media print {
1504
- .explico-keep-left {
1650
+ .explico-side-controls {
1505
1651
  display: none !important;
1506
1652
  }
1507
1653
  }
@@ -1556,14 +1702,6 @@ starlight-menu-button > button {
1556
1702
  overflow-wrap: anywhere;
1557
1703
  }
1558
1704
 
1559
- .sl-markdown-content .citation-status {
1560
- padding-inline: 0.4em;
1561
- border: 1px solid var(--sl-color-gray-5);
1562
- border-radius: 999px;
1563
- font-size: var(--sl-text-xs);
1564
- text-transform: lowercase;
1565
- }
1566
-
1567
1705
  .sl-markdown-content .citation-backref {
1568
1706
  margin-inline-start: 0.15rem;
1569
1707
  }
@@ -1606,12 +1744,12 @@ starlight-menu-button > button {
1606
1744
  }
1607
1745
 
1608
1746
  /* Equation identity (D7): a keyed `$$…$$` is wrapped by the remark adapter in
1609
- `.keyed-equation` with an `id="eq-<key>"`, a `data-eq-number`, and a
1610
- `.keyed-equation__controls` pair — a focusable number link, and a "Copy
1611
- LaTeX" button. Prose refers to it with `[[eq:key]]` → `a.equation-ref`. The
1612
- deep-link highlight is a pure-CSS `:target` animation with a
1613
- reduced-motion fallback; `KeyedFloatEnhancer.astro` only re-triggers it on
1614
- a repeated activation and moves focus. */
1747
+ `.keyed-equation` with an `id="eq-<key>"`, a `data-eq-number`, its number
1748
+ (`.keyed-equation__controls`, a focusable number link) and a "Copy LaTeX"
1749
+ button on its bottom border. Prose refers to it with `[[eq:key]]` →
1750
+ `a.equation-ref`. The deep-link highlight is a pure-CSS `:target`
1751
+ animation with a reduced-motion fallback; `KeyedFloatEnhancer.astro` only
1752
+ re-triggers it on a repeated activation and moves focus. */
1615
1753
  .sl-markdown-content .keyed-equation {
1616
1754
  position: relative;
1617
1755
  scroll-margin-block-start: 5rem;
@@ -1646,23 +1784,32 @@ starlight-menu-button > button {
1646
1784
  transition: opacity 120ms ease-out;
1647
1785
  }
1648
1786
 
1787
+ /* Copy LaTeX, centred on the block's bottom border: the edge of its hover
1788
+ highlight. `float-toolbar.ts` measures each equation and sets
1789
+ `--eq-copy-inline-end`: under the number, or, in an equation too short
1790
+ for the number's copy-link button between the two, slid left along the
1791
+ border, clear of the number. It shows while the equation is hovered or
1792
+ focused, and always on a touch screen. */
1649
1793
  .sl-markdown-content .keyed-equation__copy {
1794
+ --eq-copy-size: 1.375rem;
1795
+ position: absolute;
1796
+ inset-block-end: calc(var(--eq-copy-size) / -2);
1797
+ inset-inline-end: var(--eq-copy-inline-end, 1rem);
1798
+ z-index: 2;
1650
1799
  display: inline-flex;
1651
1800
  align-items: center;
1652
1801
  justify-content: center;
1653
- width: 1.5rem;
1654
- height: 1.5rem;
1802
+ width: var(--eq-copy-size);
1803
+ height: var(--eq-copy-size);
1655
1804
  padding: 0;
1656
1805
  border: 1px solid var(--sl-color-gray-5);
1657
1806
  border-radius: 999px;
1658
- color: var(--sl-color-gray-1);
1659
- background: transparent;
1807
+ color: var(--sl-color-gray-2);
1808
+ background: var(--sl-color-bg);
1809
+ box-shadow: 0 1px 3px rgb(0 0 0 / 0.12);
1660
1810
  font: inherit;
1661
1811
  cursor: pointer;
1662
- /* Quiet by default for the same reason as the number above — it fades in
1663
- alongside it, not the equation, so print (which drops it entirely, see
1664
- below) never has to reconcile the two. */
1665
- opacity: 0.45;
1812
+ opacity: 0;
1666
1813
  transition:
1667
1814
  opacity 120ms ease-out,
1668
1815
  border-color 120ms ease-out,
@@ -1670,8 +1817,8 @@ starlight-menu-button > button {
1670
1817
  }
1671
1818
 
1672
1819
  .sl-markdown-content .keyed-equation__copy svg {
1673
- width: 0.85rem;
1674
- height: 0.85rem;
1820
+ width: 0.75rem;
1821
+ height: 0.75rem;
1675
1822
  fill: none;
1676
1823
  stroke: currentColor;
1677
1824
  stroke-width: 1.75;
@@ -1713,6 +1860,78 @@ starlight-menu-button > button {
1713
1860
  outline-offset: 2px;
1714
1861
  }
1715
1862
 
1863
+ /* The toolbar on a float's number (`float-toolbar.ts`): Pin and Dock just
1864
+ above the number, Copy link just below it, none of them over it. The
1865
+ script places each part against the number, inside the number's own
1866
+ box; it shows only with a mouse, and never prints. */
1867
+ .float-toolbar {
1868
+ display: contents;
1869
+ }
1870
+
1871
+ .float-toolbar[hidden] {
1872
+ display: none;
1873
+ }
1874
+
1875
+ .float-toolbar__above,
1876
+ .float-toolbar__link {
1877
+ position: absolute;
1878
+ z-index: 4;
1879
+ display: inline-flex;
1880
+ gap: 0.25rem;
1881
+ }
1882
+
1883
+ .float-toolbar__button,
1884
+ .float-toolbar__link {
1885
+ align-items: center;
1886
+ justify-content: center;
1887
+ width: 1.375rem;
1888
+ height: 1.375rem;
1889
+ padding: 0;
1890
+ border: 1px solid var(--sl-color-gray-5);
1891
+ border-radius: 999px;
1892
+ color: var(--sl-color-gray-2);
1893
+ background: var(--sl-color-bg);
1894
+ box-shadow: 0 1px 3px rgb(0 0 0 / 0.12);
1895
+ font: inherit;
1896
+ text-decoration: none;
1897
+ cursor: pointer;
1898
+ }
1899
+
1900
+ .float-toolbar__button {
1901
+ display: inline-flex;
1902
+ }
1903
+
1904
+ .float-toolbar__button:hover,
1905
+ .float-toolbar__button:focus-visible,
1906
+ .float-toolbar__link:hover,
1907
+ .float-toolbar__link:focus-visible {
1908
+ border-color: var(--sl-color-accent);
1909
+ color: var(--sl-color-text-accent);
1910
+ }
1911
+
1912
+ .float-toolbar svg {
1913
+ width: 0.75rem;
1914
+ height: 0.75rem;
1915
+ }
1916
+
1917
+ .float-toolbar__button svg {
1918
+ fill: none;
1919
+ stroke: currentColor;
1920
+ stroke-width: 1.5;
1921
+ stroke-linecap: round;
1922
+ stroke-linejoin: round;
1923
+ }
1924
+
1925
+ .float-toolbar__link svg {
1926
+ fill: currentColor;
1927
+ }
1928
+
1929
+ @media print {
1930
+ .float-toolbar {
1931
+ display: none !important;
1932
+ }
1933
+ }
1934
+
1716
1935
  /* The click-to-copy gesture (`KeyedFloatEnhancer.astro`) works on any part of
1717
1936
  the box — including the reserved gutter between the equation and its
1718
1937
  number/button, not just the rendered `.katex-html` — that isn't a symbol
@@ -2128,6 +2347,8 @@ starlight-menu-button > button {
2128
2347
  }
2129
2348
 
2130
2349
  .sl-markdown-content .float-caption__number {
2350
+ /* The room the chain icon used to keep before the short caption. */
2351
+ margin-inline-end: 0.45em;
2131
2352
  font-weight: 600;
2132
2353
  font-variant-numeric: tabular-nums;
2133
2354
  color: var(--sl-color-text-accent);
@@ -2139,31 +2360,7 @@ starlight-menu-button > button {
2139
2360
  text-decoration: underline;
2140
2361
  }
2141
2362
 
2142
- .sl-markdown-content .float-caption__anchor {
2143
- display: inline-flex;
2144
- width: 1.15em;
2145
- height: 1.15em;
2146
- margin-inline: 0.15em 0.3em;
2147
- vertical-align: -0.2em;
2148
- align-items: center;
2149
- justify-content: center;
2150
- color: var(--sl-color-gray-4);
2151
- text-decoration: none;
2152
- -webkit-user-select: none;
2153
- user-select: none;
2154
- }
2155
-
2156
- .sl-markdown-content .float-caption__anchor svg {
2157
- width: 0.85em;
2158
- height: 0.85em;
2159
- }
2160
-
2161
- .sl-markdown-content .float-caption__anchor:hover,
2162
- .sl-markdown-content .float-caption__anchor:focus-visible {
2163
- color: var(--sl-color-text-accent);
2164
- }
2165
-
2166
- .sl-markdown-content :is(.float-caption__number, .float-caption__anchor):focus-visible {
2363
+ .sl-markdown-content .float-caption__number:focus-visible {
2167
2364
  outline: 2px solid var(--sl-color-accent);
2168
2365
  outline-offset: 0.15rem;
2169
2366
  }
@@ -2651,12 +2848,14 @@ starlight-menu-button > button {
2651
2848
  opacity: 1;
2652
2849
  }
2653
2850
 
2654
- /* A narrow screen shows no section number and, often, has no hover to reveal
2655
- the chain icon — yet the icon is the way to the section's link and, in its
2656
- "Link copied · § 1.1.2", its number (docs/plans/titles-and-containers.md
2657
- Q3). So it is always shown there, smaller and in the muted text colour.
2658
- The size variable also sets the room `anchor-links.css` reserves for it at
2659
- the end of the title. After the hover rules above, which it overrides. */
2851
+ /* A narrow screen shows no section number, so the chain icon is the way to
2852
+ the section's link and, in its "Link copied · § 1.1.2", its number
2853
+ (docs/plans/titles-and-containers.md Q3). There it is smaller and in the
2854
+ muted text colour. The size variable also sets the room `anchor-links.css`
2855
+ reserves for it at the end of the title. A touch screen has no hover to
2856
+ reveal it, so there it is always shown; a narrow window with a mouse
2857
+ reveals it on hover, as a wide one does, instead of showing it on every
2858
+ heading. After the hover rules above, which it overrides. */
2660
2859
  @media screen and (width < 50em) {
2661
2860
  .sl-markdown-content .sl-heading-wrapper {
2662
2861
  --sl-anchor-icon-size: 0.7em;
@@ -2664,11 +2863,17 @@ starlight-menu-button > button {
2664
2863
 
2665
2864
  .sl-markdown-content .sl-heading-wrapper > .sl-anchor-link,
2666
2865
  .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar > .sl-anchor-link {
2667
- opacity: 1;
2668
2866
  color: var(--sl-color-gray-3);
2669
2867
  }
2670
2868
  }
2671
2869
 
2870
+ @media screen and (width < 50em) and (hover: none) {
2871
+ .sl-markdown-content .sl-heading-wrapper > .sl-anchor-link,
2872
+ .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar > .sl-anchor-link {
2873
+ opacity: 1;
2874
+ }
2875
+ }
2876
+
2672
2877
  /* The chevron sits to the *left* of the title without shifting the title's
2673
2878
  own position: `markdown.css`'s default marker is inline, right before the
2674
2879
  text, pushed left slightly by a negative margin — meaning the title
@@ -3125,7 +3330,7 @@ starlight-menu-button > button {
3125
3330
  }
3126
3331
  }
3127
3332
 
3128
- /* Browser-native lesson printing. The print controller opens real native
3333
+ /* Browser-native page printing. The print controller opens real native
3129
3334
  disclosures before the browser lays out pages; these rules own only the
3130
3335
  paper presentation and therefore also remain a useful no-JS fallback. */
3131
3336
  @media print {
@@ -3175,7 +3380,6 @@ starlight-menu-button > button {
3175
3380
  .keyed-table__pagination-links,
3176
3381
  .keyed-table__more-row,
3177
3382
  .sl-anchor-link,
3178
- .float-caption__anchor,
3179
3383
  .short-note__collapse,
3180
3384
  .assessment-actions,
3181
3385
  .assessment-chevron,
@@ -3186,6 +3390,18 @@ starlight-menu-button > button {
3186
3390
  display: none !important;
3187
3391
  }
3188
3392
 
3393
+ /* Print is always light (above), so the splash hero prints its light image
3394
+ whatever theme the reader is in; the dark one's pale strokes would vanish
3395
+ on white paper. Starlight's own site logo does this with `print:hidden`
3396
+ and `print:block`, but its hero does not. */
3397
+ .hero > img.light\:sl-hidden {
3398
+ display: none;
3399
+ }
3400
+
3401
+ .hero > img.dark\:sl-hidden {
3402
+ display: block;
3403
+ }
3404
+
3189
3405
  .main-frame,
3190
3406
  .main-pane,
3191
3407
  .content-panel,