explico 0.8.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/CHANGELOG.md +1257 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +46 -2
  5. package/dist/compiler/cli.js +70 -4
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +68 -5
  8. package/dist/compiler/course-config.d.ts +3 -194
  9. package/dist/compiler/course-config.js +78 -5
  10. package/dist/compiler/exports/standalone-html.d.ts +6 -1
  11. package/dist/compiler/knowledge-version.js +1 -1
  12. package/dist/compiler/lesson-derivation.d.ts +27 -10
  13. package/dist/compiler/lesson-derivation.js +23 -8
  14. package/dist/compiler/logging.d.ts +4 -1
  15. package/dist/compiler/logging.js +4 -1
  16. package/dist/compiler/manifest.d.ts +204 -31
  17. package/dist/compiler/manifest.js +270 -55
  18. package/dist/compiler/resolution-report.d.ts +6 -1
  19. package/dist/course-config-Dsnri9Tk.d.ts +464 -0
  20. package/dist/reference/base-accents.d.mts +26 -0
  21. package/dist/reference/base-accents.mjs +31 -0
  22. package/dist/reference/base-roman-labels.d.mts +2 -2
  23. package/dist/reference/base-roman-labels.mjs +2 -2
  24. package/dist/reference/consistency.d.ts +14 -2
  25. package/dist/reference/consistency.js +108 -4
  26. package/dist/reference/delimiter-size.d.ts +3 -2
  27. package/dist/reference/diagrams-validate.d.ts +7 -2
  28. package/dist/reference/diagrams-validate.js +9 -1
  29. package/dist/reference/equations-validate.d.ts +8 -5
  30. package/dist/reference/equations-validate.js +1 -0
  31. package/dist/reference/equations.d.mts +60 -31
  32. package/dist/reference/equations.mjs +83 -43
  33. package/dist/reference/examples-validate.d.ts +31 -0
  34. package/dist/reference/examples-validate.js +48 -0
  35. package/dist/reference/figures-validate.d.ts +1 -1
  36. package/dist/reference/figures-validate.js +4 -1
  37. package/dist/reference/folds-validate.d.ts +33 -0
  38. package/dist/reference/folds-validate.js +32 -0
  39. package/dist/reference/gate-abbreviations.d.ts +3 -1
  40. package/dist/reference/gate-abbreviations.js +1 -0
  41. package/dist/reference/gate-course-rules.d.ts +32 -0
  42. package/dist/reference/gate-course-rules.js +85 -0
  43. package/dist/reference/gate-heading-length.d.ts +5 -2
  44. package/dist/reference/gate-heading-length.js +45 -21
  45. package/dist/reference/gate-heading-structure.d.ts +41 -0
  46. package/dist/reference/gate-heading-structure.js +113 -0
  47. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  48. package/dist/reference/gate-math-punctuation.js +251 -0
  49. package/dist/reference/gate-math.d.ts +22 -6
  50. package/dist/reference/gate-math.js +42 -8
  51. package/dist/reference/gate-part-pages.d.ts +50 -0
  52. package/dist/reference/gate-part-pages.js +118 -0
  53. package/dist/reference/gate-prose-notation.d.ts +21 -11
  54. package/dist/reference/gate-prose-notation.js +25 -7
  55. package/dist/reference/katex-delimiters.mjs +20 -7
  56. package/dist/reference/keyed-float-validate.d.ts +27 -10
  57. package/dist/reference/keyed-float-validate.js +15 -1
  58. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  59. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  60. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  61. package/dist/reference/knowledge-checks.d.mts +3 -1
  62. package/dist/reference/knowledge-checks.mjs +9 -4
  63. package/dist/reference/lab-math-scope.d.ts +1 -0
  64. package/dist/reference/lab-math-slot.d.ts +34 -0
  65. package/dist/reference/lab-math-slot.js +6 -0
  66. package/dist/reference/math-glyphs.d.mts +80 -4
  67. package/dist/reference/math-glyphs.mjs +572 -37
  68. package/dist/reference/page-notation.d.ts +6 -1
  69. package/dist/reference/raw-floats-validate.js +1 -1
  70. package/dist/reference/raw-floats.mjs +1 -1
  71. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  72. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  73. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  74. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  75. package/dist/reference/remark-asides.d.mts +3 -0
  76. package/dist/reference/remark-asides.mjs +180 -0
  77. package/dist/reference/remark-memo.d.mts +3 -1
  78. package/dist/reference/remark-memo.mjs +42 -11
  79. package/dist/reference/remark-notation.d.mts +60 -10
  80. package/dist/reference/remark-notation.mjs +387 -36
  81. package/dist/reference/render-lab-math.d.ts +4 -26
  82. package/dist/reference/render-lab-math.js +4 -4
  83. package/dist/reference/sections-validate.d.ts +27 -0
  84. package/dist/reference/sections-validate.js +41 -0
  85. package/dist/reference/sections.d.mts +97 -0
  86. package/dist/reference/sections.mjs +306 -0
  87. package/dist/reference/tables-validate.d.ts +1 -1
  88. package/dist/reference/types.d.ts +17 -1
  89. package/dist/reference/unit-words.d.ts +37 -0
  90. package/dist/reference/unit-words.js +170 -0
  91. package/dist/rules/catalog.d.ts +176 -0
  92. package/dist/rules/catalog.js +983 -0
  93. package/dist/rules/course-rules.d.ts +115 -0
  94. package/dist/rules/course-rules.js +292 -0
  95. package/dist/rules/course-rules.schema.json +138 -0
  96. package/dist/rules/in-effect.d.ts +53 -0
  97. package/dist/rules/in-effect.js +142 -0
  98. package/dist/rules/rules.schema.json +1072 -0
  99. package/dist/rules/settings.d.ts +3 -0
  100. package/dist/rules/settings.js +290 -0
  101. package/dist/rules/waivers.d.ts +53 -0
  102. package/dist/rules/waivers.js +132 -0
  103. package/dist/session/PreferenceStore.d.ts +5 -0
  104. package/dist/session/PreferenceStore.js +9 -0
  105. package/docs/VERSIONING.md +5 -3
  106. package/docs/authoring/README.md +232 -40
  107. package/docs/authoring/agents.md +55 -9
  108. package/docs/authoring/components.md +178 -22
  109. package/docs/authoring/migrating.md +1152 -2
  110. package/docs/authoring/notation.md +576 -322
  111. package/docs/authoring/prose.md +551 -0
  112. package/docs/authoring/retired.md +35 -0
  113. package/docs/authoring/structure-rules.md +422 -40
  114. package/docs/authoring/structure.md +445 -62
  115. package/docs/authoring/versioning.md +20 -28
  116. package/docs/authoring/visualizations.md +142 -20
  117. package/package.json +2 -2
  118. package/src/compiler/cli.ts +116 -6
  119. package/src/compiler/collections.ts +111 -7
  120. package/src/compiler/course-config.ts +273 -34
  121. package/src/compiler/knowledge-version.ts +1 -1
  122. package/src/compiler/lesson-derivation.ts +54 -15
  123. package/src/compiler/logging.ts +8 -2
  124. package/src/compiler/manifest.ts +573 -104
  125. package/src/components/BlockHeading.astro +113 -0
  126. package/src/components/CourseContents.astro +221 -0
  127. package/src/components/CurriculumMap.astro +5 -6
  128. package/src/components/FloatIndex.astro +200 -0
  129. package/src/components/HoverPanel.astro +431 -79
  130. package/src/components/PartBridge.astro +27 -0
  131. package/src/components/PartOverview.astro +401 -0
  132. package/src/components/PartScope.astro +27 -0
  133. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  134. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  135. package/src/components/assessments/AssessmentSet.astro +23 -26
  136. package/src/components/block-heading.ts +146 -0
  137. package/src/components/card-arrow.ts +243 -0
  138. package/src/components/card-drag.ts +631 -0
  139. package/src/components/card-jump.ts +45 -0
  140. package/src/components/card-manager.ts +1589 -0
  141. package/src/components/card-memory.ts +177 -0
  142. package/src/components/card-placement.ts +504 -0
  143. package/src/components/card-side.ts +336 -0
  144. package/src/components/card-travel.ts +197 -0
  145. package/src/components/citation/CitationLayer.astro +77 -76
  146. package/src/components/diagram/Diagram.astro +161 -316
  147. package/src/components/disclosure/course-disclosure.ts +24 -0
  148. package/src/components/disclosure/hover-preview.ts +122 -42
  149. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  150. package/src/components/disclosure/print-as-shown.ts +252 -0
  151. package/src/components/examples/CompactExample.astro +3 -1
  152. package/src/components/examples/CompactExamples.astro +101 -94
  153. package/src/components/figure/Figure.astro +174 -274
  154. package/src/components/figure/FloatCaption.astro +87 -0
  155. package/src/components/figure/figure-state.ts +58 -0
  156. package/src/components/figure/figure-tools.ts +87 -0
  157. package/src/components/float-name.ts +101 -0
  158. package/src/components/fold/Fold.astro +125 -0
  159. package/src/components/fold/fold-id.ts +40 -0
  160. package/src/components/memo/Memo.astro +162 -231
  161. package/src/components/memo/ShortNote.astro +6 -6
  162. package/src/components/notation/NotationFormula.astro +5 -1
  163. package/src/components/notation/NotationGlossary.astro +3 -4
  164. package/src/components/notation/NotationLayer.astro +169 -162
  165. package/src/components/notation/NotationMath.astro +10 -1
  166. package/src/components/reference/FloatRefLayer.astro +485 -423
  167. package/src/components/starlight/AboutPanel.astro +5 -4
  168. package/src/components/starlight/CollapsibleSections.astro +207 -93
  169. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  170. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  171. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  172. package/src/components/starlight/LayoutHeader.astro +298 -19
  173. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  174. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  175. package/src/components/starlight/LayoutSidebar.astro +229 -11
  176. package/src/components/starlight/LessonExportActions.astro +53 -72
  177. package/src/components/starlight/LessonFooter.astro +26 -4
  178. package/src/components/starlight/section-parts.ts +15 -2
  179. package/src/components/starlight/section-summary.ts +181 -0
  180. package/src/components/table/Table.astro +95 -76
  181. package/src/components/table/TableOverflowExpander.astro +65 -28
  182. package/src/content-config.ts +40 -4
  183. package/src/reference/base-accents.mjs +31 -0
  184. package/src/reference/base-roman-labels.mjs +2 -2
  185. package/src/reference/consistency.ts +176 -5
  186. package/src/reference/delimiter-size.ts +3 -2
  187. package/src/reference/diagrams-validate.ts +17 -1
  188. package/src/reference/equations-validate.ts +10 -5
  189. package/src/reference/equations.mjs +83 -43
  190. package/src/reference/examples-validate.ts +97 -0
  191. package/src/reference/figures-validate.ts +6 -1
  192. package/src/reference/folds-validate.ts +65 -0
  193. package/src/reference/gate-abbreviations.ts +1 -0
  194. package/src/reference/gate-course-rules.ts +134 -0
  195. package/src/reference/gate-heading-length.ts +66 -35
  196. package/src/reference/gate-heading-structure.ts +193 -0
  197. package/src/reference/gate-math-punctuation.ts +381 -0
  198. package/src/reference/gate-math.ts +69 -10
  199. package/src/reference/gate-part-pages.ts +217 -0
  200. package/src/reference/gate-prose-notation.ts +59 -12
  201. package/src/reference/katex-delimiters.mjs +20 -7
  202. package/src/reference/keyed-float-validate.ts +45 -11
  203. package/src/reference/keyed-jsx-floats.mjs +73 -20
  204. package/src/reference/knowledge-checks-validate.ts +1 -1
  205. package/src/reference/knowledge-checks.mjs +9 -4
  206. package/src/reference/lab-math-slot.ts +36 -0
  207. package/src/reference/math-glyphs.mjs +572 -37
  208. package/src/reference/raw-floats-validate.ts +1 -1
  209. package/src/reference/raw-floats.mjs +1 -1
  210. package/src/reference/rehype-heading-anchors.mjs +116 -17
  211. package/src/reference/rehype-table-header-labels.mjs +21 -3
  212. package/src/reference/remark-asides.mjs +180 -0
  213. package/src/reference/remark-memo.mjs +42 -11
  214. package/src/reference/remark-notation.mjs +387 -36
  215. package/src/reference/render-lab-math.ts +16 -31
  216. package/src/reference/sections-validate.ts +81 -0
  217. package/src/reference/sections.mjs +306 -0
  218. package/src/reference/tables-validate.ts +2 -1
  219. package/src/reference/types.ts +17 -0
  220. package/src/reference/unit-words.ts +233 -0
  221. package/src/rules/catalog.ts +1298 -0
  222. package/src/rules/course-rules.ts +456 -0
  223. package/src/rules/in-effect.ts +235 -0
  224. package/src/rules/settings.ts +448 -0
  225. package/src/rules/waivers.ts +249 -0
  226. package/src/session/PreferenceStore.ts +15 -0
  227. package/src/styles/global.css +1467 -163
  228. package/src/styles/surfaces.css +182 -0
  229. package/src/components/hover-panel.ts +0 -339
  230. package/src/components/popup-stack.ts +0 -345
@@ -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,160 @@ 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 label that does not fit wraps onto more lines, and the side panels
1047
+ never scroll sideways: a word longer than the panel is wide is
1048
+ hyphenated where the language allows, else broken. Starlight's
1049
+ `overflow-wrap: anywhere` on the contents links broke words at any
1050
+ letter before hyphenation got a chance. */
1051
+ .layout-navigation-content,
1052
+ .layout-page-sidebar-content,
1053
+ .layout-page-sidebar-content a {
1054
+ overflow-wrap: break-word;
1055
+ hyphens: auto;
1056
+ }
1057
+
1058
+ /* With every label wrapping, what still reaches past the navigation's
1059
+ edge is a compact chapter's resting tooltip (absolutely placed, and
1060
+ invisible): it would only add a sideways scrollbar. A showing tooltip is
1061
+ `position: fixed` (below), so this clip never cuts it. */
1062
+ .sidebar-pane {
1063
+ overflow-x: hidden;
624
1064
  }
625
1065
 
626
1066
  html[data-navigation-hidden] {
627
1067
  --sl-content-inline-start: var(--layout-edge-rail-width);
1068
+ --layout-navigation-width: var(--layout-edge-rail-width);
628
1069
  }
629
1070
 
630
1071
  html[data-navigation-hidden] .sidebar-pane {
@@ -633,12 +1074,6 @@ body {
633
1074
  transition: width 140ms ease;
634
1075
  }
635
1076
 
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
1077
  html[data-navigation-hidden] .layout-navigation-content {
643
1078
  visibility: hidden;
644
1079
  opacity: 0;
@@ -646,7 +1081,9 @@ body {
646
1081
  transition: opacity 100ms ease;
647
1082
  }
648
1083
 
649
- html[data-navigation-hidden]:not([data-navigation-peek-suppressed])
1084
+ html[data-navigation-hidden][data-navigation-peek-armed]:not(
1085
+ [data-navigation-peek-suppressed]
1086
+ )
650
1087
  .sidebar-pane:hover {
651
1088
  width: var(--sl-sidebar-width);
652
1089
  overflow-y: auto;
@@ -654,13 +1091,37 @@ body {
654
1091
  box-shadow: var(--sl-shadow-lg);
655
1092
  }
656
1093
 
657
- html[data-navigation-hidden]:not([data-navigation-peek-suppressed])
1094
+ html[data-navigation-hidden][data-navigation-peek-armed]:not(
1095
+ [data-navigation-peek-suppressed]
1096
+ )
658
1097
  .sidebar-pane:hover
659
1098
  .layout-navigation-content {
660
1099
  visibility: visible;
661
1100
  opacity: 1;
662
1101
  pointer-events: auto;
663
1102
  }
1103
+
1104
+ /* While the panel peeks open over the article, the control reaches across
1105
+ to the separator the open panel would put there — it does not travel to
1106
+ it. A reader aiming at the control is hovering the rail, which is what
1107
+ triggers the peek in the first place; moving the control at that moment
1108
+ takes it out from under the pointer that was coming for it, and the
1109
+ reader has to chase it across the panel. Growing instead keeps its outer
1110
+ edge, and the glyph inside it, exactly where the pointer found them,
1111
+ while its inner end still shows where the control will settle. The width
1112
+ is a length, like the panel's own, so the two animate in step. */
1113
+ html[data-navigation-hidden][data-navigation-peek-armed]:not(
1114
+ [data-navigation-peek-suppressed]
1115
+ )
1116
+ .sidebar-pane:hover
1117
+ .layout-navigation-toggle {
1118
+ place-items: center start;
1119
+ width: calc(
1120
+ var(--sl-sidebar-width) - var(--layout-edge-rail-width) +
1121
+ var(--layout-edge-toggle-width)
1122
+ );
1123
+ transition: width 140ms ease;
1124
+ }
664
1125
  }
665
1126
 
666
1127
  @media (min-width: 72rem) {
@@ -746,14 +1207,53 @@ body {
746
1207
  .layout-page-sidebar {
747
1208
  position: relative;
748
1209
  min-height: 100%;
749
- padding-block-start: 2.75rem;
1210
+ padding-block-start: var(--layout-edge-toggle-reserve);
750
1211
  }
751
1212
 
1213
+ /* `.right-sidebar` is `position: fixed` with `padding-top:
1214
+ var(--sl-nav-height)`, so this panel's own top edge is already the
1215
+ underside of the header: the same gap here puts this control at exactly
1216
+ the block start the navigation's fixed control computes. */
752
1217
  .layout-contents-toggle {
753
1218
  position: absolute;
754
1219
  z-index: 1;
755
- inset-block-start: 0.5rem;
756
- inset-inline-start: 0.35rem;
1220
+ inset-block-start: var(--layout-edge-toggle-gap);
1221
+ inset-inline-start: var(--layout-edge-toggle-gap);
1222
+ border-radius: 0 0 var(--layout-edge-toggle-radius) 0;
1223
+ }
1224
+
1225
+ [dir='rtl'] .layout-contents-toggle {
1226
+ border-radius: 0 0 0 var(--layout-edge-toggle-radius);
1227
+ }
1228
+
1229
+ .layout-contents-resizer {
1230
+ position: fixed;
1231
+ z-index: calc(var(--sl-z-index-toc) + 1);
1232
+ inset-block: var(--sl-nav-height) 0;
1233
+ inset-inline-end: calc(
1234
+ var(--layout-contents-width, var(--toc-actual-width, 0px)) - 1px
1235
+ );
1236
+ }
1237
+
1238
+ /* A width the reader set replaces the shrink-to-fit rail above. The
1239
+ `:not()` keeps this out of the collapsed state's way, which owns the
1240
+ same declaration at the same specificity. */
1241
+ html[data-contents-resized]:not([data-page-sidebar-hidden])
1242
+ .right-sidebar-container,
1243
+ html[data-contents-resized]:not([data-page-sidebar-hidden]) .right-sidebar {
1244
+ width: var(--layout-contents-width);
1245
+ max-width: none;
1246
+ }
1247
+
1248
+ /* Starlight sizes the column inside from the navigation's width
1249
+ (`--sl-sidebar-width`), not from this panel: in a panel the reader set,
1250
+ it fills the panel, so its headings wrap at the panel's edge instead of
1251
+ running past it. */
1252
+ html[data-contents-resized]:not([data-page-sidebar-hidden])
1253
+ .right-sidebar-panel
1254
+ .sl-container {
1255
+ width: auto;
1256
+ max-width: none;
757
1257
  }
758
1258
 
759
1259
  html[data-page-sidebar-hidden] {
@@ -764,15 +1264,6 @@ body {
764
1264
  width: var(--layout-edge-rail-width);
765
1265
  }
766
1266
 
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
1267
  html[data-page-sidebar-hidden] .right-sidebar {
777
1268
  z-index: var(--sl-z-index-toc);
778
1269
  inset-inline-start: auto;
@@ -794,7 +1285,9 @@ body {
794
1285
  transition: opacity 100ms ease;
795
1286
  }
796
1287
 
797
- html[data-page-sidebar-hidden]:not([data-page-sidebar-peek-suppressed])
1288
+ html[data-page-sidebar-hidden][data-page-sidebar-peek-armed]:not(
1289
+ [data-page-sidebar-peek-suppressed]
1290
+ )
798
1291
  .right-sidebar:hover {
799
1292
  width: max-content;
800
1293
  max-width: 80ch;
@@ -802,13 +1295,32 @@ body {
802
1295
  box-shadow: var(--sl-shadow-lg);
803
1296
  }
804
1297
 
805
- html[data-page-sidebar-hidden]:not([data-page-sidebar-peek-suppressed])
1298
+ html[data-page-sidebar-hidden][data-page-sidebar-peek-armed]:not(
1299
+ [data-page-sidebar-peek-suppressed]
1300
+ )
806
1301
  .right-sidebar:hover
807
1302
  .layout-page-sidebar-content {
808
1303
  visibility: visible;
809
1304
  opacity: 1;
810
1305
  pointer-events: auto;
811
1306
  }
1307
+
1308
+ /* The same growth, mirrored. This control is positioned inside its panel,
1309
+ so a peek drags it inward with the panel's own edge unless its outer edge
1310
+ is pinned to where the rail put it; with both insets set, the box grows
1311
+ to meet the separator on its own. */
1312
+ html[data-page-sidebar-hidden][data-page-sidebar-peek-armed]:not(
1313
+ [data-page-sidebar-peek-suppressed]
1314
+ )
1315
+ .right-sidebar:hover
1316
+ .layout-contents-toggle {
1317
+ place-items: center end;
1318
+ inset-inline-end: calc(
1319
+ var(--layout-edge-rail-width) - var(--layout-edge-toggle-width) -
1320
+ var(--layout-edge-toggle-gap) - 1px
1321
+ );
1322
+ width: auto;
1323
+ }
812
1324
  }
813
1325
 
814
1326
  @media (prefers-reduced-motion: reduce) {
@@ -820,6 +1332,162 @@ body {
820
1332
  }
821
1333
  }
822
1334
 
1335
+ /* The side column's room (rule S3 of docs/plans/hover-card-consistency.md):
1336
+ while explanation cards sit to the right of the article, the article
1337
+ slides left through its own start margin, which `card-side.ts` computes
1338
+ and hands over as `--explico-article-start`. `.main-pane` keeps its width
1339
+ and the rail stays where it is. `:root` outranks the `html[…] .main-pane`
1340
+ rules above that centre the article; `screen` keeps the slide off paper.
1341
+ The slide's animation is a transform the card manager runs, so this
1342
+ margin is always the final one. */
1343
+ @media screen and (min-width: 72rem) {
1344
+ :root[data-article-slid] .main-pane {
1345
+ --sl-content-margin-inline: var(--explico-article-start, 0px) auto;
1346
+ }
1347
+ }
1348
+
1349
+ /* Both side panels sit above the explanation cards, whose layer takes the
1350
+ header's level just before `.page` (`card-placement.ts`): an open panel is
1351
+ never covered — a reader who needs its room collapses it — and a collapsed
1352
+ one peeks open over the cards. Each panel's controls live inside it, so
1353
+ they rise with it. The header outranks both: the rail's box starts at the
1354
+ window's top, under the header. An expanded table or diagram covers the
1355
+ panels, and the cards opened from inside it must cover it, so while one is
1356
+ open the panels drop back to Starlight's own levels. */
1357
+ .header {
1358
+ z-index: calc(var(--sl-z-index-navbar) + 1);
1359
+ }
1360
+
1361
+ /* A slimmer site header: 3rem at every width, where Starlight's is 3.5rem
1362
+ and 4rem from 50rem up. The title, the logo (sized from the height and
1363
+ padding) and the search box scale with it, so more of the window goes to
1364
+ the lesson, and to the cards and side column under the header.
1365
+ Everything that sits under the header reads `--sl-nav-height`. */
1366
+ :root {
1367
+ --sl-nav-height: 3rem;
1368
+ --sl-nav-pad-y: 0.5rem;
1369
+ }
1370
+
1371
+ .site-title {
1372
+ font-size: var(--sl-text-xl);
1373
+ }
1374
+
1375
+ @media (min-width: 50rem) {
1376
+ button[data-open-modal] {
1377
+ height: 2rem;
1378
+ }
1379
+ }
1380
+
1381
+ @media (min-width: 50rem) {
1382
+ :root .sidebar-pane {
1383
+ z-index: var(--sl-z-index-navbar);
1384
+ }
1385
+ }
1386
+
1387
+ @media (min-width: 72rem) {
1388
+ :root .right-sidebar,
1389
+ :root[data-page-sidebar-hidden] .right-sidebar {
1390
+ z-index: var(--sl-z-index-navbar);
1391
+ }
1392
+ }
1393
+
1394
+ :root:has(.table-overflow-expanded, .diagram-is-expanded) .sidebar-pane {
1395
+ z-index: var(--sl-z-index-menu);
1396
+ }
1397
+
1398
+ :root:has(.table-overflow-expanded, .diagram-is-expanded) .right-sidebar {
1399
+ z-index: var(--sl-z-index-toc);
1400
+ }
1401
+
1402
+ /* S9: "Keep the article left", fixed at the top of the side column just
1403
+ under the header, over the cards scrolling past it. `card-side.ts` sets
1404
+ its `left`. */
1405
+ .explico-keep-left {
1406
+ position: fixed;
1407
+ z-index: var(--sl-z-index-navbar);
1408
+ top: calc(var(--sl-nav-height) + 0.5rem);
1409
+ display: inline-flex;
1410
+ align-items: center;
1411
+ gap: 0.45rem;
1412
+ margin: 0;
1413
+ padding: 0.2rem 0.65rem 0.2rem 0.4rem;
1414
+ border: var(--ex-edge-border);
1415
+ border-radius: 999px;
1416
+ font: inherit;
1417
+ font-size: var(--sl-text-xs);
1418
+ line-height: 1.4;
1419
+ color: var(--sl-color-gray-2);
1420
+ background: var(--sl-color-bg);
1421
+ box-shadow: var(--sl-shadow-sm);
1422
+ cursor: pointer;
1423
+ }
1424
+
1425
+ .explico-keep-left[hidden] {
1426
+ display: none;
1427
+ }
1428
+
1429
+ .explico-keep-left:hover {
1430
+ color: var(--sl-color-text);
1431
+ }
1432
+
1433
+ .explico-keep-left:focus-visible {
1434
+ outline: 3px solid var(--sl-color-accent);
1435
+ outline-offset: 2px;
1436
+ }
1437
+
1438
+ .explico-keep-left-switch {
1439
+ position: relative;
1440
+ flex: none;
1441
+ width: 1.75em;
1442
+ height: 1em;
1443
+ border-radius: 999px;
1444
+ background: var(--sl-color-gray-4);
1445
+ transition: background-color 100ms ease;
1446
+ }
1447
+
1448
+ .explico-keep-left-switch::after {
1449
+ content: '';
1450
+ position: absolute;
1451
+ top: 0.125em;
1452
+ left: 0.125em;
1453
+ width: 0.75em;
1454
+ height: 0.75em;
1455
+ border-radius: 50%;
1456
+ background: var(--sl-color-bg);
1457
+ transition: transform 100ms ease;
1458
+ }
1459
+
1460
+ .explico-keep-left[aria-pressed='true'] .explico-keep-left-switch {
1461
+ background: var(--sl-color-accent);
1462
+ }
1463
+
1464
+ .explico-keep-left[aria-pressed='true'] .explico-keep-left-switch::after {
1465
+ transform: translateX(0.75em);
1466
+ }
1467
+
1468
+ @media (prefers-reduced-motion: reduce) {
1469
+ .explico-keep-left-switch,
1470
+ .explico-keep-left-switch::after {
1471
+ transition: none;
1472
+ }
1473
+ }
1474
+
1475
+ @media (forced-colors: active) {
1476
+ .explico-keep-left-switch {
1477
+ border: 1px solid ButtonText;
1478
+ }
1479
+
1480
+ .explico-keep-left[aria-pressed='true'] .explico-keep-left-switch {
1481
+ background: Highlight;
1482
+ }
1483
+ }
1484
+
1485
+ @media print {
1486
+ .explico-keep-left {
1487
+ display: none !important;
1488
+ }
1489
+ }
1490
+
823
1491
  /* Source citations: inline `[n]` markers and the end-of-page reference list
824
1492
  emitted by the remark-citation adapter. The hover/pin panel is styled in
825
1493
  the CitationLayer island. */
@@ -1270,61 +1938,217 @@ body {
1270
1938
  }
1271
1939
  }
1272
1940
 
1273
- .sl-markdown-content .keyed-equation:target {
1274
- animation: eq-flash 5s ease-out both;
1941
+ .sl-markdown-content .keyed-equation:target {
1942
+ animation: eq-flash 5s ease-out both;
1943
+ }
1944
+
1945
+ @media (prefers-reduced-motion: reduce) {
1946
+ .sl-markdown-content .keyed-equation:target {
1947
+ animation: none;
1948
+ background: var(--sl-color-accent-low);
1949
+ outline: 2px solid var(--sl-color-accent);
1950
+ outline-offset: 0.2rem;
1951
+ }
1952
+ }
1953
+
1954
+ @media (forced-colors: active) {
1955
+ .sl-markdown-content .keyed-equation:target {
1956
+ animation: none;
1957
+ outline: 2px solid Highlight;
1958
+ outline-offset: 0.2rem;
1959
+ }
1960
+ .sl-markdown-content a.equation-ref:hover,
1961
+ .sl-markdown-content a.equation-ref:focus-visible {
1962
+ outline: 2px solid Highlight;
1963
+ }
1964
+ }
1965
+
1966
+ @media print {
1967
+ .sl-markdown-content .keyed-equation__controls {
1968
+ position: static;
1969
+ display: block;
1970
+ transform: none;
1971
+ text-align: end;
1972
+ }
1973
+ .sl-markdown-content a.keyed-equation__number {
1974
+ color: inherit;
1975
+ opacity: 1;
1976
+ }
1977
+ /* A clipboard button means nothing on paper. */
1978
+ .sl-markdown-content .keyed-equation__copy {
1979
+ display: none;
1980
+ }
1981
+ .sl-markdown-content .keyed-equation {
1982
+ padding-inline-end: 0;
1983
+ }
1984
+ }
1985
+
1986
+ /* One caption line for every float (docs/plans/titles-and-containers.md
1987
+ Part 3): `Figure 2.2.1 [chain] ‹caption›` — the number links to the float,
1988
+ the chain icon copies that link (`HeadingAnchorEnhancer.astro`), the
1989
+ caption is the float's one long caption. A figure and a diagram put it
1990
+ below the picture (`FloatCaption.astro`), a table above it, the LaTeX
1991
+ convention. The line never hides (docs/plans/figures-and-short-captions.md,
1992
+ decision 1); its buttons sit at its end (`.float-tools`, below). */
1993
+ .sl-markdown-content .float-caption {
1994
+ /* Anchors the button row (`.float-tools`) to the line's end. */
1995
+ position: relative;
1996
+ margin-block: 0.5rem 0;
1997
+ font-size: var(--sl-text-sm);
1998
+ line-height: 1.5;
1999
+ color: var(--sl-color-gray-3);
2000
+ break-before: avoid-page;
2001
+ }
2002
+
2003
+ .sl-markdown-content .keyed-table > .float-caption {
2004
+ margin-block: 0 0.5rem;
2005
+ break-before: auto;
2006
+ break-after: avoid-page;
2007
+ }
2008
+
2009
+ /* A float's buttons sit together at the top right of its caption line, in
2010
+ one boxed style, always visible: a table's widen ↔ (the same action as
2011
+ the column at its right edge) and its fold ⌃ / ⌄, a diagram's widen ↔.
2012
+ The caption's text keeps clear of a shown one. A table's scripts add the
2013
+ row (`Table.astro`, `TableOverflowExpander.astro`), whichever runs first;
2014
+ a figure's or a diagram's is rendered by `FloatCaption.astro`. */
2015
+ .sl-markdown-content .float-tools {
2016
+ position: absolute;
2017
+ inset-block-start: 0;
2018
+ inset-inline-end: 0;
2019
+ display: inline-flex;
2020
+ gap: 0.25rem;
2021
+ }
2022
+
2023
+ .sl-markdown-content .float-caption:has(> .float-tools > .float-tool:not([hidden])) {
2024
+ padding-inline-end: 2.25rem;
2025
+ }
2026
+
2027
+ .sl-markdown-content
2028
+ .float-caption:has(> .float-tools > .float-tool:not([hidden]) ~ .float-tool:not([hidden])) {
2029
+ padding-inline-end: 4.25rem;
2030
+ }
2031
+
2032
+ .sl-markdown-content .float-tool {
2033
+ display: inline-grid;
2034
+ place-items: center;
2035
+ width: 1.75rem;
2036
+ height: 1.5rem;
2037
+ margin: 0;
2038
+ padding: 0;
2039
+ border: 1px solid var(--sl-color-gray-5);
2040
+ border-radius: 0.3rem;
2041
+ background: var(--sl-color-bg);
2042
+ color: var(--sl-color-gray-2);
2043
+ cursor: pointer;
2044
+ }
2045
+
2046
+ .sl-markdown-content .float-tool[hidden] {
2047
+ display: none;
2048
+ }
2049
+
2050
+ .sl-markdown-content .float-tool svg {
2051
+ width: 0.875rem;
2052
+ height: 0.875rem;
2053
+ fill: none;
2054
+ stroke: currentColor;
2055
+ stroke-width: 2.2;
2056
+ stroke-linecap: round;
2057
+ stroke-linejoin: round;
2058
+ }
2059
+
2060
+ .sl-markdown-content .float-tool:hover {
2061
+ color: var(--sl-color-text-accent);
2062
+ border-color: var(--sl-color-text-accent);
2063
+ }
2064
+
2065
+ .sl-markdown-content .float-tool:focus-visible {
2066
+ outline: 2px solid var(--sl-color-accent);
2067
+ outline-offset: 1px;
2068
+ }
2069
+
2070
+ /* The widen button's two faces: ↔ widens, →← brings the float back. */
2071
+ .sl-markdown-content .float-tool[aria-expanded='true'] .float-tool__widen,
2072
+ .sl-markdown-content .float-tool[aria-expanded='false'] .float-tool__narrow {
2073
+ display: none;
1275
2074
  }
1276
2075
 
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
- }
2076
+ /* The fold button's two faces, for a lab's tools (`Figure.astro`): ⌃ folds
2077
+ them away, ⌄ brings them back. A table's toggle swaps its icon instead. */
2078
+ .sl-markdown-content .float-tool[aria-expanded='true'] .float-tool__unfold,
2079
+ .sl-markdown-content .float-tool[aria-expanded='false'] .float-tool__fold {
2080
+ display: none;
1284
2081
  }
1285
2082
 
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
- }
2083
+ /* The caption line's live end: the case a lab's plot shows, after the long
2084
+ caption (`FloatCaption.astro`, `figure/figure-state.ts`). */
2085
+ .sl-markdown-content .float-caption__state:not(:empty) {
2086
+ margin-inline-start: 0.25em;
1296
2087
  }
1297
2088
 
1298
2089
  @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 {
2090
+ .sl-markdown-content .float-tools {
1311
2091
  display: none;
1312
2092
  }
1313
- .sl-markdown-content .keyed-equation {
2093
+
2094
+ .sl-markdown-content .float-caption:has(> .float-tools) {
1314
2095
  padding-inline-end: 0;
1315
2096
  }
1316
2097
  }
1317
2098
 
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;
2099
+ /* A short caption opens the line as its bold title, the journal-legend
2100
+ style: "**Sampled flight data.** Position, …" (`FloatCaption.astro`,
2101
+ `Table.astro`; docs/plans/figures-and-short-captions.md, decision 9.1). */
2102
+ .sl-markdown-content .float-caption__title {
2103
+ color: var(--sl-color-text);
2104
+ font-weight: 600;
1324
2105
  }
1325
2106
 
1326
- .sl-markdown-content .diagram__summary + .diagram__body {
1327
- margin-block-start: 0;
2107
+ .sl-markdown-content .float-caption__number {
2108
+ font-weight: 600;
2109
+ font-variant-numeric: tabular-nums;
2110
+ color: var(--sl-color-text-accent);
2111
+ text-decoration: none;
2112
+ }
2113
+
2114
+ .sl-markdown-content .float-caption__number:hover,
2115
+ .sl-markdown-content .float-caption__number:focus-visible {
2116
+ text-decoration: underline;
2117
+ }
2118
+
2119
+ .sl-markdown-content .float-caption__anchor {
2120
+ display: inline-flex;
2121
+ width: 1.15em;
2122
+ height: 1.15em;
2123
+ margin-inline: 0.15em 0.3em;
2124
+ vertical-align: -0.2em;
2125
+ align-items: center;
2126
+ justify-content: center;
2127
+ color: var(--sl-color-gray-4);
2128
+ text-decoration: none;
2129
+ -webkit-user-select: none;
2130
+ user-select: none;
2131
+ }
2132
+
2133
+ .sl-markdown-content .float-caption__anchor svg {
2134
+ width: 0.85em;
2135
+ height: 0.85em;
2136
+ }
2137
+
2138
+ .sl-markdown-content .float-caption__anchor:hover,
2139
+ .sl-markdown-content .float-caption__anchor:focus-visible {
2140
+ color: var(--sl-color-text-accent);
2141
+ }
2142
+
2143
+ .sl-markdown-content :is(.float-caption__number, .float-caption__anchor):focus-visible {
2144
+ outline: 2px solid var(--sl-color-accent);
2145
+ outline-offset: 0.15rem;
2146
+ }
2147
+
2148
+ @media print {
2149
+ .sl-markdown-content .float-caption__number {
2150
+ color: inherit;
2151
+ }
1328
2152
  }
1329
2153
 
1330
2154
  /* `<Table>`/`<Figure>`/`<Diagram>` keyed floats — the same deep-link
@@ -1346,12 +2170,36 @@ body {
1346
2170
  white-space: nowrap;
1347
2171
  }
1348
2172
 
2173
+ /* `[[sec-…]]` reads the same, but may print a section's title rather than
2174
+ `§ 1.1.2` (an unnumbered heading, or a course that hides section numbers),
2175
+ so it wraps like prose; the no-break space inside `§ 1.1.2` keeps that one
2176
+ whole. `[[check-…]]` and `[[ex-…]]` follow the section rules too
2177
+ (docs/plans/titles-and-containers.md Q11): a course that hides section
2178
+ numbers gets "Example “‹title›”". */
2179
+ .sl-markdown-content a.section-ref,
2180
+ .sl-markdown-content a.knowledge-check-ref,
2181
+ .sl-markdown-content a.example-ref {
2182
+ font-variant-numeric: tabular-nums;
2183
+ font-weight: 600;
2184
+ text-decoration: underline;
2185
+ text-underline-offset: 0.15em;
2186
+ padding-inline: 0.1em;
2187
+ border-radius: 0.2em;
2188
+ color: var(--sl-color-text-accent);
2189
+ }
2190
+
1349
2191
  .sl-markdown-content a.table-ref:hover,
1350
2192
  .sl-markdown-content a.table-ref:focus-visible,
1351
2193
  .sl-markdown-content a.figure-ref:hover,
1352
2194
  .sl-markdown-content a.figure-ref:focus-visible,
1353
2195
  .sl-markdown-content a.diagram-ref:hover,
1354
- .sl-markdown-content a.diagram-ref:focus-visible {
2196
+ .sl-markdown-content a.diagram-ref:focus-visible,
2197
+ .sl-markdown-content a.section-ref:hover,
2198
+ .sl-markdown-content a.section-ref:focus-visible,
2199
+ .sl-markdown-content a.knowledge-check-ref:hover,
2200
+ .sl-markdown-content a.knowledge-check-ref:focus-visible,
2201
+ .sl-markdown-content a.example-ref:hover,
2202
+ .sl-markdown-content a.example-ref:focus-visible {
1355
2203
  background: var(--sl-color-accent-low);
1356
2204
  }
1357
2205
 
@@ -1373,13 +2221,34 @@ body {
1373
2221
  }
1374
2222
  }
1375
2223
 
2224
+ /* A reference whose target sits inside a closed memo or section opens a
2225
+ preview card, as if the target were off screen; while that card is open,
2226
+ the fold's bar is marked the same way, so the reader sees where the
2227
+ target lives (`FloatRefLayer.astro`). */
2228
+ .sl-markdown-content .float-ref-fold-hint {
2229
+ background: var(--sl-color-accent-low);
2230
+ outline: 2px solid var(--sl-color-accent);
2231
+ outline-offset: 0.2rem;
2232
+ border-radius: 0.3rem;
2233
+ transition:
2234
+ background 120ms ease-out,
2235
+ outline-color 120ms ease-out;
2236
+ }
2237
+
2238
+ @media (forced-colors: active) {
2239
+ .sl-markdown-content .float-ref-fold-hint {
2240
+ outline-color: Highlight;
2241
+ }
2242
+ }
2243
+
1376
2244
  /* A referencing `[[tab-key]]`/`[[fig-key]]` link elsewhere in the prose was
1377
2245
  hovered/focused and the float it points to is already fully on screen:
1378
2246
  highlight it in place instead of opening a preview card
1379
2247
  (`FloatRefLayer.astro` decides which) — mirrors `.eq-ref-highlight`. */
1380
2248
  .sl-markdown-content .keyed-table.tab-ref-highlight,
1381
2249
  .sl-markdown-content .keyed-figure.fig-ref-highlight,
1382
- .sl-markdown-content .diagram.dia-ref-highlight {
2250
+ .sl-markdown-content .diagram.dia-ref-highlight,
2251
+ .sl-markdown-content :is(h2, h3, h4, h5, h6).sec-ref-highlight {
1383
2252
  background: var(--sl-color-accent-low);
1384
2253
  outline: 2px solid var(--sl-color-accent);
1385
2254
  outline-offset: 0.2rem;
@@ -1395,7 +2264,8 @@ body {
1395
2264
  .sl-markdown-content .diagram:target,
1396
2265
  .sl-markdown-content .keyed-table.tab-ref-highlight,
1397
2266
  .sl-markdown-content .keyed-figure.fig-ref-highlight,
1398
- .sl-markdown-content .diagram.dia-ref-highlight {
2267
+ .sl-markdown-content .diagram.dia-ref-highlight,
2268
+ .sl-markdown-content :is(h2, h3, h4, h5, h6).sec-ref-highlight {
1399
2269
  animation: none;
1400
2270
  outline: 2px solid Highlight;
1401
2271
  outline-offset: 0.2rem;
@@ -1405,7 +2275,13 @@ body {
1405
2275
  .sl-markdown-content a.figure-ref:hover,
1406
2276
  .sl-markdown-content a.figure-ref:focus-visible,
1407
2277
  .sl-markdown-content a.diagram-ref:hover,
1408
- .sl-markdown-content a.diagram-ref:focus-visible {
2278
+ .sl-markdown-content a.diagram-ref:focus-visible,
2279
+ .sl-markdown-content a.section-ref:hover,
2280
+ .sl-markdown-content a.section-ref:focus-visible,
2281
+ .sl-markdown-content a.knowledge-check-ref:hover,
2282
+ .sl-markdown-content a.knowledge-check-ref:focus-visible,
2283
+ .sl-markdown-content a.example-ref:hover,
2284
+ .sl-markdown-content a.example-ref:focus-visible {
1409
2285
  outline: 2px solid Highlight;
1410
2286
  }
1411
2287
  }
@@ -1463,6 +2339,197 @@ body {
1463
2339
  }
1464
2340
  }
1465
2341
 
2342
+ /* Outline numbers (docs/plans/titles-and-containers.md Part 1). A lesson's
2343
+ numbered heading carries `data-sec-number="1.1.2"` (`remark-notation.mjs`,
2344
+ from the manifest), printed here ahead of the title as generated content —
2345
+ so the heading's own text, its anchor and the "On this page" entry stay the
2346
+ title alone. The number shows on a wide screen and in print; on a narrow one
2347
+ only on a collapsed section's bar, which says what is folded away, and in
2348
+ the chain icon's "Link copied · § 1.1.2". The en space is part of the
2349
+ content, not a margin, so a screen reader hears "1.1.2 Title", not
2350
+ "1.1.2Title". */
2351
+ .sl-markdown-content [data-sec-number]::before {
2352
+ content: attr(data-sec-number) '\2002';
2353
+ color: var(--sl-color-text-accent);
2354
+ font-variant-numeric: tabular-nums;
2355
+ }
2356
+
2357
+ @media screen and (width < 50em) {
2358
+ .sl-markdown-content [data-sec-number]::before {
2359
+ content: none;
2360
+ }
2361
+
2362
+ .sl-markdown-content
2363
+ details.sl-section-collapsible:not([data-state='open'])
2364
+ > .sl-section-summary
2365
+ > [data-sec-number]::before {
2366
+ content: attr(data-sec-number) '\2002';
2367
+ }
2368
+ }
2369
+
2370
+ @media print {
2371
+ .sl-markdown-content [data-sec-number]::before {
2372
+ color: inherit;
2373
+ }
2374
+ }
2375
+
2376
+ /* Kind badges (Part 6): the `<h2>` a knowledge check and a worked-example set
2377
+ draw (`BlockHeading.astro`). The kind's icon leads the author's title, as
2378
+ it does in "On this page"; the kind's word and the block's number are a
2379
+ small label in the card's top-right corner, inside its top padding. The
2380
+ label is out of the text flow, so the title keeps the whole line at every
2381
+ width and still wraps word by word, and the number reads as the block's
2382
+ label rather than as a section number. `data-labels="icon"` keeps the word
2383
+ for a screen reader only (`sr-only`). The card's padding is set for it
2384
+ below; `--block-label-lift` is how far the label sits above the title's
2385
+ line box, measured from the card's inner top edge. */
2386
+ .sl-markdown-content .ex-section-card:has(> .block-heading-wrapper) {
2387
+ --block-label-lift: 1.25rem;
2388
+ padding-block-start: var(--block-label-lift);
2389
+ }
2390
+
2391
+ .sl-markdown-content .block-heading-wrapper {
2392
+ position: relative;
2393
+ }
2394
+
2395
+ .sl-markdown-content .block-heading {
2396
+ /* A wrapped title's later lines start under its first word, not under
2397
+ the icon. */
2398
+ padding-inline-start: 1.1em;
2399
+ text-indent: -1.1em;
2400
+ }
2401
+
2402
+ .sl-markdown-content .block-heading__icon {
2403
+ display: inline-block;
2404
+ width: 0.8em;
2405
+ height: 0.8em;
2406
+ margin-inline-end: 0.3em;
2407
+ vertical-align: -0.06em;
2408
+ color: var(--sl-color-text-accent);
2409
+ }
2410
+
2411
+ .sl-markdown-content .block-badge {
2412
+ position: absolute;
2413
+ inset-block-start: calc(0.3rem - var(--block-label-lift, 1.25rem));
2414
+ inset-inline-end: 0;
2415
+ color: var(--sl-color-text-accent);
2416
+ font-size: 0.75rem;
2417
+ font-weight: 600;
2418
+ letter-spacing: 0.05em;
2419
+ line-height: 1.2;
2420
+ text-indent: 0;
2421
+ text-transform: uppercase;
2422
+ white-space: nowrap;
2423
+ }
2424
+
2425
+ /* Narrow and touch screens set the heading inline beside the section's
2426
+ chevron, with the summary's own hanging indent (below); the heading's
2427
+ indent above would double it. */
2428
+ @media screen and (width < 50em), screen and (hover: none) {
2429
+ .sl-markdown-content .block-heading {
2430
+ padding-inline-start: 0;
2431
+ text-indent: 0;
2432
+ }
2433
+ }
2434
+
2435
+ /* The number follows the section numbers' rules above (Q11): shown on a wide
2436
+ screen and in print; on a narrow one only on a folded bar, and in the chain
2437
+ icon's "Link copied · Example 1.1.1". Real text, so the heading's name reads
2438
+ "Example 1.1.1 ‹title›" wherever it shows. */
2439
+ .sl-markdown-content .block-heading__number {
2440
+ color: var(--sl-color-text-accent);
2441
+ font-variant-numeric: tabular-nums;
2442
+ }
2443
+
2444
+ @media screen and (width < 50em) {
2445
+ .sl-markdown-content .block-heading__number {
2446
+ display: none;
2447
+ }
2448
+
2449
+ .sl-markdown-content
2450
+ details.sl-section-collapsible:not([data-state='open'])
2451
+ > .sl-section-summary
2452
+ > .block-heading
2453
+ .block-heading__number {
2454
+ display: inline;
2455
+ }
2456
+ }
2457
+
2458
+ @media print {
2459
+ .sl-markdown-content .block-heading__number {
2460
+ color: inherit;
2461
+ }
2462
+ }
2463
+
2464
+ /* "On this page" lists a check and an example set by its title alone, as it
2465
+ lists a section; the badge's icon before the title says which kind it is
2466
+ (`LayoutPageSidebar.astro` marks the link `data-block-kind`). A mask, so
2467
+ the icon takes the link's colour, current and hovered alike. */
2468
+ :is(starlight-toc, mobile-starlight-toc) a[data-block-kind]::before {
2469
+ content: '';
2470
+ display: inline-block;
2471
+ width: 0.95em;
2472
+ height: 0.95em;
2473
+ margin-inline-end: 0.35em;
2474
+ vertical-align: -0.12em;
2475
+ background-color: currentColor;
2476
+ mask: var(--block-kind-icon) center / contain no-repeat;
2477
+ }
2478
+
2479
+ :is(starlight-toc, mobile-starlight-toc) a[data-block-kind='check'] {
2480
+ --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");
2481
+ }
2482
+
2483
+ :is(starlight-toc, mobile-starlight-toc) a[data-block-kind='example'] {
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='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");
2485
+ }
2486
+
2487
+ /* "Link copied · § 1.1.2" — `HeadingAnchorEnhancer.astro` places it under the
2488
+ title in page coordinates, clamped to the text column, and removes it
2489
+ after a moment; the same accent chip as an equation's "Copied!", with an
2490
+ edge and a shadow because the section behind it is flashing the same
2491
+ accent tint. */
2492
+ .sl-anchor-copied {
2493
+ position: absolute;
2494
+ z-index: 30;
2495
+ padding: 0.15rem 0.5rem;
2496
+ border: 1px solid var(--sl-color-accent);
2497
+ border-radius: 0.35rem;
2498
+ box-shadow: 0 0.2rem 0.6rem rgba(0, 0, 0, 0.15);
2499
+ background: var(--sl-color-accent-low);
2500
+ color: var(--sl-color-text-accent);
2501
+ font-size: var(--sl-text-xs);
2502
+ line-height: 1.5;
2503
+ /* One line for a short name; a long title wraps inside the cap, never
2504
+ wider than the column (the script keeps it inside the column). */
2505
+ width: max-content;
2506
+ max-width: min(28rem, calc(100vw - 2rem));
2507
+ pointer-events: none;
2508
+ animation: sl-anchor-copied 1.8s ease both;
2509
+ }
2510
+
2511
+ @keyframes sl-anchor-copied {
2512
+ 0%,
2513
+ 75% {
2514
+ opacity: 1;
2515
+ }
2516
+ 100% {
2517
+ opacity: 0;
2518
+ }
2519
+ }
2520
+
2521
+ @media (prefers-reduced-motion: reduce) {
2522
+ .sl-anchor-copied {
2523
+ animation: none;
2524
+ }
2525
+ }
2526
+
2527
+ @media print {
2528
+ .sl-anchor-copied {
2529
+ display: none;
2530
+ }
2531
+ }
2532
+
1466
2533
  /* Collapsible sections (docs/plans/collapsible-disclosure.md Part 3),
1467
2534
  wrapped at runtime by `CollapsibleSections.astro` around headings an
1468
2535
  author opted in with `{collapsible}`. Starlight's own `markdown.css` gives
@@ -1535,13 +2602,21 @@ body {
1535
2602
  never satisfies this selector. Both rules are unlayered, so they win over
1536
2603
  `anchor-links.css`'s `@layer starlight.content` version regardless of
1537
2604
  specificity — this replaces that default behavior for sections rather
1538
- than adding to it. */
2605
+ than adding to it. Like Starlight's own rule, the icon hides only where a
2606
+ hover can reveal it: on a touch screen it stays visible, since it is a
2607
+ real tap target at the end of the title line either way (an invisible one
2608
+ used to take a tap meant for the title and change the URL instead). */
1539
2609
  .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar > .sl-anchor-link {
1540
2610
  position: absolute;
1541
2611
  top: 0.2em;
1542
2612
  inset-inline-end: 0;
1543
2613
  margin-inline-start: 0;
1544
- opacity: 0;
2614
+ }
2615
+
2616
+ @media (hover: hover) {
2617
+ .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar > .sl-anchor-link {
2618
+ opacity: 0;
2619
+ }
1545
2620
  }
1546
2621
 
1547
2622
  .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar:has(> .sl-section-collapsible > summary:hover)
@@ -1553,24 +2628,72 @@ body {
1553
2628
  opacity: 1;
1554
2629
  }
1555
2630
 
2631
+ /* A narrow screen shows no section number and, often, has no hover to reveal
2632
+ the chain icon — yet the icon is the way to the section's link and, in its
2633
+ "Link copied · § 1.1.2", its number (docs/plans/titles-and-containers.md
2634
+ Q3). So it is always shown there, smaller and in the muted text colour.
2635
+ The size variable also sets the room `anchor-links.css` reserves for it at
2636
+ the end of the title. After the hover rules above, which it overrides. */
2637
+ @media screen and (width < 50em) {
2638
+ .sl-markdown-content .sl-heading-wrapper {
2639
+ --sl-anchor-icon-size: 0.7em;
2640
+ }
2641
+
2642
+ .sl-markdown-content .sl-heading-wrapper > .sl-anchor-link,
2643
+ .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar > .sl-anchor-link {
2644
+ opacity: 1;
2645
+ color: var(--sl-color-gray-3);
2646
+ }
2647
+ }
2648
+
1556
2649
  /* The chevron sits to the *left* of the title without shifting the title's
1557
2650
  own position: `markdown.css`'s default marker is inline, right before the
1558
2651
  text, pushed left slightly by a negative margin — meaning the title
1559
2652
  itself sits further right than an ordinary (non-collapsible) heading.
1560
2653
  Taking the marker fully out of flow with `position: absolute` and a
1561
2654
  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. */
2655
+ own text always starts at the same position a plain heading's would.
2656
+ That gutter is only `--sl-content-pad-x` wide (16–24px) and ends at the
2657
+ sidebar's border, so the marker is centred in it — a fixed `-1.6em` once
2658
+ put the glyph right on that border line. Inside a `[card]` section the
2659
+ gutter is the card's own padding (`--ex-section-gutter`, `surfaces.css`).
2660
+ Narrow and touch screens have no usable gutter at all; the marker moves
2661
+ into the title line there (below). */
1563
2662
  .sl-markdown-content .sl-section-summary {
1564
2663
  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`. */
2664
+ /* Room for the absolutely-positioned anchor icon at the end of the line. */
1567
2665
  padding-inline-end: 1.5em;
2666
+ /* The title's own font size, so the gutter chevron can centre on the
2667
+ title's first line — not on the whole summary, which also holds the
2668
+ "Hidden here" line while closed. */
2669
+ --sl-section-title-size: var(--sl-text-h2);
2670
+ }
2671
+
2672
+ .sl-markdown-content .sl-section-summary:has(> h3) {
2673
+ --sl-section-title-size: var(--sl-text-h3);
2674
+ }
2675
+
2676
+ .sl-markdown-content .sl-section-summary:has(> h4) {
2677
+ --sl-section-title-size: var(--sl-text-h4);
2678
+ }
2679
+
2680
+ .sl-markdown-content .sl-section-summary:has(> h5) {
2681
+ --sl-section-title-size: var(--sl-text-h5);
2682
+ }
2683
+
2684
+ .sl-markdown-content .sl-section-summary:has(> h6) {
2685
+ --sl-section-title-size: var(--sl-text-h6, var(--sl-text-base));
1568
2686
  }
1569
2687
 
1570
2688
  .sl-markdown-content .sl-section-summary::before {
1571
2689
  position: absolute;
1572
- left: -1.6em;
1573
- top: 50%;
2690
+ /* `<summary>` starts 0.5rem left of the text (`markdown.css`), so this
2691
+ centres the marker box on the middle of the gutter. */
2692
+ left: calc(
2693
+ 0.5rem - var(--ex-section-gutter, var(--sl-content-pad-x, 1rem)) / 2 -
2694
+ var(--sl-details-marker-size, 1.25rem) / 2
2695
+ );
2696
+ top: calc(var(--sl-section-title-size) * var(--sl-line-height-headings) / 2);
1574
2697
  margin: 0; /* cancel `markdown.css`'s inline-flow margin trick */
1575
2698
  transform: translateY(-50%);
1576
2699
  opacity: 0;
@@ -1602,6 +2725,109 @@ body {
1602
2725
  transition: opacity 100ms ease;
1603
2726
  }
1604
2727
 
2728
+ /* A closed section's cues beyond the chevron — the two `aria-hidden` spans
2729
+ `CollapsibleSections.astro` appends to `<summary>` after the heading.
2730
+ `.sl-section-hint` ("(click to expand)", "(tap to expand)" on touch) is
2731
+ for narrow/touch screens only (enabled in the block below);
2732
+ `.sl-section-stub` ("Hidden here: 2 paragraphs, 1 equation") shows on
2733
+ every screen. Both vanish once the section is `open` — the stub by
2734
+ collapsing its grid row, so a peek grown into a real expand slides the
2735
+ body up smoothly instead of jumping by a line. */
2736
+ .sl-markdown-content .sl-section-hint {
2737
+ display: none;
2738
+ margin-inline-start: 0.4em;
2739
+ font-size: var(--sl-text-sm);
2740
+ font-weight: 400;
2741
+ color: var(--sl-color-gray-3);
2742
+ white-space: nowrap;
2743
+ }
2744
+
2745
+ .sl-markdown-content .sl-section-hint::before {
2746
+ content: '(click to expand)';
2747
+ }
2748
+
2749
+ @media (hover: none) {
2750
+ .sl-markdown-content .sl-section-hint::before {
2751
+ content: '(tap to expand)';
2752
+ }
2753
+ }
2754
+
2755
+ .sl-markdown-content .sl-section-stub {
2756
+ display: grid;
2757
+ grid-template-rows: 0fr;
2758
+ opacity: 0;
2759
+ margin-block-start: 0;
2760
+ padding-block-end: 0;
2761
+ border-block-end: 0 dashed var(--sl-color-gray-5);
2762
+ text-indent: 0; /* the narrow-screen hanging indent is for the title only */
2763
+ font-size: var(--sl-text-sm);
2764
+ font-weight: 400;
2765
+ line-height: 1.5;
2766
+ color: var(--sl-color-gray-3);
2767
+ transition:
2768
+ grid-template-rows 220ms ease,
2769
+ opacity 150ms ease,
2770
+ margin-block-start 220ms ease,
2771
+ padding-block-end 220ms ease,
2772
+ border-block-end-width 220ms ease;
2773
+ }
2774
+
2775
+ .sl-markdown-content .sl-section-stub > span {
2776
+ overflow: hidden;
2777
+ min-height: 0;
2778
+ }
2779
+
2780
+ .sl-markdown-content details.sl-section-collapsible:not([data-state='open'])
2781
+ > .sl-section-summary
2782
+ > .sl-section-stub {
2783
+ grid-template-rows: 1fr;
2784
+ opacity: 1;
2785
+ margin-block-start: 0.3rem;
2786
+ padding-block-end: 0.45rem;
2787
+ border-block-end-width: 1px;
2788
+ }
2789
+
2790
+ /* Narrow and touch screens: there is no gutter to hold the chevron (on a
2791
+ phone it used to render off-screen), and no hover to reveal it on an open
2792
+ section. It moves into the title line and stays visible in both states —
2793
+ `›` closed, `⌄` open, so it also tells a reader the section can fold —
2794
+ with a hanging indent that keeps a wrapped title's later lines under its
2795
+ text, not under the chevron. The room reserved is the marker's own box
2796
+ minus its negative start margin plus its end gap (`markdown.css`).
2797
+ `screen` only: a printer reports `hover: none` too, and an inline heading
2798
+ would lose the print stylesheet's `break-after: avoid`. */
2799
+ @media screen and (width < 50em), screen and (hover: none) {
2800
+ .sl-markdown-content .sl-section-summary {
2801
+ --sl-section-marker-room: calc(
2802
+ var(--sl-details-marker-size, 1.25rem) * 0.75 + 0.25rem
2803
+ );
2804
+ padding-inline-start: calc(0.5rem + var(--sl-section-marker-room));
2805
+ text-indent: calc(-1 * var(--sl-section-marker-room));
2806
+ }
2807
+
2808
+ .sl-markdown-content .sl-section-summary > :is(h1, h2, h3, h4, h5, h6) {
2809
+ display: inline;
2810
+ }
2811
+
2812
+ .sl-markdown-content .sl-section-summary::before {
2813
+ position: static;
2814
+ vertical-align: -0.15em;
2815
+ transform: none;
2816
+ opacity: 1;
2817
+ transition: transform 150ms ease;
2818
+ }
2819
+
2820
+ .sl-markdown-content details.sl-section-collapsible[open] > .sl-section-summary::before {
2821
+ transform: rotateZ(90deg);
2822
+ }
2823
+
2824
+ .sl-markdown-content details.sl-section-collapsible:not([data-state='open'])
2825
+ > .sl-section-summary
2826
+ > .sl-section-hint {
2827
+ display: inline;
2828
+ }
2829
+ }
2830
+
1605
2831
  /* The section body: normally native `<details>` hiding/showing is all that's
1606
2832
  needed (nothing renders while closed), but `peeking` (a collapsed section
1607
2833
  hovered/focused — see `CollapsibleSections.astro`) has to force the
@@ -1650,13 +2876,16 @@ body {
1650
2876
 
1651
2877
  @media (prefers-reduced-motion: reduce) {
1652
2878
  .sl-markdown-content .sl-section-summary::before,
1653
- .sl-markdown-content .sl-section-body {
2879
+ .sl-markdown-content .sl-section-body,
2880
+ .sl-markdown-content .sl-section-stub {
1654
2881
  transition: none;
1655
2882
  }
1656
2883
  }
1657
2884
 
1658
2885
  @media print {
1659
- .sl-markdown-content .sl-section-summary::before {
2886
+ .sl-markdown-content .sl-section-summary::before,
2887
+ .sl-markdown-content .sl-section-hint,
2888
+ .sl-markdown-content .sl-section-stub {
1660
2889
  display: none !important;
1661
2890
  }
1662
2891
 
@@ -1664,14 +2893,13 @@ body {
1664
2893
  display: none;
1665
2894
  }
1666
2895
 
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`. */
2896
+ /* Print follows the screen, and CSS couldn't do otherwise anyway: the
2897
+ HTML spec has the browser skip rendering a closed <details>'s
2898
+ non-summary children outright — a rule no author CSS can override
2899
+ (`display`, `content-visibility`, `!important` included). What JS does
2900
+ at print time (`src/components/disclosure/print-as-shown.ts`) is settle
2901
+ a peeking or mid-animation section to the state it reads as, and add a
2902
+ "(content collapsed …)" note after each closed one. */
1675
2903
  }
1676
2904
 
1677
2905
  /* Table column narrowing + expand-on-overflow
@@ -1702,10 +2930,10 @@ body {
1702
2930
  `TableOverflowExpander.astro`'s script, and to every table on the site,
1703
2931
  not just ones wide enough to overflow. `color-mix` against `--sl-color-bg`
1704
2932
  (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. */
2933
+ the same technique `surfaces.css`'s accent tint uses, and for the same
2934
+ reason: a token like `--sl-color-gray-7` is only defined for one theme.
2935
+ The hover rule is unlayered and comes after the stripe rule, so it wins
2936
+ over an even/odd row's own stripe regardless of specificity. */
1709
2937
  .sl-markdown-content table:not(:where(.not-content *)) tbody tr:nth-child(even) {
1710
2938
  background: color-mix(in srgb, var(--sl-color-bg) 85%, var(--sl-color-gray-5) 15%);
1711
2939
  }
@@ -1731,6 +2959,19 @@ body {
1731
2959
  flex: 1 1 auto;
1732
2960
  min-width: 0;
1733
2961
  overflow-x: auto;
2962
+ /* A size container, so a row spanning the whole table (the "… N more
2963
+ rows …" separator, `Table.astro`) can size its label to the part of
2964
+ the table on screen (`100cqi`), not to its whole scrolled width. It
2965
+ takes its width from the flex row, never from its content, so the
2966
+ inline-size containment this implies changes nothing else. */
2967
+ container-type: inline-size;
2968
+ }
2969
+
2970
+ /* A preview card sizes itself to its content, and a size container has no
2971
+ width of its own to give it: in a card the table's width sizes the card,
2972
+ as it always did (`FloatRefLayer.astro`). */
2973
+ .hover-panel .table-overflow-scroll {
2974
+ container-type: normal;
1734
2975
  }
1735
2976
 
1736
2977
  /* A scrollable region must be reachable by keyboard (WCAG 2.1.1) — the
@@ -1898,6 +3139,7 @@ body {
1898
3139
  .right-sidebar-container,
1899
3140
  .pagination-links,
1900
3141
  .layout-edge-toggle,
3142
+ .layout-edge-resizer,
1901
3143
  [data-explico-export-chrome],
1902
3144
  [data-popup-root],
1903
3145
  [data-citation-panel],
@@ -1909,11 +3151,8 @@ body {
1909
3151
  .table-overflow-expand-btn,
1910
3152
  .keyed-table__pagination-links,
1911
3153
  .keyed-table__more-row,
1912
- .diagram-toolbar,
1913
3154
  .sl-anchor-link,
1914
- .keyed-figure__title-anchor,
1915
- .diagram__title-anchor,
1916
- .memo-block__title-anchor,
3155
+ .float-caption__anchor,
1917
3156
  .short-note__collapse,
1918
3157
  .assessment-actions,
1919
3158
  .assessment-chevron,
@@ -1956,9 +3195,6 @@ body {
1956
3195
  }
1957
3196
 
1958
3197
  .sl-markdown-content :is(h1, h2, h3, h4, h5, h6),
1959
- .keyed-figure__summary,
1960
- .diagram__summary,
1961
- .memo-block__summary,
1962
3198
  .assessment-item-heading {
1963
3199
  break-after: avoid-page;
1964
3200
  page-break-after: avoid;
@@ -1978,7 +3214,7 @@ body {
1978
3214
  .compact-example,
1979
3215
  .assessment-item,
1980
3216
  .lab-result,
1981
- .lab-chart-figure,
3217
+ .lab-chart,
1982
3218
  .sl-markdown-content pre,
1983
3219
  .sl-markdown-content blockquote {
1984
3220
  break-inside: avoid-page;
@@ -2038,15 +3274,15 @@ body {
2038
3274
  display: table-row !important;
2039
3275
  }
2040
3276
 
2041
- .assessment-panel[hidden],
2042
- .short-note[hidden],
3277
+ /* An open example set's inactive tabs are hidden only to save screen
3278
+ space. A closed question panel or short note is the reader's own
3279
+ disclosure, and print follows the screen for those — it stays closed
3280
+ and gets a `.explico-collapsed-note` (below) instead. */
2043
3281
  .compact-example[hidden] {
2044
3282
  display: block !important;
2045
3283
  }
2046
3284
 
2047
3285
  .diagram,
2048
- .diagram__titlebar,
2049
- .diagram__details,
2050
3286
  .diagram__body,
2051
3287
  .diagram-stage,
2052
3288
  .diagram-scroll {
@@ -2077,6 +3313,32 @@ body {
2077
3313
  height: 100% !important;
2078
3314
  }
2079
3315
 
3316
+ /* WebKit (Safari, every iOS browser) paints a `foreignObject` descendant
3317
+ with its own render layer — every KaTeX `position: relative` span —
3318
+ without the SVG's internal transforms, the `viewBox` scale above
3319
+ included, so a printed diagram loses its formulas. Where CSS can divide
3320
+ lengths, keep the SVG at its build-time size and scale the HTML canvas
3321
+ instead; the negative block margin gives the layout box the scaled
3322
+ height. Without that support the `viewBox` scaling above stays. */
3323
+ @supports (transform: scale(calc(100cqw / 1px))) {
3324
+ .diagram-scroll {
3325
+ container-type: inline-size;
3326
+ }
3327
+
3328
+ .diagram-canvas {
3329
+ width: calc(var(--diagram-width) * 1px) !important;
3330
+ max-width: none !important;
3331
+ height: calc(var(--diagram-height) * 1px) !important;
3332
+ aspect-ratio: auto;
3333
+ margin-block-end: calc(
3334
+ 100cqw * var(--diagram-height) / var(--diagram-width) -
3335
+ var(--diagram-height) * 1px
3336
+ );
3337
+ transform: scale(calc(100cqw / (var(--diagram-width) * 1px)));
3338
+ transform-origin: 0 0;
3339
+ }
3340
+ }
3341
+
2080
3342
  .lab-chart,
2081
3343
  .lab-math,
2082
3344
  .katex-display {
@@ -2085,10 +3347,14 @@ body {
2085
3347
  overflow: visible !important;
2086
3348
  }
2087
3349
 
3350
+ /* Not KaTeX's own SVGs: a `\sqrt` radical (and stretchy arrows/accents)
3351
+ is a deliberately oversized SVG clipped by its parent, and its height
3352
+ comes from KaTeX's `height` attribute — `height: auto` resolved it to
3353
+ 0, so every printed square root lost its radical sign. */
2088
3354
  .lab-chart svg,
2089
3355
  .keyed-figure img,
2090
3356
  .sl-markdown-content img,
2091
- .sl-markdown-content svg {
3357
+ .sl-markdown-content svg:not(.katex *) {
2092
3358
  max-width: 100% !important;
2093
3359
  height: auto;
2094
3360
  }
@@ -2108,14 +3374,18 @@ body {
2108
3374
  }
2109
3375
 
2110
3376
  :is(
3377
+ .ex-tier-meta,
3378
+ .ex-tier-accent,
3379
+ .ex-tier-aside,
3380
+ .starlight-aside,
3381
+ .sl-markdown-content article.card,
2111
3382
  .lab-shell,
2112
3383
  .lab-result,
2113
3384
  .assessment-shell,
2114
3385
  .assessment-item,
2115
3386
  .memo-block-frame,
2116
- .keyed-figure__details,
2117
3387
  .compact-examples,
2118
- .diagram__details
3388
+ .block-badge
2119
3389
  ) {
2120
3390
  color: #111 !important;
2121
3391
  border-color: #777 !important;
@@ -2130,4 +3400,38 @@ body {
2130
3400
  -webkit-print-color-adjust: exact;
2131
3401
  }
2132
3402
 
3403
+ /* "(content collapsed — full text: …)" under a disclosure the reader left
3404
+ closed (`src/components/disclosure/print-as-shown.ts`). Sized in `rem`:
3405
+ a section's note sits inside its heading-sized title bar. */
3406
+ .explico-collapsed-note {
3407
+ display: block;
3408
+ margin-block: 0.15rem 0.75rem;
3409
+ font-size: 0.8rem;
3410
+ font-style: italic;
3411
+ font-weight: 400;
3412
+ line-height: 1.4;
3413
+ color: #444;
3414
+ overflow-wrap: anywhere;
3415
+ text-indent: 0;
3416
+ }
3417
+
3418
+ .explico-collapsed-note a[href] {
3419
+ color: #08775b !important;
3420
+ text-decoration: underline;
3421
+ print-color-adjust: exact;
3422
+ -webkit-print-color-adjust: exact;
3423
+ }
3424
+
3425
+ .memo-block-frame .explico-collapsed-note {
3426
+ margin: 0;
3427
+ padding-block-end: 0.25rem;
3428
+ }
3429
+ }
3430
+
3431
+ /* The note exists only while a print is being prepared, and never shows on
3432
+ screen even then (the print button prepares before the dialog opens). */
3433
+ @media not print {
3434
+ .explico-collapsed-note {
3435
+ display: none;
3436
+ }
2133
3437
  }