explico 0.7.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/CHANGELOG.md +1409 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +54 -3
  5. package/dist/compiler/cli.js +80 -6
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +84 -8
  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 +14 -1
  15. package/dist/compiler/logging.js +6 -2
  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 +21 -11
  27. package/dist/reference/delimiter-size.js +15 -39
  28. package/dist/reference/diagrams-validate.d.ts +7 -2
  29. package/dist/reference/diagrams-validate.js +9 -1
  30. package/dist/reference/equations-validate.d.ts +8 -5
  31. package/dist/reference/equations-validate.js +1 -0
  32. package/dist/reference/equations.d.mts +69 -34
  33. package/dist/reference/equations.mjs +90 -44
  34. package/dist/reference/examples-validate.d.ts +31 -0
  35. package/dist/reference/examples-validate.js +48 -0
  36. package/dist/reference/figures-validate.d.ts +1 -1
  37. package/dist/reference/figures-validate.js +4 -1
  38. package/dist/reference/folds-validate.d.ts +33 -0
  39. package/dist/reference/folds-validate.js +32 -0
  40. package/dist/reference/gate-abbreviations.d.ts +3 -1
  41. package/dist/reference/gate-abbreviations.js +3 -2
  42. package/dist/reference/gate-course-rules.d.ts +32 -0
  43. package/dist/reference/gate-course-rules.js +85 -0
  44. package/dist/reference/gate-heading-length.d.ts +5 -2
  45. package/dist/reference/gate-heading-length.js +47 -23
  46. package/dist/reference/gate-heading-structure.d.ts +41 -0
  47. package/dist/reference/gate-heading-structure.js +113 -0
  48. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  49. package/dist/reference/gate-math-punctuation.js +251 -0
  50. package/dist/reference/gate-math.d.ts +44 -6
  51. package/dist/reference/gate-math.js +63 -10
  52. package/dist/reference/gate-part-pages.d.ts +50 -0
  53. package/dist/reference/gate-part-pages.js +118 -0
  54. package/dist/reference/gate-prose-notation.d.ts +32 -11
  55. package/dist/reference/gate-prose-notation.js +30 -8
  56. package/dist/reference/katex-delimiters.d.mts +15 -0
  57. package/dist/reference/katex-delimiters.mjs +138 -0
  58. package/dist/reference/katex-options.mjs +4 -0
  59. package/dist/reference/keyed-float-validate.d.ts +27 -10
  60. package/dist/reference/keyed-float-validate.js +15 -1
  61. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  62. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  63. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  64. package/dist/reference/knowledge-checks.d.mts +3 -1
  65. package/dist/reference/knowledge-checks.mjs +9 -4
  66. package/dist/reference/lab-math-scope.d.ts +1 -0
  67. package/dist/reference/lab-math-slot.d.ts +34 -0
  68. package/dist/reference/lab-math-slot.js +6 -0
  69. package/dist/reference/math-glyphs.d.mts +80 -4
  70. package/dist/reference/math-glyphs.mjs +588 -39
  71. package/dist/reference/page-notation.d.ts +6 -1
  72. package/dist/reference/raw-floats-validate.js +1 -1
  73. package/dist/reference/raw-floats.mjs +1 -1
  74. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  75. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  76. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  77. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  78. package/dist/reference/remark-asides.d.mts +3 -0
  79. package/dist/reference/remark-asides.mjs +180 -0
  80. package/dist/reference/remark-memo.d.mts +3 -1
  81. package/dist/reference/remark-memo.mjs +42 -11
  82. package/dist/reference/remark-notation.d.mts +60 -10
  83. package/dist/reference/remark-notation.mjs +387 -36
  84. package/dist/reference/render-lab-math.d.ts +4 -26
  85. package/dist/reference/render-lab-math.js +5 -5
  86. package/dist/reference/sections-validate.d.ts +27 -0
  87. package/dist/reference/sections-validate.js +41 -0
  88. package/dist/reference/sections.d.mts +97 -0
  89. package/dist/reference/sections.mjs +306 -0
  90. package/dist/reference/tables-validate.d.ts +1 -1
  91. package/dist/reference/types.d.ts +24 -1
  92. package/dist/reference/unit-words.d.ts +37 -0
  93. package/dist/reference/unit-words.js +170 -0
  94. package/dist/rules/catalog.d.ts +176 -0
  95. package/dist/rules/catalog.js +983 -0
  96. package/dist/rules/course-rules.d.ts +115 -0
  97. package/dist/rules/course-rules.js +292 -0
  98. package/dist/rules/course-rules.schema.json +138 -0
  99. package/dist/rules/in-effect.d.ts +53 -0
  100. package/dist/rules/in-effect.js +142 -0
  101. package/dist/rules/rules.schema.json +1072 -0
  102. package/dist/rules/settings.d.ts +3 -0
  103. package/dist/rules/settings.js +290 -0
  104. package/dist/rules/waivers.d.ts +53 -0
  105. package/dist/rules/waivers.js +132 -0
  106. package/dist/session/PreferenceStore.d.ts +5 -0
  107. package/dist/session/PreferenceStore.js +9 -0
  108. package/docs/VERSIONING.md +2 -2
  109. package/docs/authoring/README.md +232 -40
  110. package/docs/authoring/agents.md +55 -9
  111. package/docs/authoring/components.md +178 -22
  112. package/docs/authoring/migrating.md +1324 -2
  113. package/docs/authoring/notation.md +616 -344
  114. package/docs/authoring/prose.md +551 -0
  115. package/docs/authoring/retired.md +35 -0
  116. package/docs/authoring/structure-rules.md +422 -40
  117. package/docs/authoring/structure.md +445 -62
  118. package/docs/authoring/versioning.md +20 -28
  119. package/docs/authoring/visualizations.md +142 -20
  120. package/docs/katex-bracket-scaling.md +191 -0
  121. package/package.json +3 -2
  122. package/src/compiler/cli.ts +139 -6
  123. package/src/compiler/collections.ts +144 -10
  124. package/src/compiler/course-config.ts +273 -34
  125. package/src/compiler/knowledge-version.ts +1 -1
  126. package/src/compiler/lesson-derivation.ts +54 -15
  127. package/src/compiler/logging.ts +21 -4
  128. package/src/compiler/manifest.ts +573 -104
  129. package/src/components/BlockHeading.astro +113 -0
  130. package/src/components/CourseContents.astro +221 -0
  131. package/src/components/CurriculumMap.astro +5 -6
  132. package/src/components/FloatIndex.astro +200 -0
  133. package/src/components/HoverPanel.astro +431 -79
  134. package/src/components/PartBridge.astro +27 -0
  135. package/src/components/PartOverview.astro +401 -0
  136. package/src/components/PartScope.astro +27 -0
  137. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  138. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  139. package/src/components/assessments/AssessmentSet.astro +23 -26
  140. package/src/components/block-heading.ts +146 -0
  141. package/src/components/card-arrow.ts +243 -0
  142. package/src/components/card-drag.ts +631 -0
  143. package/src/components/card-jump.ts +45 -0
  144. package/src/components/card-manager.ts +1589 -0
  145. package/src/components/card-memory.ts +177 -0
  146. package/src/components/card-placement.ts +504 -0
  147. package/src/components/card-side.ts +336 -0
  148. package/src/components/card-travel.ts +197 -0
  149. package/src/components/citation/CitationLayer.astro +77 -76
  150. package/src/components/diagram/Diagram.astro +161 -316
  151. package/src/components/disclosure/course-disclosure.ts +24 -0
  152. package/src/components/disclosure/hover-preview.ts +122 -42
  153. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  154. package/src/components/disclosure/print-as-shown.ts +252 -0
  155. package/src/components/examples/CompactExample.astro +3 -1
  156. package/src/components/examples/CompactExamples.astro +101 -94
  157. package/src/components/figure/Figure.astro +174 -274
  158. package/src/components/figure/FloatCaption.astro +87 -0
  159. package/src/components/figure/figure-state.ts +58 -0
  160. package/src/components/figure/figure-tools.ts +87 -0
  161. package/src/components/float-name.ts +101 -0
  162. package/src/components/fold/Fold.astro +125 -0
  163. package/src/components/fold/fold-id.ts +40 -0
  164. package/src/components/memo/Memo.astro +162 -231
  165. package/src/components/memo/ShortNote.astro +6 -6
  166. package/src/components/notation/NotationFormula.astro +5 -1
  167. package/src/components/notation/NotationGlossary.astro +24 -14
  168. package/src/components/notation/NotationLayer.astro +169 -162
  169. package/src/components/notation/NotationMath.astro +10 -1
  170. package/src/components/notation/render-notation-math.ts +111 -10
  171. package/src/components/reference/FloatRefLayer.astro +485 -423
  172. package/src/components/starlight/AboutPanel.astro +22 -6
  173. package/src/components/starlight/CollapsibleSections.astro +207 -93
  174. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  175. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  176. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  177. package/src/components/starlight/LayoutHeader.astro +298 -19
  178. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  179. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  180. package/src/components/starlight/LayoutSidebar.astro +229 -11
  181. package/src/components/starlight/LessonExportActions.astro +53 -72
  182. package/src/components/starlight/LessonFooter.astro +26 -4
  183. package/src/components/starlight/section-parts.ts +15 -2
  184. package/src/components/starlight/section-summary.ts +181 -0
  185. package/src/components/table/Table.astro +95 -76
  186. package/src/components/table/TableOverflowExpander.astro +65 -28
  187. package/src/content-config.ts +40 -4
  188. package/src/reference/base-accents.mjs +31 -0
  189. package/src/reference/base-roman-labels.mjs +2 -2
  190. package/src/reference/consistency.ts +176 -5
  191. package/src/reference/delimiter-size.ts +40 -61
  192. package/src/reference/diagrams-validate.ts +17 -1
  193. package/src/reference/equations-validate.ts +10 -5
  194. package/src/reference/equations.mjs +90 -44
  195. package/src/reference/examples-validate.ts +97 -0
  196. package/src/reference/figures-validate.ts +6 -1
  197. package/src/reference/folds-validate.ts +65 -0
  198. package/src/reference/gate-abbreviations.ts +3 -2
  199. package/src/reference/gate-course-rules.ts +134 -0
  200. package/src/reference/gate-heading-length.ts +68 -37
  201. package/src/reference/gate-heading-structure.ts +193 -0
  202. package/src/reference/gate-math-punctuation.ts +381 -0
  203. package/src/reference/gate-math.ts +119 -12
  204. package/src/reference/gate-part-pages.ts +217 -0
  205. package/src/reference/gate-prose-notation.ts +74 -13
  206. package/src/reference/katex-delimiters.mjs +138 -0
  207. package/src/reference/katex-options.mjs +4 -0
  208. package/src/reference/keyed-float-validate.ts +45 -11
  209. package/src/reference/keyed-jsx-floats.mjs +73 -20
  210. package/src/reference/knowledge-checks-validate.ts +1 -1
  211. package/src/reference/knowledge-checks.mjs +9 -4
  212. package/src/reference/lab-math-slot.ts +36 -0
  213. package/src/reference/math-glyphs.mjs +588 -39
  214. package/src/reference/raw-floats-validate.ts +1 -1
  215. package/src/reference/raw-floats.mjs +1 -1
  216. package/src/reference/rehype-heading-anchors.mjs +116 -17
  217. package/src/reference/rehype-table-header-labels.mjs +21 -3
  218. package/src/reference/remark-asides.mjs +180 -0
  219. package/src/reference/remark-memo.mjs +42 -11
  220. package/src/reference/remark-notation.mjs +387 -36
  221. package/src/reference/render-lab-math.ts +17 -32
  222. package/src/reference/sections-validate.ts +81 -0
  223. package/src/reference/sections.mjs +306 -0
  224. package/src/reference/tables-validate.ts +2 -1
  225. package/src/reference/types.ts +24 -0
  226. package/src/reference/unit-words.ts +233 -0
  227. package/src/rules/catalog.ts +1298 -0
  228. package/src/rules/course-rules.ts +456 -0
  229. package/src/rules/in-effect.ts +235 -0
  230. package/src/rules/settings.ts +448 -0
  231. package/src/rules/waivers.ts +249 -0
  232. package/src/session/PreferenceStore.ts +15 -0
  233. package/src/styles/global.css +1471 -163
  234. package/src/styles/surfaces.css +182 -0
  235. package/src/components/hover-panel.ts +0 -339
  236. package/src/components/popup-stack.ts +0 -345
@@ -1,4 +1,5 @@
1
1
  @import 'katex/dist/katex.min.css';
2
+ @import './surfaces.css';
2
3
 
3
4
  :root {
4
5
  --sl-color-accent-low: #132f2a;
@@ -13,6 +14,57 @@
13
14
  --sl-color-accent-high: #123c32;
14
15
  }
15
16
 
17
+ /* Heading sizes, one even step (about 1.25) apart. Starlight's defaults are
18
+ sized for a wide docs column: from 50em up an h2 is 35px, which fits some
19
+ 25 characters of a lesson column. Everything that sizes itself from a
20
+ heading reads these tokens — the section chevron's centring, a folded bar,
21
+ the `1.5em` above a section — so it all follows. Both breakpoints are set
22
+ here: Starlight's own values are layered, so this unlayered block would
23
+ otherwise beat its wide-screen step too. */
24
+ :root {
25
+ --sl-text-h1: 1.875rem;
26
+ --sl-text-h2: 1.5rem;
27
+ --sl-text-h3: 1.25rem;
28
+ --sl-text-h4: 1.0625rem;
29
+ --sl-text-h5: 1rem;
30
+ }
31
+
32
+ @media (min-width: 50em) {
33
+ :root {
34
+ --sl-text-h1: 2.25rem;
35
+ --sl-text-h2: 1.75rem;
36
+ --sl-text-h3: 1.375rem;
37
+ --sl-text-h4: 1.125rem;
38
+ --sl-text-h5: 1rem;
39
+ }
40
+ }
41
+
42
+ /* Previous / Next under a lesson: side by side down to a 26rem column.
43
+ Starlight stacks them below 2 × 18rem, and a lesson column between both
44
+ sidebars is already narrower than that, so every page ended in two
45
+ full-width cards. Their titles are one step smaller. Starlight's footer
46
+ also keeps a 3rem top margin for its "last updated" and edit links; a
47
+ course that shows neither keeps only the footer's own gap. Starlight's
48
+ rules are layered, so these unlayered ones win. */
49
+ .pagination-links {
50
+ grid-template-columns: repeat(auto-fit, minmax(min(12.5rem, 100%), 1fr));
51
+ }
52
+
53
+ .pagination-links .link-title {
54
+ font-size: var(--sl-text-lg);
55
+ }
56
+
57
+ footer .meta:not(:has(*)) {
58
+ margin-top: 0;
59
+ }
60
+
61
+ /* The first block under the title keeps only the content panel's own
62
+ padding; a card's or a section's top margin on top of it doubled the gap
63
+ under the title rule. */
64
+ .sl-markdown-content > :first-child {
65
+ margin-block-start: 0;
66
+ }
67
+
16
68
  /* Every intentionally wide surface owns a local horizontal scroller. Keep a
17
69
  replaced element (notably an SVG containing `foreignObject`) from widening
18
70
  iOS WebKit's layout viewport before that local overflow rule takes effect.
@@ -25,31 +77,32 @@ body {
25
77
 
26
78
  .lab-shell {
27
79
  margin-block: 1.5rem;
28
- padding: clamp(1rem, 3vw, 1.5rem);
29
- border: 1px solid var(--sl-color-gray-5);
30
- border-top: 4px solid var(--sl-color-accent);
31
- border-radius: 0.2rem;
32
- /* `--sl-color-gray-7` (the previous background) is only defined in
33
- Starlight's light-theme override, not the dark-theme base palette — it
34
- silently fell back to no background at all in dark mode. `--sl-color-bg`
35
- is defined in both. */
36
- background: color-mix(in srgb, var(--sl-color-bg) 94%, var(--sl-color-accent) 6%);
37
- box-shadow: 0 1rem 2.6rem rgba(0, 0, 0, 0.13);
38
- }
39
-
40
- /* A lab already wrapped in a `<Figure>` sits inside `.keyed-figure__details`,
41
- which draws this exact same card (border, thick top line, tinted
42
- background) around the title *and* the lab together — a nested
43
- `.lab-shell` drawing its own copy of that card again would stack two
44
- borders/backgrounds/lines instead of one. Strip it back to plain spacing
45
- here; a `.lab-shell` used on its own (not inside a figure) is unaffected. */
80
+ }
81
+
82
+ /* A lab is figure material, and a figure is the plain tier (`surfaces.css`):
83
+ no card of its own, only spacing. Inside a `<Figure>` the figure's own
84
+ margin already spaces it. A lab that should read as set apart goes in a
85
+ `### … [card]` section instead (docs/plans/titles-and-containers.md,
86
+ settled decision 7). */
46
87
  .keyed-figure__body .lab-shell {
47
88
  margin-block: 0;
48
- padding: 0;
49
- border: none;
50
- border-radius: 0;
51
- background: none;
52
- box-shadow: none;
89
+ }
90
+
91
+ /* A lab's tools, in the tray after its figure's caption line
92
+ (`Figure.astro`'s `tools` slot): the tray pads them, so they bring no
93
+ margin of their own. Their first line says what to do. */
94
+ .keyed-figure__tools .lab-shell {
95
+ margin-block: 0;
96
+ }
97
+
98
+ .lab-tools > :last-child {
99
+ margin-block-end: 0;
100
+ }
101
+
102
+ .lab-instruction {
103
+ margin-block: 0 0.75rem;
104
+ font-size: var(--sl-text-sm);
105
+ color: var(--sl-color-gray-2);
53
106
  }
54
107
 
55
108
  .lab-shell label,
@@ -113,21 +166,25 @@ body {
113
166
 
114
167
  /* Long lesson equations must scroll inside the content column instead of
115
168
  widening the entire page. This also contains KaTeX's visually hidden MathML
116
- tree, whose intrinsic width can otherwise affect narrow viewports. */
169
+ tree, whose intrinsic width can otherwise affect narrow viewports.
170
+ `overflow-y: hidden` clips at the padding edge, and with no padding that
171
+ edge is KaTeX's metric strut — but glyph ink overshoots its metrics (a
172
+ denominator's `g` descender, a stretched `\right]`), so the bottom of the
173
+ equation was cut off. The padding is the room that ink needs; em-based so
174
+ it scales with the math. */
117
175
  .sl-markdown-content .katex-display {
118
176
  max-width: 100%;
119
177
  overflow-x: auto;
120
178
  overflow-y: hidden;
179
+ padding-block: 0.25em;
121
180
  }
122
181
 
123
- .lab-chart-figure {
124
- margin: 1.25rem 0;
125
- }
126
-
127
- .lab-chart-figure figcaption {
128
- margin-top: 0.5rem;
129
- font-size: 0.85rem;
130
- color: var(--sl-color-gray-2);
182
+ /* Focusable only while it overflows (`KeyedFloatEnhancer.astro`), so it can
183
+ be scrolled from the keyboard. */
184
+ .sl-markdown-content .katex-display:focus-visible {
185
+ outline: 2px solid var(--sl-color-accent);
186
+ outline-offset: 2px;
187
+ border-radius: 0.2rem;
131
188
  }
132
189
 
133
190
  .lab-controls {
@@ -236,12 +293,11 @@ body {
236
293
  margin: 0;
237
294
  }
238
295
 
296
+ /* A knowledge check is one accent card around its heading and this shell
297
+ together (`AssessmentSet.astro` marks the heading `data-card="accent"`,
298
+ `CollapsibleSections.astro` draws the card), so the shell itself is plain. */
239
299
  .assessment-shell {
240
- margin-block: 0.75rem 1.5rem;
241
- padding: clamp(1rem, 3vw, 1.5rem);
242
- border: 1px solid var(--sl-color-gray-5);
243
- border-radius: 0.75rem;
244
- background: var(--sl-color-gray-7);
300
+ margin-block: 0.75rem 0;
245
301
  }
246
302
 
247
303
  .assessment-shell :focus-visible {
@@ -249,23 +305,22 @@ body {
249
305
  outline-offset: 2px;
250
306
  }
251
307
 
252
- .assessment-heading-number {
253
- color: var(--sl-color-text-accent);
254
- white-space: nowrap;
255
- }
256
-
257
308
  .assessment-shell > h3 {
258
309
  margin-block: 0 0.75rem;
259
310
  }
260
311
 
261
312
  .assessment-progress {
262
- margin: 0 0 0.75rem;
313
+ margin: 0.75rem 0 0;
263
314
  font-size: 0.85rem;
264
315
  font-variant-numeric: tabular-nums;
265
316
  text-align: end;
266
317
  color: var(--sl-color-gray-2);
267
318
  }
268
319
 
320
+ .assessment-progress:empty {
321
+ margin: 0;
322
+ }
323
+
269
324
  .assessment-list {
270
325
  list-style: none;
271
326
  margin: 0;
@@ -275,8 +330,8 @@ body {
275
330
  }
276
331
 
277
332
  .assessment-item {
278
- border: 1px solid var(--sl-color-gray-5);
279
- border-radius: 0.5rem;
333
+ border: var(--ex-edge-border);
334
+ border-radius: var(--ex-radius-accent);
280
335
  background: var(--sl-color-bg);
281
336
  }
282
337
 
@@ -413,12 +468,12 @@ body {
413
468
 
414
469
  .assessment-feedback.is-correct {
415
470
  border-left-color: var(--sl-color-accent);
416
- background: var(--sl-color-gray-7);
471
+ background: var(--ex-surface-tint-neutral);
417
472
  }
418
473
 
419
474
  .assessment-feedback.is-incorrect {
420
475
  border-left-color: var(--sl-color-gray-3);
421
- background: var(--sl-color-gray-7);
476
+ background: var(--ex-surface-tint-neutral);
422
477
  }
423
478
 
424
479
  /* Reader-controlled desktop layout. Collapsed panels remain as narrow edge
@@ -433,33 +488,247 @@ body {
433
488
  display: none !important;
434
489
  }
435
490
 
491
+ /* The drag handle that sets a panel's width. It does nothing without the
492
+ script that wires it, so it ships hidden and that script reveals it. */
493
+ .layout-edge-resizer {
494
+ display: none;
495
+ }
496
+
497
+ .layout-edge-resizer[hidden] {
498
+ display: none !important;
499
+ }
500
+
501
+ /* A part's own page is reachable two ways, because the two answer different
502
+ questions. The `Overview` row is part of the chapter list and is there in
503
+ the HTML, so it is keyboard-reachable, crawlable, and obvious to a reader
504
+ who has never used this sidebar before; the control in the part's header is
505
+ the shortcut for one who has. Both point at `/<part>/`. */
506
+ .sidebar-content a.explico-part-overview,
507
+ .layout-navigation-content a.explico-part-overview {
508
+ display: flex;
509
+ align-items: center;
510
+ gap: 0.45rem;
511
+ }
512
+
513
+ /* Two stacked sheets: the part, rather than one of its chapters. Drawn from a
514
+ border and a shadow so it inherits the link's colour in every state and
515
+ needs no icon font. The sliver between the sheets is the row's background,
516
+ so it has to follow the row when that background changes. */
517
+ a.explico-part-overview {
518
+ --explico-overview-gap: var(--sl-color-bg-sidebar);
519
+ }
520
+
521
+ a.explico-part-overview[aria-current='page'] {
522
+ --explico-overview-gap: var(--sl-color-text-accent);
523
+ }
524
+
525
+ a.explico-part-overview::before {
526
+ content: '';
527
+ flex: none;
528
+ width: 0.55rem;
529
+ height: 0.68rem;
530
+ border: 1.5px solid currentColor;
531
+ border-radius: 0.12rem;
532
+ box-shadow:
533
+ 0.16rem 0.16rem 0 -0.1rem var(--explico-overview-gap),
534
+ 0.16rem 0.16rem 0 0 currentColor;
535
+ }
536
+
537
+ /* The caret leads its part, the way a disclosure triangle leads the thing it
538
+ opens: `>` collapsed, `v` open, in the reading direction before the name it
539
+ applies to. Starlight parks it at the far end of the row instead, where it
540
+ reads as a separate control that happens to share the line. */
541
+ .layout-navigation-content summary {
542
+ justify-content: flex-start;
543
+ gap: 0.25rem;
544
+ }
545
+
546
+ .layout-navigation-content summary > .caret {
547
+ order: -1;
548
+ }
549
+
550
+ .layout-navigation-content summary > .group-label {
551
+ flex: 1;
552
+ min-width: 0;
553
+ }
554
+
555
+ /* The part header's own control. The caret having moved to the front, this
556
+ takes the row's end; the label's click still collapses, exactly as before.
557
+
558
+ It is a sibling of `<summary>`, not a child — a link inside a summary is a
559
+ nested interactive control — so it is lifted into the header row from
560
+ outside it, and the label reserves the width it needs. */
561
+ details.layout-part-has-open {
562
+ position: relative;
563
+ }
564
+
565
+ details.layout-part-has-open > summary > .group-label {
566
+ padding-inline-end: 1.4rem;
567
+ }
568
+
569
+ .layout-part-open {
570
+ position: absolute;
571
+ inset-block-start: 0.3rem;
572
+ inset-inline-end: 0.3rem;
573
+ z-index: 1;
574
+ display: grid;
575
+ place-items: center;
576
+ width: 1.4rem;
577
+ height: 1.4rem;
578
+ border-radius: 0.25rem;
579
+ color: var(--sl-color-gray-3);
580
+ font-size: var(--sl-text-sm);
581
+ line-height: 1;
582
+ text-decoration: none;
583
+ }
584
+
585
+ .layout-part-open:hover,
586
+ .layout-part-open:focus-visible {
587
+ color: var(--sl-color-text-accent);
588
+ background: var(--sl-color-accent-low);
589
+ }
590
+
591
+ @media print {
592
+ .layout-part-open {
593
+ display: none;
594
+ }
595
+ }
596
+
597
+ /* Both rails answer a pointer. Starlight changes only the text colour on
598
+ hover, which on a list of 20 chapters is easy to lose track of; a filled row
599
+ says which line the pointer is actually on. */
600
+ .layout-navigation-content a:not(.layout-part-open):hover,
601
+ .layout-navigation-content a:not(.layout-part-open):focus-visible,
602
+ .layout-page-sidebar-content a:hover,
603
+ .layout-page-sidebar-content a:focus-visible {
604
+ color: var(--sl-color-white);
605
+ background: var(--sl-color-gray-6);
606
+ }
607
+
608
+ /* The part header answers a pointer like the rows under it — it is a control,
609
+ and it was the one row in the panel that looked inert. */
610
+ .layout-navigation-content summary {
611
+ border-radius: 0.25rem;
612
+ }
613
+
614
+ /* …and it says, quietly and all the time, which part the open page belongs
615
+ to. Three weights, so the states never read as the same thing: the current
616
+ chapter is a solid accent row, the part holding it is tinted, a hovered row
617
+ is a neutral fill that goes away again. `:has()` keeps this in CSS, which
618
+ means it is right in the first painted frame. */
619
+ .layout-navigation-content details:has(a[aria-current='page']) > summary {
620
+ background: var(--sl-color-accent-low);
621
+ }
622
+
623
+ .layout-navigation-content summary:hover,
624
+ .layout-navigation-content summary:focus-visible {
625
+ background: var(--sl-color-gray-6);
626
+ }
627
+
628
+ .layout-page-sidebar-content a:hover,
629
+ .layout-page-sidebar-content a:focus-visible {
630
+ color: var(--sl-color-text-accent);
631
+ }
632
+
633
+ /* How far the reader has come, drawn as the rail itself: the segment beside
634
+ every chapter already read is accent, the rest is the hairline it always
635
+ was, so the boundary between them is the reader's position in the course.
636
+ `data-covered` is set by the panel's own script; without it the rail is
637
+ simply the hairline throughout. */
638
+ .layout-navigation-content ul ul li {
639
+ border-inline-start-color: var(--sl-color-hairline-light);
640
+ transition: border-color 120ms ease;
641
+ }
642
+
643
+ .layout-navigation-content ul ul li[data-covered] {
644
+ border-inline-start-color: var(--sl-color-text-accent);
645
+ }
646
+
647
+ /* The contents rail has no such line of its own; covered headings give it
648
+ one, so a long page shows how much of itself is behind the reader. */
649
+ .layout-page-sidebar-content nav ul li {
650
+ border-inline-start: 2px solid transparent;
651
+ padding-inline-start: 0.35rem;
652
+ }
653
+
654
+ .layout-page-sidebar-content nav ul li[data-covered] {
655
+ border-inline-start-color: var(--sl-color-text-accent);
656
+ }
657
+
658
+ .layout-page-sidebar-content nav ul li[data-current] {
659
+ border-inline-start-color: var(--sl-color-text-accent);
660
+ }
661
+
662
+ @media (prefers-reduced-motion: reduce) {
663
+ .layout-navigation-content ul ul li {
664
+ transition: none;
665
+ }
666
+ }
667
+
436
668
  @media (min-width: 50rem) {
437
669
  :root {
438
670
  --layout-edge-rail-width: 2.5rem;
439
- --layout-edge-toggle-size: 2rem;
671
+ /* The collapse control is a rectangle docked into the corner the header
672
+ and the panel's separator line make, not a round button floating near
673
+ it. One gap sets both of those edges; the radius rounds only the
674
+ corner that is free of them. */
675
+ --layout-edge-toggle-width: 1.875rem;
676
+ --layout-edge-toggle-height: 1.125rem;
677
+ --layout-edge-toggle-gap: 2px;
678
+ --layout-edge-toggle-radius: 0.4375rem;
679
+ /* What a panel keeps clear at its top so the control never lands on the
680
+ first chapter's own expand caret, which sits at the same edge. */
681
+ --layout-edge-toggle-reserve: calc(
682
+ var(--layout-edge-toggle-height) + var(--layout-edge-toggle-gap) * 2
683
+ );
684
+ /* The navigation panel's width as it currently stands: the reader's own
685
+ if they resized it, the edge rail's while it is collapsed. The control
686
+ and the drag handle both dock against whichever is in force, so
687
+ neither needs a second rule for the collapsed state. */
688
+ --layout-navigation-width: var(--sl-sidebar-width);
689
+ /* How long the pointer rests on a collapsed rail before it peeks open.
690
+ Long enough that crossing the rail on the way somewhere else never
691
+ opens anything, short enough that meaning to look never feels like
692
+ waiting. `LayoutHeader.astro` reads this and arms the peek; the CSS
693
+ below stays keyed off `:hover`, with the armed attribute as its
694
+ gate. */
695
+ --layout-peek-delay: 40ms;
440
696
  }
441
697
 
442
698
  .layout-edge-toggle:not([hidden]) {
443
699
  display: grid;
444
700
  place-items: center;
445
- width: var(--layout-edge-toggle-size);
446
- height: var(--layout-edge-toggle-size);
701
+ width: var(--layout-edge-toggle-width);
702
+ height: var(--layout-edge-toggle-height);
447
703
  border: 1px solid var(--sl-color-gray-5);
448
- border-radius: 999px;
449
704
  padding: 0;
450
705
  color: var(--sl-color-text-accent);
451
706
  background: var(--sl-color-bg-nav);
452
707
  box-shadow: var(--sl-shadow-sm);
453
- font: inherit;
708
+ font-family: var(--sl-font-system-mono);
709
+ font-size: 0.68rem;
710
+ font-weight: 600;
454
711
  line-height: 1;
712
+ letter-spacing: -0.09em;
455
713
  cursor: pointer;
456
714
  }
457
715
 
458
- .layout-edge-toggle .layout-edge-icon {
716
+ .layout-edge-glyph {
459
717
  display: block;
460
- flex: none;
461
- width: 1rem;
462
- height: 1rem;
718
+ /* A box the width of the resting control, minus its border. Centred here
719
+ rather than by the button's own `place-items`, so that when the control
720
+ grows during a peek (below) the glyph's box can be pinned to the end
721
+ the pointer is on and the glyph itself does not move at all. */
722
+ width: calc(var(--layout-edge-toggle-width) - 2px);
723
+ /* The tracking above also trims the run's trailing side bearing; put that
724
+ much back so the three glyphs sit centred in that box. */
725
+ padding-inline-start: 0.09em;
726
+ text-align: center;
727
+ }
728
+
729
+ /* The glyphs point the way the panel travels, which mirrors with the page. */
730
+ [dir='rtl'] .layout-edge-glyph {
731
+ transform: scaleX(-1);
463
732
  }
464
733
 
465
734
  .layout-edge-toggle:hover {
@@ -472,17 +741,23 @@ body {
472
741
  outline-offset: 2px;
473
742
  }
474
743
 
475
- .layout-edge-toggle .layout-edge-icon-restore,
476
- .layout-edge-toggle[aria-expanded='false'] .layout-edge-icon-hide {
744
+ .layout-edge-toggle .layout-edge-glyph-restore,
745
+ .layout-edge-toggle[aria-expanded='false'] .layout-edge-glyph-hide {
477
746
  display: none;
478
747
  }
479
748
 
480
- .layout-edge-toggle[aria-expanded='false'] .layout-edge-icon-restore {
749
+ .layout-edge-toggle[aria-expanded='false'] .layout-edge-glyph-restore {
481
750
  display: block;
482
751
  }
483
752
 
484
753
  .layout-navigation-panel {
485
754
  min-height: auto;
755
+ /* Starlight's own `.sidebar-content` already pads 1rem above this panel,
756
+ so reserve only the rest of what the docked control covers. */
757
+ padding-block-start: max(
758
+ 0px,
759
+ calc(var(--layout-edge-toggle-reserve) - 1rem)
760
+ );
486
761
  }
487
762
 
488
763
  /* A compact chapter remains useful as a row of lesson icons. The
@@ -602,6 +877,29 @@ body {
602
877
  transform: translateY(-50%) translateX(0);
603
878
  }
604
879
 
880
+ /* The panel that holds this tooltip is a scroll container
881
+ (`.sidebar-pane` is `overflow-y: auto`, which forces `overflow-x` to
882
+ match), so an absolutely positioned child cannot leave it: a lesson
883
+ title longer than the panel is wide simply ended at the panel's edge.
884
+ While one is showing, `LayoutSidebar.astro` marks its link and writes the
885
+ anchor's own viewport coordinates here — `position: fixed` takes the
886
+ tooltip out of that scroll container entirely, and the explicit physical
887
+ insets replace the `inset-inline-start: calc(100% + …)` above, which
888
+ means something different once the containing block is the viewport. */
889
+ .layout-compact-sidebar
890
+ details[data-compact]
891
+ > ul:not(.top-level)
892
+ > li
893
+ > a[data-tooltip-open]
894
+ > span {
895
+ position: fixed;
896
+ z-index: calc(var(--sl-z-index-menu) + 2);
897
+ inset-inline: auto;
898
+ top: var(--layout-tooltip-y, 50%);
899
+ left: var(--layout-tooltip-x, 0px);
900
+ max-width: min(22rem, 45vw);
901
+ }
902
+
605
903
  .layout-compact-sidebar details[data-compact] > summary .caret {
606
904
  transform: rotateZ(0);
607
905
  }
@@ -614,17 +912,164 @@ body {
614
912
  transform: rotateZ(180deg);
615
913
  }
616
914
 
915
+ /* Compact mode turns every chapter into the same outlined page, so the
916
+ overview row would be indistinguishable from chapter one. Fill its sheets
917
+ and give it the accent: the part itself, not a page in it. */
918
+ .layout-compact-sidebar
919
+ details[data-compact]
920
+ > ul:not(.top-level)
921
+ > li
922
+ > a.explico-part-overview {
923
+ color: var(--sl-color-text-accent);
924
+ border-color: var(--sl-color-text-accent);
925
+ }
926
+
927
+ .layout-compact-sidebar
928
+ details[data-compact]
929
+ > ul:not(.top-level)
930
+ > li
931
+ > a.explico-part-overview::before {
932
+ background: currentColor;
933
+ }
934
+
935
+ /* On the part's own page the tile is already accent-filled, so filled
936
+ accent sheets on top of it are one green shape. Invert the glyph and
937
+ hollow it out: an outline reads at 28px where a fill does not, and it is
938
+ the same reversal Starlight gives a current chapter's text. */
939
+ .layout-compact-sidebar
940
+ details[data-compact]
941
+ > ul:not(.top-level)
942
+ > li
943
+ > a.explico-part-overview[aria-current='page'] {
944
+ color: var(--sl-color-text-invert);
945
+ border-color: var(--sl-color-text-accent);
946
+ }
947
+
948
+ .layout-compact-sidebar
949
+ details[data-compact]
950
+ > ul:not(.top-level)
951
+ > li
952
+ > a.explico-part-overview[aria-current='page']::before {
953
+ background: transparent;
954
+ }
955
+
956
+ /* Compact mode has no rail to colour, so a covered chapter's tile carries
957
+ the progress instead of the line beside it. */
958
+ .layout-compact-sidebar
959
+ details[data-compact]
960
+ > ul:not(.top-level)
961
+ > li[data-covered]
962
+ > a:not([aria-current='page']) {
963
+ border-color: var(--sl-color-accent-low);
964
+ background: var(--sl-color-accent-low);
965
+ }
966
+
617
967
  .layout-navigation-toggle {
618
968
  position: fixed;
619
969
  z-index: calc(var(--sl-z-index-menu) + 1);
620
- inset-block-start: calc(var(--sl-nav-height) + 0.75rem);
970
+ /* Both panels' controls take this same block start, so the pair reads as
971
+ one row under the header instead of two separately placed buttons. */
972
+ inset-block-start: calc(
973
+ var(--sl-nav-height) + var(--layout-edge-toggle-gap)
974
+ );
975
+ /* The 1px is the panel's own separator border: the gap is measured from
976
+ the line the reader sees, not from the border box behind it. */
621
977
  inset-inline-start: calc(
622
- var(--sl-sidebar-width) - var(--layout-edge-toggle-size) - 0.35rem
978
+ var(--layout-navigation-width) - var(--layout-edge-toggle-width) -
979
+ var(--layout-edge-toggle-gap) - 1px
623
980
  );
981
+ border-radius: 0 0 0 var(--layout-edge-toggle-radius);
982
+ }
983
+
984
+ [dir='rtl'] .layout-navigation-toggle {
985
+ border-radius: 0 0 var(--layout-edge-toggle-radius) 0;
986
+ }
987
+
988
+ /* The handle that sets the panel's width straddles that same separator. */
989
+ /* The handle covers the separator line and reaches outward from it, into
990
+ the article. It deliberately does not reach inward: the navigation panel
991
+ scrolls, and its scrollbar sits exactly there — a handle straddling the
992
+ line evenly would take the outer few pixels of that scrollbar with it. */
993
+ .layout-navigation-resizer {
994
+ position: fixed;
995
+ z-index: calc(var(--sl-z-index-menu) + 1);
996
+ inset-block: var(--sl-nav-height) 0;
997
+ inset-inline-start: calc(var(--layout-navigation-width) - 1px);
998
+ }
999
+
1000
+ .layout-edge-resizer:not([hidden]) {
1001
+ display: block;
1002
+ width: 0.375rem;
1003
+ border: 0;
1004
+ padding: 0;
1005
+ background: transparent;
1006
+ cursor: col-resize;
1007
+ touch-action: none;
1008
+ }
1009
+
1010
+ .layout-edge-resizer::after {
1011
+ content: '';
1012
+ display: block;
1013
+ position: absolute;
1014
+ inset-block: 0;
1015
+ width: 1px;
1016
+ background: transparent;
1017
+ transition: background 120ms ease;
1018
+ }
1019
+
1020
+ /* Land the lit line on the separator itself, at whichever end of the
1021
+ handle that panel's separator falls. */
1022
+ .layout-navigation-resizer::after {
1023
+ inset-inline-start: 0;
1024
+ }
1025
+
1026
+ .layout-contents-resizer::after {
1027
+ inset-inline-end: 0;
1028
+ }
1029
+
1030
+ .layout-edge-resizer:is(:hover, [data-resizing])::after {
1031
+ background: var(--sl-color-accent);
1032
+ }
1033
+
1034
+ .layout-edge-resizer:focus-visible {
1035
+ outline: 3px solid var(--sl-color-accent-high);
1036
+ outline-offset: -3px;
1037
+ }
1038
+
1039
+ /* A collapsed panel has no width for the reader to set; its control does
1040
+ that job instead. */
1041
+ html[data-navigation-hidden] .layout-navigation-resizer,
1042
+ html[data-page-sidebar-hidden] .layout-contents-resizer {
1043
+ display: none;
1044
+ }
1045
+
1046
+ /* A panel the reader narrowed past its own labels scrolls sideways rather
1047
+ than wrapping every chapter title into a column of single words. */
1048
+ html[data-navigation-resized] .layout-navigation-content :is(a, summary) {
1049
+ white-space: nowrap;
1050
+ }
1051
+
1052
+ html[data-navigation-resized] .sidebar-pane {
1053
+ overflow-x: auto;
1054
+ }
1055
+
1056
+ /* A compact chapter's tooltip is a child of one of those links, and it is
1057
+ the one label that must still wrap — it is sized by its own max-width,
1058
+ not by the panel, so inheriting `nowrap` would run its text straight out
1059
+ of its own background. */
1060
+ html[data-navigation-resized]
1061
+ .layout-compact-sidebar
1062
+ details[data-compact]
1063
+ > ul:not(.top-level)
1064
+ > li
1065
+ > a
1066
+ > span {
1067
+ white-space: normal;
624
1068
  }
625
1069
 
626
1070
  html[data-navigation-hidden] {
627
1071
  --sl-content-inline-start: var(--layout-edge-rail-width);
1072
+ --layout-navigation-width: var(--layout-edge-rail-width);
628
1073
  }
629
1074
 
630
1075
  html[data-navigation-hidden] .sidebar-pane {
@@ -633,12 +1078,6 @@ body {
633
1078
  transition: width 140ms ease;
634
1079
  }
635
1080
 
636
- html[data-navigation-hidden] .layout-navigation-toggle {
637
- inset-inline-start: calc(
638
- (var(--layout-edge-rail-width) - var(--layout-edge-toggle-size)) / 2
639
- );
640
- }
641
-
642
1081
  html[data-navigation-hidden] .layout-navigation-content {
643
1082
  visibility: hidden;
644
1083
  opacity: 0;
@@ -646,7 +1085,9 @@ body {
646
1085
  transition: opacity 100ms ease;
647
1086
  }
648
1087
 
649
- html[data-navigation-hidden]:not([data-navigation-peek-suppressed])
1088
+ html[data-navigation-hidden][data-navigation-peek-armed]:not(
1089
+ [data-navigation-peek-suppressed]
1090
+ )
650
1091
  .sidebar-pane:hover {
651
1092
  width: var(--sl-sidebar-width);
652
1093
  overflow-y: auto;
@@ -654,13 +1095,37 @@ body {
654
1095
  box-shadow: var(--sl-shadow-lg);
655
1096
  }
656
1097
 
657
- html[data-navigation-hidden]:not([data-navigation-peek-suppressed])
1098
+ html[data-navigation-hidden][data-navigation-peek-armed]:not(
1099
+ [data-navigation-peek-suppressed]
1100
+ )
658
1101
  .sidebar-pane:hover
659
1102
  .layout-navigation-content {
660
1103
  visibility: visible;
661
1104
  opacity: 1;
662
1105
  pointer-events: auto;
663
1106
  }
1107
+
1108
+ /* While the panel peeks open over the article, the control reaches across
1109
+ to the separator the open panel would put there — it does not travel to
1110
+ it. A reader aiming at the control is hovering the rail, which is what
1111
+ triggers the peek in the first place; moving the control at that moment
1112
+ takes it out from under the pointer that was coming for it, and the
1113
+ reader has to chase it across the panel. Growing instead keeps its outer
1114
+ edge, and the glyph inside it, exactly where the pointer found them,
1115
+ while its inner end still shows where the control will settle. The width
1116
+ is a length, like the panel's own, so the two animate in step. */
1117
+ html[data-navigation-hidden][data-navigation-peek-armed]:not(
1118
+ [data-navigation-peek-suppressed]
1119
+ )
1120
+ .sidebar-pane:hover
1121
+ .layout-navigation-toggle {
1122
+ place-items: center start;
1123
+ width: calc(
1124
+ var(--sl-sidebar-width) - var(--layout-edge-rail-width) +
1125
+ var(--layout-edge-toggle-width)
1126
+ );
1127
+ transition: width 140ms ease;
1128
+ }
664
1129
  }
665
1130
 
666
1131
  @media (min-width: 72rem) {
@@ -746,14 +1211,53 @@ body {
746
1211
  .layout-page-sidebar {
747
1212
  position: relative;
748
1213
  min-height: 100%;
749
- padding-block-start: 2.75rem;
1214
+ padding-block-start: var(--layout-edge-toggle-reserve);
750
1215
  }
751
1216
 
1217
+ /* `.right-sidebar` is `position: fixed` with `padding-top:
1218
+ var(--sl-nav-height)`, so this panel's own top edge is already the
1219
+ underside of the header: the same gap here puts this control at exactly
1220
+ the block start the navigation's fixed control computes. */
752
1221
  .layout-contents-toggle {
753
1222
  position: absolute;
754
1223
  z-index: 1;
755
- inset-block-start: 0.5rem;
756
- inset-inline-start: 0.35rem;
1224
+ inset-block-start: var(--layout-edge-toggle-gap);
1225
+ inset-inline-start: var(--layout-edge-toggle-gap);
1226
+ border-radius: 0 0 var(--layout-edge-toggle-radius) 0;
1227
+ }
1228
+
1229
+ [dir='rtl'] .layout-contents-toggle {
1230
+ border-radius: 0 0 0 var(--layout-edge-toggle-radius);
1231
+ }
1232
+
1233
+ .layout-contents-resizer {
1234
+ position: fixed;
1235
+ z-index: calc(var(--sl-z-index-toc) + 1);
1236
+ inset-block: var(--sl-nav-height) 0;
1237
+ inset-inline-end: calc(
1238
+ var(--layout-contents-width, var(--toc-actual-width, 0px)) - 1px
1239
+ );
1240
+ }
1241
+
1242
+ /* A width the reader set replaces the shrink-to-fit rail above. The
1243
+ `:not()` keeps this out of the collapsed state's way, which owns the
1244
+ same declaration at the same specificity. */
1245
+ html[data-contents-resized]:not([data-page-sidebar-hidden])
1246
+ .right-sidebar-container,
1247
+ html[data-contents-resized]:not([data-page-sidebar-hidden]) .right-sidebar {
1248
+ width: var(--layout-contents-width);
1249
+ max-width: none;
1250
+ }
1251
+
1252
+ /* Starlight hides this panel's scrollbar (`scrollbar-width: none`), which
1253
+ would quietly cut a heading the reader narrowed the panel past. */
1254
+ html[data-contents-resized]:not([data-page-sidebar-hidden]) .right-sidebar {
1255
+ overflow-x: auto;
1256
+ scrollbar-width: thin;
1257
+ }
1258
+
1259
+ html[data-contents-resized] .layout-page-sidebar-content :is(a, h2) {
1260
+ white-space: nowrap;
757
1261
  }
758
1262
 
759
1263
  html[data-page-sidebar-hidden] {
@@ -764,15 +1268,6 @@ body {
764
1268
  width: var(--layout-edge-rail-width);
765
1269
  }
766
1270
 
767
- html[data-page-sidebar-hidden] .layout-contents-toggle {
768
- position: fixed;
769
- inset-block-start: calc(var(--sl-nav-height) + 0.5rem);
770
- inset-inline-start: auto;
771
- inset-inline-end: calc(
772
- (var(--layout-edge-rail-width) - var(--layout-edge-toggle-size)) / 2
773
- );
774
- }
775
-
776
1271
  html[data-page-sidebar-hidden] .right-sidebar {
777
1272
  z-index: var(--sl-z-index-toc);
778
1273
  inset-inline-start: auto;
@@ -794,7 +1289,9 @@ body {
794
1289
  transition: opacity 100ms ease;
795
1290
  }
796
1291
 
797
- html[data-page-sidebar-hidden]:not([data-page-sidebar-peek-suppressed])
1292
+ html[data-page-sidebar-hidden][data-page-sidebar-peek-armed]:not(
1293
+ [data-page-sidebar-peek-suppressed]
1294
+ )
798
1295
  .right-sidebar:hover {
799
1296
  width: max-content;
800
1297
  max-width: 80ch;
@@ -802,13 +1299,32 @@ body {
802
1299
  box-shadow: var(--sl-shadow-lg);
803
1300
  }
804
1301
 
805
- html[data-page-sidebar-hidden]:not([data-page-sidebar-peek-suppressed])
1302
+ html[data-page-sidebar-hidden][data-page-sidebar-peek-armed]:not(
1303
+ [data-page-sidebar-peek-suppressed]
1304
+ )
806
1305
  .right-sidebar:hover
807
1306
  .layout-page-sidebar-content {
808
1307
  visibility: visible;
809
1308
  opacity: 1;
810
1309
  pointer-events: auto;
811
1310
  }
1311
+
1312
+ /* The same growth, mirrored. This control is positioned inside its panel,
1313
+ so a peek drags it inward with the panel's own edge unless its outer edge
1314
+ is pinned to where the rail put it; with both insets set, the box grows
1315
+ to meet the separator on its own. */
1316
+ html[data-page-sidebar-hidden][data-page-sidebar-peek-armed]:not(
1317
+ [data-page-sidebar-peek-suppressed]
1318
+ )
1319
+ .right-sidebar:hover
1320
+ .layout-contents-toggle {
1321
+ place-items: center end;
1322
+ inset-inline-end: calc(
1323
+ var(--layout-edge-rail-width) - var(--layout-edge-toggle-width) -
1324
+ var(--layout-edge-toggle-gap) - 1px
1325
+ );
1326
+ width: auto;
1327
+ }
812
1328
  }
813
1329
 
814
1330
  @media (prefers-reduced-motion: reduce) {
@@ -820,6 +1336,162 @@ body {
820
1336
  }
821
1337
  }
822
1338
 
1339
+ /* The side column's room (rule S3 of docs/plans/hover-card-consistency.md):
1340
+ while explanation cards sit to the right of the article, the article
1341
+ slides left through its own start margin, which `card-side.ts` computes
1342
+ and hands over as `--explico-article-start`. `.main-pane` keeps its width
1343
+ and the rail stays where it is. `:root` outranks the `html[…] .main-pane`
1344
+ rules above that centre the article; `screen` keeps the slide off paper.
1345
+ The slide's animation is a transform the card manager runs, so this
1346
+ margin is always the final one. */
1347
+ @media screen and (min-width: 72rem) {
1348
+ :root[data-article-slid] .main-pane {
1349
+ --sl-content-margin-inline: var(--explico-article-start, 0px) auto;
1350
+ }
1351
+ }
1352
+
1353
+ /* Both side panels sit above the explanation cards, whose layer takes the
1354
+ header's level just before `.page` (`card-placement.ts`): an open panel is
1355
+ never covered — a reader who needs its room collapses it — and a collapsed
1356
+ one peeks open over the cards. Each panel's controls live inside it, so
1357
+ they rise with it. The header outranks both: the rail's box starts at the
1358
+ window's top, under the header. An expanded table or diagram covers the
1359
+ panels, and the cards opened from inside it must cover it, so while one is
1360
+ open the panels drop back to Starlight's own levels. */
1361
+ .header {
1362
+ z-index: calc(var(--sl-z-index-navbar) + 1);
1363
+ }
1364
+
1365
+ /* A slimmer site header: 3rem at every width, where Starlight's is 3.5rem
1366
+ and 4rem from 50rem up. The title, the logo (sized from the height and
1367
+ padding) and the search box scale with it, so more of the window goes to
1368
+ the lesson, and to the cards and side column under the header.
1369
+ Everything that sits under the header reads `--sl-nav-height`. */
1370
+ :root {
1371
+ --sl-nav-height: 3rem;
1372
+ --sl-nav-pad-y: 0.5rem;
1373
+ }
1374
+
1375
+ .site-title {
1376
+ font-size: var(--sl-text-xl);
1377
+ }
1378
+
1379
+ @media (min-width: 50rem) {
1380
+ button[data-open-modal] {
1381
+ height: 2rem;
1382
+ }
1383
+ }
1384
+
1385
+ @media (min-width: 50rem) {
1386
+ :root .sidebar-pane {
1387
+ z-index: var(--sl-z-index-navbar);
1388
+ }
1389
+ }
1390
+
1391
+ @media (min-width: 72rem) {
1392
+ :root .right-sidebar,
1393
+ :root[data-page-sidebar-hidden] .right-sidebar {
1394
+ z-index: var(--sl-z-index-navbar);
1395
+ }
1396
+ }
1397
+
1398
+ :root:has(.table-overflow-expanded, .diagram-is-expanded) .sidebar-pane {
1399
+ z-index: var(--sl-z-index-menu);
1400
+ }
1401
+
1402
+ :root:has(.table-overflow-expanded, .diagram-is-expanded) .right-sidebar {
1403
+ z-index: var(--sl-z-index-toc);
1404
+ }
1405
+
1406
+ /* S9: "Keep the article left", fixed at the top of the side column just
1407
+ under the header, over the cards scrolling past it. `card-side.ts` sets
1408
+ its `left`. */
1409
+ .explico-keep-left {
1410
+ position: fixed;
1411
+ z-index: var(--sl-z-index-navbar);
1412
+ top: calc(var(--sl-nav-height) + 0.5rem);
1413
+ display: inline-flex;
1414
+ align-items: center;
1415
+ gap: 0.45rem;
1416
+ margin: 0;
1417
+ padding: 0.2rem 0.65rem 0.2rem 0.4rem;
1418
+ border: var(--ex-edge-border);
1419
+ border-radius: 999px;
1420
+ font: inherit;
1421
+ font-size: var(--sl-text-xs);
1422
+ line-height: 1.4;
1423
+ color: var(--sl-color-gray-2);
1424
+ background: var(--sl-color-bg);
1425
+ box-shadow: var(--sl-shadow-sm);
1426
+ cursor: pointer;
1427
+ }
1428
+
1429
+ .explico-keep-left[hidden] {
1430
+ display: none;
1431
+ }
1432
+
1433
+ .explico-keep-left:hover {
1434
+ color: var(--sl-color-text);
1435
+ }
1436
+
1437
+ .explico-keep-left:focus-visible {
1438
+ outline: 3px solid var(--sl-color-accent);
1439
+ outline-offset: 2px;
1440
+ }
1441
+
1442
+ .explico-keep-left-switch {
1443
+ position: relative;
1444
+ flex: none;
1445
+ width: 1.75em;
1446
+ height: 1em;
1447
+ border-radius: 999px;
1448
+ background: var(--sl-color-gray-4);
1449
+ transition: background-color 100ms ease;
1450
+ }
1451
+
1452
+ .explico-keep-left-switch::after {
1453
+ content: '';
1454
+ position: absolute;
1455
+ top: 0.125em;
1456
+ left: 0.125em;
1457
+ width: 0.75em;
1458
+ height: 0.75em;
1459
+ border-radius: 50%;
1460
+ background: var(--sl-color-bg);
1461
+ transition: transform 100ms ease;
1462
+ }
1463
+
1464
+ .explico-keep-left[aria-pressed='true'] .explico-keep-left-switch {
1465
+ background: var(--sl-color-accent);
1466
+ }
1467
+
1468
+ .explico-keep-left[aria-pressed='true'] .explico-keep-left-switch::after {
1469
+ transform: translateX(0.75em);
1470
+ }
1471
+
1472
+ @media (prefers-reduced-motion: reduce) {
1473
+ .explico-keep-left-switch,
1474
+ .explico-keep-left-switch::after {
1475
+ transition: none;
1476
+ }
1477
+ }
1478
+
1479
+ @media (forced-colors: active) {
1480
+ .explico-keep-left-switch {
1481
+ border: 1px solid ButtonText;
1482
+ }
1483
+
1484
+ .explico-keep-left[aria-pressed='true'] .explico-keep-left-switch {
1485
+ background: Highlight;
1486
+ }
1487
+ }
1488
+
1489
+ @media print {
1490
+ .explico-keep-left {
1491
+ display: none !important;
1492
+ }
1493
+ }
1494
+
823
1495
  /* Source citations: inline `[n]` markers and the end-of-page reference list
824
1496
  emitted by the remark-citation adapter. The hover/pin panel is styled in
825
1497
  the CitationLayer island. */
@@ -1270,61 +1942,217 @@ body {
1270
1942
  }
1271
1943
  }
1272
1944
 
1273
- .sl-markdown-content .keyed-equation:target {
1274
- animation: eq-flash 5s ease-out both;
1945
+ .sl-markdown-content .keyed-equation:target {
1946
+ animation: eq-flash 5s ease-out both;
1947
+ }
1948
+
1949
+ @media (prefers-reduced-motion: reduce) {
1950
+ .sl-markdown-content .keyed-equation:target {
1951
+ animation: none;
1952
+ background: var(--sl-color-accent-low);
1953
+ outline: 2px solid var(--sl-color-accent);
1954
+ outline-offset: 0.2rem;
1955
+ }
1956
+ }
1957
+
1958
+ @media (forced-colors: active) {
1959
+ .sl-markdown-content .keyed-equation:target {
1960
+ animation: none;
1961
+ outline: 2px solid Highlight;
1962
+ outline-offset: 0.2rem;
1963
+ }
1964
+ .sl-markdown-content a.equation-ref:hover,
1965
+ .sl-markdown-content a.equation-ref:focus-visible {
1966
+ outline: 2px solid Highlight;
1967
+ }
1968
+ }
1969
+
1970
+ @media print {
1971
+ .sl-markdown-content .keyed-equation__controls {
1972
+ position: static;
1973
+ display: block;
1974
+ transform: none;
1975
+ text-align: end;
1976
+ }
1977
+ .sl-markdown-content a.keyed-equation__number {
1978
+ color: inherit;
1979
+ opacity: 1;
1980
+ }
1981
+ /* A clipboard button means nothing on paper. */
1982
+ .sl-markdown-content .keyed-equation__copy {
1983
+ display: none;
1984
+ }
1985
+ .sl-markdown-content .keyed-equation {
1986
+ padding-inline-end: 0;
1987
+ }
1988
+ }
1989
+
1990
+ /* One caption line for every float (docs/plans/titles-and-containers.md
1991
+ Part 3): `Figure 2.2.1 [chain] ‹caption›` — the number links to the float,
1992
+ the chain icon copies that link (`HeadingAnchorEnhancer.astro`), the
1993
+ caption is the float's one long caption. A figure and a diagram put it
1994
+ below the picture (`FloatCaption.astro`), a table above it, the LaTeX
1995
+ convention. The line never hides (docs/plans/figures-and-short-captions.md,
1996
+ decision 1); its buttons sit at its end (`.float-tools`, below). */
1997
+ .sl-markdown-content .float-caption {
1998
+ /* Anchors the button row (`.float-tools`) to the line's end. */
1999
+ position: relative;
2000
+ margin-block: 0.5rem 0;
2001
+ font-size: var(--sl-text-sm);
2002
+ line-height: 1.5;
2003
+ color: var(--sl-color-gray-3);
2004
+ break-before: avoid-page;
2005
+ }
2006
+
2007
+ .sl-markdown-content .keyed-table > .float-caption {
2008
+ margin-block: 0 0.5rem;
2009
+ break-before: auto;
2010
+ break-after: avoid-page;
2011
+ }
2012
+
2013
+ /* A float's buttons sit together at the top right of its caption line, in
2014
+ one boxed style, always visible: a table's widen ↔ (the same action as
2015
+ the column at its right edge) and its fold ⌃ / ⌄, a diagram's widen ↔.
2016
+ The caption's text keeps clear of a shown one. A table's scripts add the
2017
+ row (`Table.astro`, `TableOverflowExpander.astro`), whichever runs first;
2018
+ a figure's or a diagram's is rendered by `FloatCaption.astro`. */
2019
+ .sl-markdown-content .float-tools {
2020
+ position: absolute;
2021
+ inset-block-start: 0;
2022
+ inset-inline-end: 0;
2023
+ display: inline-flex;
2024
+ gap: 0.25rem;
2025
+ }
2026
+
2027
+ .sl-markdown-content .float-caption:has(> .float-tools > .float-tool:not([hidden])) {
2028
+ padding-inline-end: 2.25rem;
2029
+ }
2030
+
2031
+ .sl-markdown-content
2032
+ .float-caption:has(> .float-tools > .float-tool:not([hidden]) ~ .float-tool:not([hidden])) {
2033
+ padding-inline-end: 4.25rem;
2034
+ }
2035
+
2036
+ .sl-markdown-content .float-tool {
2037
+ display: inline-grid;
2038
+ place-items: center;
2039
+ width: 1.75rem;
2040
+ height: 1.5rem;
2041
+ margin: 0;
2042
+ padding: 0;
2043
+ border: 1px solid var(--sl-color-gray-5);
2044
+ border-radius: 0.3rem;
2045
+ background: var(--sl-color-bg);
2046
+ color: var(--sl-color-gray-2);
2047
+ cursor: pointer;
2048
+ }
2049
+
2050
+ .sl-markdown-content .float-tool[hidden] {
2051
+ display: none;
2052
+ }
2053
+
2054
+ .sl-markdown-content .float-tool svg {
2055
+ width: 0.875rem;
2056
+ height: 0.875rem;
2057
+ fill: none;
2058
+ stroke: currentColor;
2059
+ stroke-width: 2.2;
2060
+ stroke-linecap: round;
2061
+ stroke-linejoin: round;
2062
+ }
2063
+
2064
+ .sl-markdown-content .float-tool:hover {
2065
+ color: var(--sl-color-text-accent);
2066
+ border-color: var(--sl-color-text-accent);
2067
+ }
2068
+
2069
+ .sl-markdown-content .float-tool:focus-visible {
2070
+ outline: 2px solid var(--sl-color-accent);
2071
+ outline-offset: 1px;
2072
+ }
2073
+
2074
+ /* The widen button's two faces: ↔ widens, →← brings the float back. */
2075
+ .sl-markdown-content .float-tool[aria-expanded='true'] .float-tool__widen,
2076
+ .sl-markdown-content .float-tool[aria-expanded='false'] .float-tool__narrow {
2077
+ display: none;
1275
2078
  }
1276
2079
 
1277
- @media (prefers-reduced-motion: reduce) {
1278
- .sl-markdown-content .keyed-equation:target {
1279
- animation: none;
1280
- background: var(--sl-color-accent-low);
1281
- outline: 2px solid var(--sl-color-accent);
1282
- outline-offset: 0.2rem;
1283
- }
2080
+ /* The fold button's two faces, for a lab's tools (`Figure.astro`): ⌃ folds
2081
+ them away, ⌄ brings them back. A table's toggle swaps its icon instead. */
2082
+ .sl-markdown-content .float-tool[aria-expanded='true'] .float-tool__unfold,
2083
+ .sl-markdown-content .float-tool[aria-expanded='false'] .float-tool__fold {
2084
+ display: none;
1284
2085
  }
1285
2086
 
1286
- @media (forced-colors: active) {
1287
- .sl-markdown-content .keyed-equation:target {
1288
- animation: none;
1289
- outline: 2px solid Highlight;
1290
- outline-offset: 0.2rem;
1291
- }
1292
- .sl-markdown-content a.equation-ref:hover,
1293
- .sl-markdown-content a.equation-ref:focus-visible {
1294
- outline: 2px solid Highlight;
1295
- }
2087
+ /* The caption line's live end: the case a lab's plot shows, after the long
2088
+ caption (`FloatCaption.astro`, `figure/figure-state.ts`). */
2089
+ .sl-markdown-content .float-caption__state:not(:empty) {
2090
+ margin-inline-start: 0.25em;
1296
2091
  }
1297
2092
 
1298
2093
  @media print {
1299
- .sl-markdown-content .keyed-equation__controls {
1300
- position: static;
1301
- display: block;
1302
- transform: none;
1303
- text-align: end;
1304
- }
1305
- .sl-markdown-content a.keyed-equation__number {
1306
- color: inherit;
1307
- opacity: 1;
1308
- }
1309
- /* A clipboard button means nothing on paper. */
1310
- .sl-markdown-content .keyed-equation__copy {
2094
+ .sl-markdown-content .float-tools {
1311
2095
  display: none;
1312
2096
  }
1313
- .sl-markdown-content .keyed-equation {
2097
+
2098
+ .sl-markdown-content .float-caption:has(> .float-tools) {
1314
2099
  padding-inline-end: 0;
1315
2100
  }
1316
2101
  }
1317
2102
 
1318
- /* The generic prose sibling-spacing rule must not insert a blank toolbar-like
1319
- row between a diagram title and its body. This comes after that generic rule
1320
- in the cascade: the expand control is overlaid on the title line, so the
1321
- canvas begins immediately below it. */
1322
- .sl-markdown-content .diagram__summary {
1323
- margin-block-end: 0;
2103
+ /* A short caption opens the line as its bold title, the journal-legend
2104
+ style: "**Sampled flight data.** Position, …" (`FloatCaption.astro`,
2105
+ `Table.astro`; docs/plans/figures-and-short-captions.md, decision 9.1). */
2106
+ .sl-markdown-content .float-caption__title {
2107
+ color: var(--sl-color-text);
2108
+ font-weight: 600;
1324
2109
  }
1325
2110
 
1326
- .sl-markdown-content .diagram__summary + .diagram__body {
1327
- margin-block-start: 0;
2111
+ .sl-markdown-content .float-caption__number {
2112
+ font-weight: 600;
2113
+ font-variant-numeric: tabular-nums;
2114
+ color: var(--sl-color-text-accent);
2115
+ text-decoration: none;
2116
+ }
2117
+
2118
+ .sl-markdown-content .float-caption__number:hover,
2119
+ .sl-markdown-content .float-caption__number:focus-visible {
2120
+ text-decoration: underline;
2121
+ }
2122
+
2123
+ .sl-markdown-content .float-caption__anchor {
2124
+ display: inline-flex;
2125
+ width: 1.15em;
2126
+ height: 1.15em;
2127
+ margin-inline: 0.15em 0.3em;
2128
+ vertical-align: -0.2em;
2129
+ align-items: center;
2130
+ justify-content: center;
2131
+ color: var(--sl-color-gray-4);
2132
+ text-decoration: none;
2133
+ -webkit-user-select: none;
2134
+ user-select: none;
2135
+ }
2136
+
2137
+ .sl-markdown-content .float-caption__anchor svg {
2138
+ width: 0.85em;
2139
+ height: 0.85em;
2140
+ }
2141
+
2142
+ .sl-markdown-content .float-caption__anchor:hover,
2143
+ .sl-markdown-content .float-caption__anchor:focus-visible {
2144
+ color: var(--sl-color-text-accent);
2145
+ }
2146
+
2147
+ .sl-markdown-content :is(.float-caption__number, .float-caption__anchor):focus-visible {
2148
+ outline: 2px solid var(--sl-color-accent);
2149
+ outline-offset: 0.15rem;
2150
+ }
2151
+
2152
+ @media print {
2153
+ .sl-markdown-content .float-caption__number {
2154
+ color: inherit;
2155
+ }
1328
2156
  }
1329
2157
 
1330
2158
  /* `<Table>`/`<Figure>`/`<Diagram>` keyed floats — the same deep-link
@@ -1346,12 +2174,36 @@ body {
1346
2174
  white-space: nowrap;
1347
2175
  }
1348
2176
 
2177
+ /* `[[sec-…]]` reads the same, but may print a section's title rather than
2178
+ `§ 1.1.2` (an unnumbered heading, or a course that hides section numbers),
2179
+ so it wraps like prose; the no-break space inside `§ 1.1.2` keeps that one
2180
+ whole. `[[check-…]]` and `[[ex-…]]` follow the section rules too
2181
+ (docs/plans/titles-and-containers.md Q11): a course that hides section
2182
+ numbers gets "Example “‹title›”". */
2183
+ .sl-markdown-content a.section-ref,
2184
+ .sl-markdown-content a.knowledge-check-ref,
2185
+ .sl-markdown-content a.example-ref {
2186
+ font-variant-numeric: tabular-nums;
2187
+ font-weight: 600;
2188
+ text-decoration: underline;
2189
+ text-underline-offset: 0.15em;
2190
+ padding-inline: 0.1em;
2191
+ border-radius: 0.2em;
2192
+ color: var(--sl-color-text-accent);
2193
+ }
2194
+
1349
2195
  .sl-markdown-content a.table-ref:hover,
1350
2196
  .sl-markdown-content a.table-ref:focus-visible,
1351
2197
  .sl-markdown-content a.figure-ref:hover,
1352
2198
  .sl-markdown-content a.figure-ref:focus-visible,
1353
2199
  .sl-markdown-content a.diagram-ref:hover,
1354
- .sl-markdown-content a.diagram-ref:focus-visible {
2200
+ .sl-markdown-content a.diagram-ref:focus-visible,
2201
+ .sl-markdown-content a.section-ref:hover,
2202
+ .sl-markdown-content a.section-ref:focus-visible,
2203
+ .sl-markdown-content a.knowledge-check-ref:hover,
2204
+ .sl-markdown-content a.knowledge-check-ref:focus-visible,
2205
+ .sl-markdown-content a.example-ref:hover,
2206
+ .sl-markdown-content a.example-ref:focus-visible {
1355
2207
  background: var(--sl-color-accent-low);
1356
2208
  }
1357
2209
 
@@ -1373,13 +2225,34 @@ body {
1373
2225
  }
1374
2226
  }
1375
2227
 
2228
+ /* A reference whose target sits inside a closed memo or section opens a
2229
+ preview card, as if the target were off screen; while that card is open,
2230
+ the fold's bar is marked the same way, so the reader sees where the
2231
+ target lives (`FloatRefLayer.astro`). */
2232
+ .sl-markdown-content .float-ref-fold-hint {
2233
+ background: var(--sl-color-accent-low);
2234
+ outline: 2px solid var(--sl-color-accent);
2235
+ outline-offset: 0.2rem;
2236
+ border-radius: 0.3rem;
2237
+ transition:
2238
+ background 120ms ease-out,
2239
+ outline-color 120ms ease-out;
2240
+ }
2241
+
2242
+ @media (forced-colors: active) {
2243
+ .sl-markdown-content .float-ref-fold-hint {
2244
+ outline-color: Highlight;
2245
+ }
2246
+ }
2247
+
1376
2248
  /* A referencing `[[tab-key]]`/`[[fig-key]]` link elsewhere in the prose was
1377
2249
  hovered/focused and the float it points to is already fully on screen:
1378
2250
  highlight it in place instead of opening a preview card
1379
2251
  (`FloatRefLayer.astro` decides which) — mirrors `.eq-ref-highlight`. */
1380
2252
  .sl-markdown-content .keyed-table.tab-ref-highlight,
1381
2253
  .sl-markdown-content .keyed-figure.fig-ref-highlight,
1382
- .sl-markdown-content .diagram.dia-ref-highlight {
2254
+ .sl-markdown-content .diagram.dia-ref-highlight,
2255
+ .sl-markdown-content :is(h2, h3, h4, h5, h6).sec-ref-highlight {
1383
2256
  background: var(--sl-color-accent-low);
1384
2257
  outline: 2px solid var(--sl-color-accent);
1385
2258
  outline-offset: 0.2rem;
@@ -1395,7 +2268,8 @@ body {
1395
2268
  .sl-markdown-content .diagram:target,
1396
2269
  .sl-markdown-content .keyed-table.tab-ref-highlight,
1397
2270
  .sl-markdown-content .keyed-figure.fig-ref-highlight,
1398
- .sl-markdown-content .diagram.dia-ref-highlight {
2271
+ .sl-markdown-content .diagram.dia-ref-highlight,
2272
+ .sl-markdown-content :is(h2, h3, h4, h5, h6).sec-ref-highlight {
1399
2273
  animation: none;
1400
2274
  outline: 2px solid Highlight;
1401
2275
  outline-offset: 0.2rem;
@@ -1405,7 +2279,13 @@ body {
1405
2279
  .sl-markdown-content a.figure-ref:hover,
1406
2280
  .sl-markdown-content a.figure-ref:focus-visible,
1407
2281
  .sl-markdown-content a.diagram-ref:hover,
1408
- .sl-markdown-content a.diagram-ref:focus-visible {
2282
+ .sl-markdown-content a.diagram-ref:focus-visible,
2283
+ .sl-markdown-content a.section-ref:hover,
2284
+ .sl-markdown-content a.section-ref:focus-visible,
2285
+ .sl-markdown-content a.knowledge-check-ref:hover,
2286
+ .sl-markdown-content a.knowledge-check-ref:focus-visible,
2287
+ .sl-markdown-content a.example-ref:hover,
2288
+ .sl-markdown-content a.example-ref:focus-visible {
1409
2289
  outline: 2px solid Highlight;
1410
2290
  }
1411
2291
  }
@@ -1463,6 +2343,197 @@ body {
1463
2343
  }
1464
2344
  }
1465
2345
 
2346
+ /* Outline numbers (docs/plans/titles-and-containers.md Part 1). A lesson's
2347
+ numbered heading carries `data-sec-number="1.1.2"` (`remark-notation.mjs`,
2348
+ from the manifest), printed here ahead of the title as generated content —
2349
+ so the heading's own text, its anchor and the "On this page" entry stay the
2350
+ title alone. The number shows on a wide screen and in print; on a narrow one
2351
+ only on a collapsed section's bar, which says what is folded away, and in
2352
+ the chain icon's "Link copied · § 1.1.2". The en space is part of the
2353
+ content, not a margin, so a screen reader hears "1.1.2 Title", not
2354
+ "1.1.2Title". */
2355
+ .sl-markdown-content [data-sec-number]::before {
2356
+ content: attr(data-sec-number) '\2002';
2357
+ color: var(--sl-color-text-accent);
2358
+ font-variant-numeric: tabular-nums;
2359
+ }
2360
+
2361
+ @media screen and (width < 50em) {
2362
+ .sl-markdown-content [data-sec-number]::before {
2363
+ content: none;
2364
+ }
2365
+
2366
+ .sl-markdown-content
2367
+ details.sl-section-collapsible:not([data-state='open'])
2368
+ > .sl-section-summary
2369
+ > [data-sec-number]::before {
2370
+ content: attr(data-sec-number) '\2002';
2371
+ }
2372
+ }
2373
+
2374
+ @media print {
2375
+ .sl-markdown-content [data-sec-number]::before {
2376
+ color: inherit;
2377
+ }
2378
+ }
2379
+
2380
+ /* Kind badges (Part 6): the `<h2>` a knowledge check and a worked-example set
2381
+ draw (`BlockHeading.astro`). The kind's icon leads the author's title, as
2382
+ it does in "On this page"; the kind's word and the block's number are a
2383
+ small label in the card's top-right corner, inside its top padding. The
2384
+ label is out of the text flow, so the title keeps the whole line at every
2385
+ width and still wraps word by word, and the number reads as the block's
2386
+ label rather than as a section number. `data-labels="icon"` keeps the word
2387
+ for a screen reader only (`sr-only`). The card's padding is set for it
2388
+ below; `--block-label-lift` is how far the label sits above the title's
2389
+ line box, measured from the card's inner top edge. */
2390
+ .sl-markdown-content .ex-section-card:has(> .block-heading-wrapper) {
2391
+ --block-label-lift: 1.25rem;
2392
+ padding-block-start: var(--block-label-lift);
2393
+ }
2394
+
2395
+ .sl-markdown-content .block-heading-wrapper {
2396
+ position: relative;
2397
+ }
2398
+
2399
+ .sl-markdown-content .block-heading {
2400
+ /* A wrapped title's later lines start under its first word, not under
2401
+ the icon. */
2402
+ padding-inline-start: 1.1em;
2403
+ text-indent: -1.1em;
2404
+ }
2405
+
2406
+ .sl-markdown-content .block-heading__icon {
2407
+ display: inline-block;
2408
+ width: 0.8em;
2409
+ height: 0.8em;
2410
+ margin-inline-end: 0.3em;
2411
+ vertical-align: -0.06em;
2412
+ color: var(--sl-color-text-accent);
2413
+ }
2414
+
2415
+ .sl-markdown-content .block-badge {
2416
+ position: absolute;
2417
+ inset-block-start: calc(0.3rem - var(--block-label-lift, 1.25rem));
2418
+ inset-inline-end: 0;
2419
+ color: var(--sl-color-text-accent);
2420
+ font-size: 0.75rem;
2421
+ font-weight: 600;
2422
+ letter-spacing: 0.05em;
2423
+ line-height: 1.2;
2424
+ text-indent: 0;
2425
+ text-transform: uppercase;
2426
+ white-space: nowrap;
2427
+ }
2428
+
2429
+ /* Narrow and touch screens set the heading inline beside the section's
2430
+ chevron, with the summary's own hanging indent (below); the heading's
2431
+ indent above would double it. */
2432
+ @media screen and (width < 50em), screen and (hover: none) {
2433
+ .sl-markdown-content .block-heading {
2434
+ padding-inline-start: 0;
2435
+ text-indent: 0;
2436
+ }
2437
+ }
2438
+
2439
+ /* The number follows the section numbers' rules above (Q11): shown on a wide
2440
+ screen and in print; on a narrow one only on a folded bar, and in the chain
2441
+ icon's "Link copied · Example 1.1.1". Real text, so the heading's name reads
2442
+ "Example 1.1.1 ‹title›" wherever it shows. */
2443
+ .sl-markdown-content .block-heading__number {
2444
+ color: var(--sl-color-text-accent);
2445
+ font-variant-numeric: tabular-nums;
2446
+ }
2447
+
2448
+ @media screen and (width < 50em) {
2449
+ .sl-markdown-content .block-heading__number {
2450
+ display: none;
2451
+ }
2452
+
2453
+ .sl-markdown-content
2454
+ details.sl-section-collapsible:not([data-state='open'])
2455
+ > .sl-section-summary
2456
+ > .block-heading
2457
+ .block-heading__number {
2458
+ display: inline;
2459
+ }
2460
+ }
2461
+
2462
+ @media print {
2463
+ .sl-markdown-content .block-heading__number {
2464
+ color: inherit;
2465
+ }
2466
+ }
2467
+
2468
+ /* "On this page" lists a check and an example set by its title alone, as it
2469
+ lists a section; the badge's icon before the title says which kind it is
2470
+ (`LayoutPageSidebar.astro` marks the link `data-block-kind`). A mask, so
2471
+ the icon takes the link's colour, current and hovered alike. */
2472
+ :is(starlight-toc, mobile-starlight-toc) a[data-block-kind]::before {
2473
+ content: '';
2474
+ display: inline-block;
2475
+ width: 0.95em;
2476
+ height: 0.95em;
2477
+ margin-inline-end: 0.35em;
2478
+ vertical-align: -0.12em;
2479
+ background-color: currentColor;
2480
+ mask: var(--block-kind-icon) center / contain no-repeat;
2481
+ }
2482
+
2483
+ :is(starlight-toc, mobile-starlight-toc) a[data-block-kind='check'] {
2484
+ --block-kind-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 8.5l4 4 8-9'/%3E%3C/svg%3E");
2485
+ }
2486
+
2487
+ :is(starlight-toc, mobile-starlight-toc) a[data-block-kind='example'] {
2488
+ --block-kind-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round'%3E%3Crect x='2' y='2' width='12' height='12' rx='2'/%3E%3Cpath d='M5 8h6M8 5v6'/%3E%3C/svg%3E");
2489
+ }
2490
+
2491
+ /* "Link copied · § 1.1.2" — `HeadingAnchorEnhancer.astro` places it under the
2492
+ title in page coordinates, clamped to the text column, and removes it
2493
+ after a moment; the same accent chip as an equation's "Copied!", with an
2494
+ edge and a shadow because the section behind it is flashing the same
2495
+ accent tint. */
2496
+ .sl-anchor-copied {
2497
+ position: absolute;
2498
+ z-index: 30;
2499
+ padding: 0.15rem 0.5rem;
2500
+ border: 1px solid var(--sl-color-accent);
2501
+ border-radius: 0.35rem;
2502
+ box-shadow: 0 0.2rem 0.6rem rgba(0, 0, 0, 0.15);
2503
+ background: var(--sl-color-accent-low);
2504
+ color: var(--sl-color-text-accent);
2505
+ font-size: var(--sl-text-xs);
2506
+ line-height: 1.5;
2507
+ /* One line for a short name; a long title wraps inside the cap, never
2508
+ wider than the column (the script keeps it inside the column). */
2509
+ width: max-content;
2510
+ max-width: min(28rem, calc(100vw - 2rem));
2511
+ pointer-events: none;
2512
+ animation: sl-anchor-copied 1.8s ease both;
2513
+ }
2514
+
2515
+ @keyframes sl-anchor-copied {
2516
+ 0%,
2517
+ 75% {
2518
+ opacity: 1;
2519
+ }
2520
+ 100% {
2521
+ opacity: 0;
2522
+ }
2523
+ }
2524
+
2525
+ @media (prefers-reduced-motion: reduce) {
2526
+ .sl-anchor-copied {
2527
+ animation: none;
2528
+ }
2529
+ }
2530
+
2531
+ @media print {
2532
+ .sl-anchor-copied {
2533
+ display: none;
2534
+ }
2535
+ }
2536
+
1466
2537
  /* Collapsible sections (docs/plans/collapsible-disclosure.md Part 3),
1467
2538
  wrapped at runtime by `CollapsibleSections.astro` around headings an
1468
2539
  author opted in with `{collapsible}`. Starlight's own `markdown.css` gives
@@ -1535,13 +2606,21 @@ body {
1535
2606
  never satisfies this selector. Both rules are unlayered, so they win over
1536
2607
  `anchor-links.css`'s `@layer starlight.content` version regardless of
1537
2608
  specificity — this replaces that default behavior for sections rather
1538
- than adding to it. */
2609
+ than adding to it. Like Starlight's own rule, the icon hides only where a
2610
+ hover can reveal it: on a touch screen it stays visible, since it is a
2611
+ real tap target at the end of the title line either way (an invisible one
2612
+ used to take a tap meant for the title and change the URL instead). */
1539
2613
  .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar > .sl-anchor-link {
1540
2614
  position: absolute;
1541
2615
  top: 0.2em;
1542
2616
  inset-inline-end: 0;
1543
2617
  margin-inline-start: 0;
1544
- opacity: 0;
2618
+ }
2619
+
2620
+ @media (hover: hover) {
2621
+ .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar > .sl-anchor-link {
2622
+ opacity: 0;
2623
+ }
1545
2624
  }
1546
2625
 
1547
2626
  .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar:has(> .sl-section-collapsible > summary:hover)
@@ -1553,24 +2632,72 @@ body {
1553
2632
  opacity: 1;
1554
2633
  }
1555
2634
 
2635
+ /* A narrow screen shows no section number and, often, has no hover to reveal
2636
+ the chain icon — yet the icon is the way to the section's link and, in its
2637
+ "Link copied · § 1.1.2", its number (docs/plans/titles-and-containers.md
2638
+ Q3). So it is always shown there, smaller and in the muted text colour.
2639
+ The size variable also sets the room `anchor-links.css` reserves for it at
2640
+ the end of the title. After the hover rules above, which it overrides. */
2641
+ @media screen and (width < 50em) {
2642
+ .sl-markdown-content .sl-heading-wrapper {
2643
+ --sl-anchor-icon-size: 0.7em;
2644
+ }
2645
+
2646
+ .sl-markdown-content .sl-heading-wrapper > .sl-anchor-link,
2647
+ .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar > .sl-anchor-link {
2648
+ opacity: 1;
2649
+ color: var(--sl-color-gray-3);
2650
+ }
2651
+ }
2652
+
1556
2653
  /* The chevron sits to the *left* of the title without shifting the title's
1557
2654
  own position: `markdown.css`'s default marker is inline, right before the
1558
2655
  text, pushed left slightly by a negative margin — meaning the title
1559
2656
  itself sits further right than an ordinary (non-collapsible) heading.
1560
2657
  Taking the marker fully out of flow with `position: absolute` and a
1561
2658
  negative `left` puts it in the gutter instead, so `.sl-section-summary`'s
1562
- own text always starts at the same position a plain heading's would. */
2659
+ own text always starts at the same position a plain heading's would.
2660
+ That gutter is only `--sl-content-pad-x` wide (16–24px) and ends at the
2661
+ sidebar's border, so the marker is centred in it — a fixed `-1.6em` once
2662
+ put the glyph right on that border line. Inside a `[card]` section the
2663
+ gutter is the card's own padding (`--ex-section-gutter`, `surfaces.css`).
2664
+ Narrow and touch screens have no usable gutter at all; the marker moves
2665
+ into the title line there (below). */
1563
2666
  .sl-markdown-content .sl-section-summary {
1564
2667
  position: relative;
1565
- /* Room for the absolutely-positioned anchor icon at the end of the line,
1566
- same reasoning as `.keyed-figure__summary` in `Figure.astro`. */
2668
+ /* Room for the absolutely-positioned anchor icon at the end of the line. */
1567
2669
  padding-inline-end: 1.5em;
2670
+ /* The title's own font size, so the gutter chevron can centre on the
2671
+ title's first line — not on the whole summary, which also holds the
2672
+ "Hidden here" line while closed. */
2673
+ --sl-section-title-size: var(--sl-text-h2);
2674
+ }
2675
+
2676
+ .sl-markdown-content .sl-section-summary:has(> h3) {
2677
+ --sl-section-title-size: var(--sl-text-h3);
2678
+ }
2679
+
2680
+ .sl-markdown-content .sl-section-summary:has(> h4) {
2681
+ --sl-section-title-size: var(--sl-text-h4);
2682
+ }
2683
+
2684
+ .sl-markdown-content .sl-section-summary:has(> h5) {
2685
+ --sl-section-title-size: var(--sl-text-h5);
2686
+ }
2687
+
2688
+ .sl-markdown-content .sl-section-summary:has(> h6) {
2689
+ --sl-section-title-size: var(--sl-text-h6, var(--sl-text-base));
1568
2690
  }
1569
2691
 
1570
2692
  .sl-markdown-content .sl-section-summary::before {
1571
2693
  position: absolute;
1572
- left: -1.6em;
1573
- top: 50%;
2694
+ /* `<summary>` starts 0.5rem left of the text (`markdown.css`), so this
2695
+ centres the marker box on the middle of the gutter. */
2696
+ left: calc(
2697
+ 0.5rem - var(--ex-section-gutter, var(--sl-content-pad-x, 1rem)) / 2 -
2698
+ var(--sl-details-marker-size, 1.25rem) / 2
2699
+ );
2700
+ top: calc(var(--sl-section-title-size) * var(--sl-line-height-headings) / 2);
1574
2701
  margin: 0; /* cancel `markdown.css`'s inline-flow margin trick */
1575
2702
  transform: translateY(-50%);
1576
2703
  opacity: 0;
@@ -1602,6 +2729,109 @@ body {
1602
2729
  transition: opacity 100ms ease;
1603
2730
  }
1604
2731
 
2732
+ /* A closed section's cues beyond the chevron — the two `aria-hidden` spans
2733
+ `CollapsibleSections.astro` appends to `<summary>` after the heading.
2734
+ `.sl-section-hint` ("(click to expand)", "(tap to expand)" on touch) is
2735
+ for narrow/touch screens only (enabled in the block below);
2736
+ `.sl-section-stub` ("Hidden here: 2 paragraphs, 1 equation") shows on
2737
+ every screen. Both vanish once the section is `open` — the stub by
2738
+ collapsing its grid row, so a peek grown into a real expand slides the
2739
+ body up smoothly instead of jumping by a line. */
2740
+ .sl-markdown-content .sl-section-hint {
2741
+ display: none;
2742
+ margin-inline-start: 0.4em;
2743
+ font-size: var(--sl-text-sm);
2744
+ font-weight: 400;
2745
+ color: var(--sl-color-gray-3);
2746
+ white-space: nowrap;
2747
+ }
2748
+
2749
+ .sl-markdown-content .sl-section-hint::before {
2750
+ content: '(click to expand)';
2751
+ }
2752
+
2753
+ @media (hover: none) {
2754
+ .sl-markdown-content .sl-section-hint::before {
2755
+ content: '(tap to expand)';
2756
+ }
2757
+ }
2758
+
2759
+ .sl-markdown-content .sl-section-stub {
2760
+ display: grid;
2761
+ grid-template-rows: 0fr;
2762
+ opacity: 0;
2763
+ margin-block-start: 0;
2764
+ padding-block-end: 0;
2765
+ border-block-end: 0 dashed var(--sl-color-gray-5);
2766
+ text-indent: 0; /* the narrow-screen hanging indent is for the title only */
2767
+ font-size: var(--sl-text-sm);
2768
+ font-weight: 400;
2769
+ line-height: 1.5;
2770
+ color: var(--sl-color-gray-3);
2771
+ transition:
2772
+ grid-template-rows 220ms ease,
2773
+ opacity 150ms ease,
2774
+ margin-block-start 220ms ease,
2775
+ padding-block-end 220ms ease,
2776
+ border-block-end-width 220ms ease;
2777
+ }
2778
+
2779
+ .sl-markdown-content .sl-section-stub > span {
2780
+ overflow: hidden;
2781
+ min-height: 0;
2782
+ }
2783
+
2784
+ .sl-markdown-content details.sl-section-collapsible:not([data-state='open'])
2785
+ > .sl-section-summary
2786
+ > .sl-section-stub {
2787
+ grid-template-rows: 1fr;
2788
+ opacity: 1;
2789
+ margin-block-start: 0.3rem;
2790
+ padding-block-end: 0.45rem;
2791
+ border-block-end-width: 1px;
2792
+ }
2793
+
2794
+ /* Narrow and touch screens: there is no gutter to hold the chevron (on a
2795
+ phone it used to render off-screen), and no hover to reveal it on an open
2796
+ section. It moves into the title line and stays visible in both states —
2797
+ `›` closed, `⌄` open, so it also tells a reader the section can fold —
2798
+ with a hanging indent that keeps a wrapped title's later lines under its
2799
+ text, not under the chevron. The room reserved is the marker's own box
2800
+ minus its negative start margin plus its end gap (`markdown.css`).
2801
+ `screen` only: a printer reports `hover: none` too, and an inline heading
2802
+ would lose the print stylesheet's `break-after: avoid`. */
2803
+ @media screen and (width < 50em), screen and (hover: none) {
2804
+ .sl-markdown-content .sl-section-summary {
2805
+ --sl-section-marker-room: calc(
2806
+ var(--sl-details-marker-size, 1.25rem) * 0.75 + 0.25rem
2807
+ );
2808
+ padding-inline-start: calc(0.5rem + var(--sl-section-marker-room));
2809
+ text-indent: calc(-1 * var(--sl-section-marker-room));
2810
+ }
2811
+
2812
+ .sl-markdown-content .sl-section-summary > :is(h1, h2, h3, h4, h5, h6) {
2813
+ display: inline;
2814
+ }
2815
+
2816
+ .sl-markdown-content .sl-section-summary::before {
2817
+ position: static;
2818
+ vertical-align: -0.15em;
2819
+ transform: none;
2820
+ opacity: 1;
2821
+ transition: transform 150ms ease;
2822
+ }
2823
+
2824
+ .sl-markdown-content details.sl-section-collapsible[open] > .sl-section-summary::before {
2825
+ transform: rotateZ(90deg);
2826
+ }
2827
+
2828
+ .sl-markdown-content details.sl-section-collapsible:not([data-state='open'])
2829
+ > .sl-section-summary
2830
+ > .sl-section-hint {
2831
+ display: inline;
2832
+ }
2833
+ }
2834
+
1605
2835
  /* The section body: normally native `<details>` hiding/showing is all that's
1606
2836
  needed (nothing renders while closed), but `peeking` (a collapsed section
1607
2837
  hovered/focused — see `CollapsibleSections.astro`) has to force the
@@ -1650,13 +2880,16 @@ body {
1650
2880
 
1651
2881
  @media (prefers-reduced-motion: reduce) {
1652
2882
  .sl-markdown-content .sl-section-summary::before,
1653
- .sl-markdown-content .sl-section-body {
2883
+ .sl-markdown-content .sl-section-body,
2884
+ .sl-markdown-content .sl-section-stub {
1654
2885
  transition: none;
1655
2886
  }
1656
2887
  }
1657
2888
 
1658
2889
  @media print {
1659
- .sl-markdown-content .sl-section-summary::before {
2890
+ .sl-markdown-content .sl-section-summary::before,
2891
+ .sl-markdown-content .sl-section-hint,
2892
+ .sl-markdown-content .sl-section-stub {
1660
2893
  display: none !important;
1661
2894
  }
1662
2895
 
@@ -1664,14 +2897,13 @@ body {
1664
2897
  display: none;
1665
2898
  }
1666
2899
 
1667
- /* Forcing every section open for print is NOT done here: the HTML spec
1668
- has the browser skip rendering a closed <details>'s non-summary
1669
- children outright — a rule no author CSS can override (`display`,
1670
- `content-visibility`, `!important` included). Only the `open`
1671
- *attribute* controls it, so `CollapsibleSections.astro` forces every
1672
- `.sl-section-collapsible` open (and `data-state="open"`) via a
1673
- `beforeprint`/`matchMedia('print')` listener instead, restoring on
1674
- `afterprint`. */
2900
+ /* Print follows the screen, and CSS couldn't do otherwise anyway: the
2901
+ HTML spec has the browser skip rendering a closed <details>'s
2902
+ non-summary children outright — a rule no author CSS can override
2903
+ (`display`, `content-visibility`, `!important` included). What JS does
2904
+ at print time (`src/components/disclosure/print-as-shown.ts`) is settle
2905
+ a peeking or mid-animation section to the state it reads as, and add a
2906
+ "(content collapsed …)" note after each closed one. */
1675
2907
  }
1676
2908
 
1677
2909
  /* Table column narrowing + expand-on-overflow
@@ -1702,10 +2934,10 @@ body {
1702
2934
  `TableOverflowExpander.astro`'s script, and to every table on the site,
1703
2935
  not just ones wide enough to overflow. `color-mix` against `--sl-color-bg`
1704
2936
  (not a fixed gray token) keeps the stripe theme-correct automatically —
1705
- the same technique `.lab-shell`'s background above uses, and for the same
1706
- reason: a token like the old `--sl-color-gray-7` is only defined for one
1707
- theme. The hover rule is unlayered and comes after the stripe rule, so it
1708
- wins over an even/odd row's own stripe regardless of specificity. */
2937
+ the same technique `surfaces.css`'s accent tint uses, and for the same
2938
+ reason: a token like `--sl-color-gray-7` is only defined for one theme.
2939
+ The hover rule is unlayered and comes after the stripe rule, so it wins
2940
+ over an even/odd row's own stripe regardless of specificity. */
1709
2941
  .sl-markdown-content table:not(:where(.not-content *)) tbody tr:nth-child(even) {
1710
2942
  background: color-mix(in srgb, var(--sl-color-bg) 85%, var(--sl-color-gray-5) 15%);
1711
2943
  }
@@ -1731,6 +2963,19 @@ body {
1731
2963
  flex: 1 1 auto;
1732
2964
  min-width: 0;
1733
2965
  overflow-x: auto;
2966
+ /* A size container, so a row spanning the whole table (the "… N more
2967
+ rows …" separator, `Table.astro`) can size its label to the part of
2968
+ the table on screen (`100cqi`), not to its whole scrolled width. It
2969
+ takes its width from the flex row, never from its content, so the
2970
+ inline-size containment this implies changes nothing else. */
2971
+ container-type: inline-size;
2972
+ }
2973
+
2974
+ /* A preview card sizes itself to its content, and a size container has no
2975
+ width of its own to give it: in a card the table's width sizes the card,
2976
+ as it always did (`FloatRefLayer.astro`). */
2977
+ .hover-panel .table-overflow-scroll {
2978
+ container-type: normal;
1734
2979
  }
1735
2980
 
1736
2981
  /* A scrollable region must be reachable by keyboard (WCAG 2.1.1) — the
@@ -1898,6 +3143,7 @@ body {
1898
3143
  .right-sidebar-container,
1899
3144
  .pagination-links,
1900
3145
  .layout-edge-toggle,
3146
+ .layout-edge-resizer,
1901
3147
  [data-explico-export-chrome],
1902
3148
  [data-popup-root],
1903
3149
  [data-citation-panel],
@@ -1909,11 +3155,8 @@ body {
1909
3155
  .table-overflow-expand-btn,
1910
3156
  .keyed-table__pagination-links,
1911
3157
  .keyed-table__more-row,
1912
- .diagram-toolbar,
1913
3158
  .sl-anchor-link,
1914
- .keyed-figure__title-anchor,
1915
- .diagram__title-anchor,
1916
- .memo-block__title-anchor,
3159
+ .float-caption__anchor,
1917
3160
  .short-note__collapse,
1918
3161
  .assessment-actions,
1919
3162
  .assessment-chevron,
@@ -1956,9 +3199,6 @@ body {
1956
3199
  }
1957
3200
 
1958
3201
  .sl-markdown-content :is(h1, h2, h3, h4, h5, h6),
1959
- .keyed-figure__summary,
1960
- .diagram__summary,
1961
- .memo-block__summary,
1962
3202
  .assessment-item-heading {
1963
3203
  break-after: avoid-page;
1964
3204
  page-break-after: avoid;
@@ -1978,7 +3218,7 @@ body {
1978
3218
  .compact-example,
1979
3219
  .assessment-item,
1980
3220
  .lab-result,
1981
- .lab-chart-figure,
3221
+ .lab-chart,
1982
3222
  .sl-markdown-content pre,
1983
3223
  .sl-markdown-content blockquote {
1984
3224
  break-inside: avoid-page;
@@ -2038,15 +3278,15 @@ body {
2038
3278
  display: table-row !important;
2039
3279
  }
2040
3280
 
2041
- .assessment-panel[hidden],
2042
- .short-note[hidden],
3281
+ /* An open example set's inactive tabs are hidden only to save screen
3282
+ space. A closed question panel or short note is the reader's own
3283
+ disclosure, and print follows the screen for those — it stays closed
3284
+ and gets a `.explico-collapsed-note` (below) instead. */
2043
3285
  .compact-example[hidden] {
2044
3286
  display: block !important;
2045
3287
  }
2046
3288
 
2047
3289
  .diagram,
2048
- .diagram__titlebar,
2049
- .diagram__details,
2050
3290
  .diagram__body,
2051
3291
  .diagram-stage,
2052
3292
  .diagram-scroll {
@@ -2077,6 +3317,32 @@ body {
2077
3317
  height: 100% !important;
2078
3318
  }
2079
3319
 
3320
+ /* WebKit (Safari, every iOS browser) paints a `foreignObject` descendant
3321
+ with its own render layer — every KaTeX `position: relative` span —
3322
+ without the SVG's internal transforms, the `viewBox` scale above
3323
+ included, so a printed diagram loses its formulas. Where CSS can divide
3324
+ lengths, keep the SVG at its build-time size and scale the HTML canvas
3325
+ instead; the negative block margin gives the layout box the scaled
3326
+ height. Without that support the `viewBox` scaling above stays. */
3327
+ @supports (transform: scale(calc(100cqw / 1px))) {
3328
+ .diagram-scroll {
3329
+ container-type: inline-size;
3330
+ }
3331
+
3332
+ .diagram-canvas {
3333
+ width: calc(var(--diagram-width) * 1px) !important;
3334
+ max-width: none !important;
3335
+ height: calc(var(--diagram-height) * 1px) !important;
3336
+ aspect-ratio: auto;
3337
+ margin-block-end: calc(
3338
+ 100cqw * var(--diagram-height) / var(--diagram-width) -
3339
+ var(--diagram-height) * 1px
3340
+ );
3341
+ transform: scale(calc(100cqw / (var(--diagram-width) * 1px)));
3342
+ transform-origin: 0 0;
3343
+ }
3344
+ }
3345
+
2080
3346
  .lab-chart,
2081
3347
  .lab-math,
2082
3348
  .katex-display {
@@ -2085,10 +3351,14 @@ body {
2085
3351
  overflow: visible !important;
2086
3352
  }
2087
3353
 
3354
+ /* Not KaTeX's own SVGs: a `\sqrt` radical (and stretchy arrows/accents)
3355
+ is a deliberately oversized SVG clipped by its parent, and its height
3356
+ comes from KaTeX's `height` attribute — `height: auto` resolved it to
3357
+ 0, so every printed square root lost its radical sign. */
2088
3358
  .lab-chart svg,
2089
3359
  .keyed-figure img,
2090
3360
  .sl-markdown-content img,
2091
- .sl-markdown-content svg {
3361
+ .sl-markdown-content svg:not(.katex *) {
2092
3362
  max-width: 100% !important;
2093
3363
  height: auto;
2094
3364
  }
@@ -2108,14 +3378,18 @@ body {
2108
3378
  }
2109
3379
 
2110
3380
  :is(
3381
+ .ex-tier-meta,
3382
+ .ex-tier-accent,
3383
+ .ex-tier-aside,
3384
+ .starlight-aside,
3385
+ .sl-markdown-content article.card,
2111
3386
  .lab-shell,
2112
3387
  .lab-result,
2113
3388
  .assessment-shell,
2114
3389
  .assessment-item,
2115
3390
  .memo-block-frame,
2116
- .keyed-figure__details,
2117
3391
  .compact-examples,
2118
- .diagram__details
3392
+ .block-badge
2119
3393
  ) {
2120
3394
  color: #111 !important;
2121
3395
  border-color: #777 !important;
@@ -2130,4 +3404,38 @@ body {
2130
3404
  -webkit-print-color-adjust: exact;
2131
3405
  }
2132
3406
 
3407
+ /* "(content collapsed — full text: …)" under a disclosure the reader left
3408
+ closed (`src/components/disclosure/print-as-shown.ts`). Sized in `rem`:
3409
+ a section's note sits inside its heading-sized title bar. */
3410
+ .explico-collapsed-note {
3411
+ display: block;
3412
+ margin-block: 0.15rem 0.75rem;
3413
+ font-size: 0.8rem;
3414
+ font-style: italic;
3415
+ font-weight: 400;
3416
+ line-height: 1.4;
3417
+ color: #444;
3418
+ overflow-wrap: anywhere;
3419
+ text-indent: 0;
3420
+ }
3421
+
3422
+ .explico-collapsed-note a[href] {
3423
+ color: #08775b !important;
3424
+ text-decoration: underline;
3425
+ print-color-adjust: exact;
3426
+ -webkit-print-color-adjust: exact;
3427
+ }
3428
+
3429
+ .memo-block-frame .explico-collapsed-note {
3430
+ margin: 0;
3431
+ padding-block-end: 0.25rem;
3432
+ }
3433
+ }
3434
+
3435
+ /* The note exists only while a print is being prepared, and never shows on
3436
+ screen even then (the print button prepares before the dialog opens). */
3437
+ @media not print {
3438
+ .explico-collapsed-note {
3439
+ display: none;
3440
+ }
2133
3441
  }