explico 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CHANGELOG.md +344 -1
  2. package/README.md +117 -3
  3. package/dist/compiler/astro.d.ts +38 -0
  4. package/dist/compiler/astro.js +258 -0
  5. package/dist/compiler/cli.d.ts +13 -0
  6. package/dist/compiler/cli.js +32 -12
  7. package/dist/compiler/collections.d.ts +3 -2
  8. package/dist/compiler/collections.js +28 -1
  9. package/dist/compiler/course-config.d.ts +86 -4
  10. package/dist/compiler/course-config.js +39 -0
  11. package/dist/compiler/exports/standalone-html.d.ts +34 -0
  12. package/dist/compiler/exports/standalone-html.js +803 -0
  13. package/dist/compiler/lesson-derivation.d.ts +1 -1
  14. package/dist/compiler/lesson-derivation.js +2 -2
  15. package/dist/compiler/manifest.d.ts +112 -7
  16. package/dist/compiler/manifest.js +205 -40
  17. package/dist/compiler/resolution-report.d.ts +4 -0
  18. package/dist/reference/base-abbreviations.d.mts +30 -0
  19. package/dist/reference/base-abbreviations.mjs +60 -0
  20. package/dist/reference/base-operators.d.mts +15 -0
  21. package/dist/reference/base-operators.mjs +40 -0
  22. package/dist/reference/base-roman-labels.d.mts +35 -0
  23. package/dist/reference/base-roman-labels.mjs +43 -0
  24. package/dist/reference/consistency.d.ts +8 -2
  25. package/dist/reference/consistency.js +50 -3
  26. package/dist/reference/diagrams-validate.d.ts +21 -0
  27. package/dist/reference/diagrams-validate.js +13 -0
  28. package/dist/reference/equations-validate.d.ts +8 -3
  29. package/dist/reference/equations-validate.js +1 -1
  30. package/dist/reference/equations.d.mts +66 -35
  31. package/dist/reference/equations.mjs +142 -40
  32. package/dist/reference/gate-abbreviations.d.ts +17 -0
  33. package/dist/reference/gate-abbreviations.js +64 -0
  34. package/dist/reference/gate-math.d.ts +28 -5
  35. package/dist/reference/gate-math.js +5 -2
  36. package/dist/reference/gate-prose-notation.d.ts +60 -0
  37. package/dist/reference/gate-prose-notation.js +125 -0
  38. package/dist/reference/keyed-float-validate.d.ts +14 -8
  39. package/dist/reference/keyed-float-validate.js +1 -1
  40. package/dist/reference/keyed-jsx-floats.d.mts +24 -21
  41. package/dist/reference/keyed-jsx-floats.mjs +90 -41
  42. package/dist/reference/knowledge-checks-validate.d.ts +35 -0
  43. package/dist/reference/knowledge-checks-validate.js +78 -0
  44. package/dist/reference/knowledge-checks.d.mts +25 -0
  45. package/dist/reference/knowledge-checks.mjs +45 -0
  46. package/dist/reference/lab-math-scope.d.ts +9 -1
  47. package/dist/reference/lab-math-scope.js +32 -4
  48. package/dist/reference/math-glyphs.d.mts +30 -0
  49. package/dist/reference/math-glyphs.mjs +367 -0
  50. package/dist/reference/mdx-attributes.d.mts +13 -0
  51. package/dist/reference/mdx-attributes.mjs +38 -0
  52. package/dist/reference/mixed-content.d.ts +17 -0
  53. package/dist/reference/mixed-content.js +29 -0
  54. package/dist/reference/page-notation.d.ts +52 -0
  55. package/dist/reference/page-notation.js +171 -0
  56. package/dist/reference/raw-floats-validate.d.ts +18 -0
  57. package/dist/reference/raw-floats-validate.js +53 -0
  58. package/dist/reference/raw-floats.d.mts +18 -0
  59. package/dist/reference/raw-floats.mjs +129 -0
  60. package/dist/reference/rehype-heading-anchors.d.mts +1 -1
  61. package/dist/reference/rehype-heading-anchors.mjs +107 -1
  62. package/dist/reference/rehype-table-header-labels.d.mts +26 -0
  63. package/dist/reference/rehype-table-header-labels.mjs +152 -0
  64. package/dist/reference/remark-abbreviations.d.mts +7 -0
  65. package/dist/reference/remark-abbreviations.mjs +174 -0
  66. package/dist/reference/remark-memo.d.mts +16 -0
  67. package/dist/reference/remark-memo.mjs +339 -0
  68. package/dist/reference/remark-notation.d.mts +102 -5
  69. package/dist/reference/remark-notation.mjs +506 -50
  70. package/dist/reference/remark-short-note.d.mts +4 -0
  71. package/dist/reference/remark-short-note.mjs +237 -0
  72. package/dist/reference/types.d.ts +16 -1
  73. package/dist/reference/units-vocab.js +4 -0
  74. package/docs/VERSIONING.md +33 -4
  75. package/docs/authoring/README.md +12 -0
  76. package/docs/authoring/agents.md +17 -2
  77. package/docs/authoring/components.md +135 -0
  78. package/docs/authoring/migrating.md +154 -7
  79. package/docs/authoring/notation.md +568 -8
  80. package/docs/authoring/structure.md +251 -0
  81. package/docs/authoring/visualizations.md +36 -3
  82. package/package.json +5 -2
  83. package/src/compiler/astro.ts +332 -0
  84. package/src/compiler/cli.ts +33 -10
  85. package/src/compiler/collections.ts +45 -4
  86. package/src/compiler/course-config.ts +131 -3
  87. package/src/compiler/exports/standalone-html.ts +963 -0
  88. package/src/compiler/lesson-derivation.ts +2 -2
  89. package/src/compiler/manifest.ts +367 -49
  90. package/src/components/HoverPanel.astro +45 -4
  91. package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
  92. package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
  93. package/src/components/assessments/AssessmentRunner.tsx +52 -17
  94. package/src/components/assessments/AssessmentSet.astro +98 -14
  95. package/src/components/assessments/RenderedContent.tsx +55 -0
  96. package/src/components/assessments/assessment-randomization.ts +16 -0
  97. package/src/components/diagram/Diagram.astro +444 -83
  98. package/src/components/disclosure/hover-preview.ts +202 -0
  99. package/src/components/disclosure/persisted-disclosure.ts +66 -0
  100. package/src/components/examples/CompactExamples.astro +2 -0
  101. package/src/components/figure/Figure.astro +279 -10
  102. package/src/components/memo/Memo.astro +453 -0
  103. package/src/components/memo/ShortNote.astro +223 -0
  104. package/src/components/notation/NotationFormula.astro +18 -1
  105. package/src/components/notation/NotationLayer.astro +116 -8
  106. package/src/components/notation/render-notation-math.ts +39 -2
  107. package/src/components/notation/safe-math-to-react.ts +50 -0
  108. package/src/components/reference/FloatRefLayer.astro +233 -15
  109. package/src/components/starlight/CollapsibleSections.astro +461 -0
  110. package/src/components/starlight/DiagramEnhancer.astro +385 -0
  111. package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
  112. package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
  113. package/src/components/starlight/LayoutHeader.astro +50 -0
  114. package/src/components/starlight/LayoutSidebar.astro +62 -1
  115. package/src/components/starlight/LayoutSocialIcons.astro +41 -2
  116. package/src/components/starlight/LessonExportActions.astro +436 -0
  117. package/src/components/starlight/LessonFooter.astro +38 -2
  118. package/src/components/starlight/section-parts.ts +31 -0
  119. package/src/components/table/Table.astro +296 -9
  120. package/src/components/table/TableOverflowExpander.astro +489 -0
  121. package/src/content-config.ts +20 -0
  122. package/src/reference/base-abbreviations.mjs +60 -0
  123. package/src/reference/base-operators.mjs +40 -0
  124. package/src/reference/base-roman-labels.mjs +43 -0
  125. package/src/reference/consistency.ts +73 -2
  126. package/src/reference/diagrams-validate.ts +40 -0
  127. package/src/reference/equations-validate.ts +6 -4
  128. package/src/reference/equations.mjs +142 -40
  129. package/src/reference/gate-abbreviations.ts +128 -0
  130. package/src/reference/gate-math.ts +27 -7
  131. package/src/reference/gate-prose-notation.ts +224 -0
  132. package/src/reference/keyed-float-validate.ts +12 -9
  133. package/src/reference/keyed-jsx-floats.mjs +90 -41
  134. package/src/reference/knowledge-checks-validate.ts +130 -0
  135. package/src/reference/knowledge-checks.mjs +45 -0
  136. package/src/reference/lab-math-scope.ts +45 -3
  137. package/src/reference/math-glyphs.mjs +367 -0
  138. package/src/reference/mdx-attributes.mjs +38 -0
  139. package/src/reference/mixed-content.ts +69 -0
  140. package/src/reference/page-notation.ts +252 -0
  141. package/src/reference/raw-floats-validate.ts +103 -0
  142. package/src/reference/raw-floats.mjs +129 -0
  143. package/src/reference/rehype-heading-anchors.mjs +107 -1
  144. package/src/reference/rehype-table-header-labels.mjs +152 -0
  145. package/src/reference/remark-abbreviations.mjs +174 -0
  146. package/src/reference/remark-memo.mjs +339 -0
  147. package/src/reference/remark-notation.mjs +506 -50
  148. package/src/reference/remark-short-note.mjs +237 -0
  149. package/src/reference/types.ts +16 -0
  150. package/src/reference/units-vocab.ts +4 -0
  151. package/src/styles/global.css +1125 -50
@@ -13,12 +13,43 @@
13
13
  --sl-color-accent-high: #123c32;
14
14
  }
15
15
 
16
+ /* Every intentionally wide surface owns a local horizontal scroller. Keep a
17
+ replaced element (notably an SVG containing `foreignObject`) from widening
18
+ iOS WebKit's layout viewport before that local overflow rule takes effect.
19
+ `clip` does not create the accidental vertical scroll container that
20
+ `overflow-x: hidden` would create when the other axis remains visible. */
21
+ html,
22
+ body {
23
+ overflow-x: clip;
24
+ }
25
+
16
26
  .lab-shell {
17
27
  margin-block: 1.5rem;
18
28
  padding: clamp(1rem, 3vw, 1.5rem);
19
29
  border: 1px solid var(--sl-color-gray-5);
20
- border-radius: 0.75rem;
21
- background: var(--sl-color-gray-7);
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. */
46
+ .keyed-figure__body .lab-shell {
47
+ margin-block: 0;
48
+ padding: 0;
49
+ border: none;
50
+ border-radius: 0;
51
+ background: none;
52
+ box-shadow: none;
22
53
  }
23
54
 
24
55
  .lab-shell label,
@@ -206,7 +237,7 @@
206
237
  }
207
238
 
208
239
  .assessment-shell {
209
- margin-block: 1.5rem;
240
+ margin-block: 0.75rem 1.5rem;
210
241
  padding: clamp(1rem, 3vw, 1.5rem);
211
242
  border: 1px solid var(--sl-color-gray-5);
212
243
  border-radius: 0.75rem;
@@ -218,23 +249,20 @@
218
249
  outline-offset: 2px;
219
250
  }
220
251
 
221
- .assessment-header {
222
- display: flex;
223
- flex-wrap: wrap;
224
- align-items: baseline;
225
- justify-content: space-between;
226
- gap: 0.5rem 1rem;
227
- margin-bottom: 1rem;
252
+ .assessment-heading-number {
253
+ color: var(--sl-color-text-accent);
254
+ white-space: nowrap;
228
255
  }
229
256
 
230
- .assessment-header h3 {
231
- margin: 0;
257
+ .assessment-shell > h3 {
258
+ margin-block: 0 0.75rem;
232
259
  }
233
260
 
234
261
  .assessment-progress {
235
- margin: 0;
262
+ margin: 0 0 0.75rem;
236
263
  font-size: 0.85rem;
237
264
  font-variant-numeric: tabular-nums;
265
+ text-align: end;
238
266
  color: var(--sl-color-gray-2);
239
267
  }
240
268
 
@@ -291,13 +319,22 @@
291
319
 
292
320
  .assessment-chevron {
293
321
  flex: none;
294
- transition: transform 0.15s ease;
322
+ transition:
323
+ opacity 150ms ease 700ms,
324
+ transform 150ms ease;
295
325
  }
296
326
 
297
327
  .assessment-disclosure[aria-expanded='true'] .assessment-chevron {
328
+ opacity: 0;
298
329
  transform: rotate(180deg);
299
330
  }
300
331
 
332
+ .assessment-disclosure[aria-expanded='true']:hover .assessment-chevron,
333
+ .assessment-disclosure[aria-expanded='true']:focus-visible .assessment-chevron {
334
+ opacity: 1;
335
+ transition: opacity 100ms ease;
336
+ }
337
+
301
338
  .assessment-panel {
302
339
  padding: 0 1rem 1rem;
303
340
  border-top: 1px solid var(--sl-color-gray-5);
@@ -445,7 +482,136 @@
445
482
  }
446
483
 
447
484
  .layout-navigation-panel {
448
- min-height: 100%;
485
+ min-height: auto;
486
+ }
487
+
488
+ /* A compact chapter remains useful as a row of lesson icons. The
489
+ link text doubles as a pointer/keyboard tooltip; opening the chapter via
490
+ its summary restores Starlight's normal, fully labelled list. */
491
+ .layout-compact-sidebar
492
+ details[data-compact]
493
+ > ul:not(.top-level) {
494
+ display: flex;
495
+ flex-wrap: wrap;
496
+ gap: 0.35rem;
497
+ margin: 0.2rem 0 0.35rem;
498
+ padding-inline-start: 0.5rem;
499
+ }
500
+
501
+ .layout-compact-sidebar
502
+ details[data-compact]
503
+ > ul:not(.top-level)
504
+ > li {
505
+ position: relative;
506
+ margin-inline-start: 0;
507
+ border-inline-start: 0;
508
+ padding-inline-start: 0;
509
+ }
510
+
511
+ .layout-compact-sidebar
512
+ details[data-compact]
513
+ > ul:not(.top-level)
514
+ > li
515
+ > a {
516
+ display: grid;
517
+ position: relative;
518
+ place-items: center;
519
+ width: 1.75rem;
520
+ height: 1.75rem;
521
+ border: 1px solid var(--sl-color-gray-5);
522
+ border-radius: 0.4rem;
523
+ padding: 0;
524
+ color: var(--sl-color-gray-3);
525
+ background: var(--sl-color-bg-sidebar);
526
+ }
527
+
528
+ .layout-compact-sidebar
529
+ details[data-compact]
530
+ > ul:not(.top-level)
531
+ > li
532
+ > a::before {
533
+ content: '';
534
+ width: 0.65rem;
535
+ height: 0.78rem;
536
+ border: 1.5px solid currentColor;
537
+ border-radius: 0.13rem;
538
+ box-shadow: 0.18rem 0.18rem 0 -0.1rem var(--sl-color-bg-sidebar),
539
+ 0.18rem 0.18rem 0 0 currentColor;
540
+ }
541
+
542
+ .layout-compact-sidebar
543
+ details[data-compact]
544
+ > ul:not(.top-level)
545
+ > li
546
+ > a:hover,
547
+ .layout-compact-sidebar
548
+ details[data-compact]
549
+ > ul:not(.top-level)
550
+ > li
551
+ > a:focus-visible {
552
+ border-color: var(--sl-color-accent);
553
+ color: var(--sl-color-text-accent);
554
+ background: var(--sl-color-accent-low);
555
+ }
556
+
557
+ .layout-compact-sidebar
558
+ details[data-compact]
559
+ > ul:not(.top-level)
560
+ > li
561
+ > a[aria-current='page'] {
562
+ color: var(--sl-color-text-invert);
563
+ background: var(--sl-color-text-accent);
564
+ }
565
+
566
+ .layout-compact-sidebar
567
+ details[data-compact]
568
+ > ul:not(.top-level)
569
+ > li
570
+ > a
571
+ > span {
572
+ position: absolute;
573
+ z-index: 2;
574
+ inset-block-start: 50%;
575
+ inset-inline-start: calc(100% + 0.45rem);
576
+ width: max-content;
577
+ max-width: 12rem;
578
+ border: 1px solid var(--sl-color-gray-5);
579
+ border-radius: 0.35rem;
580
+ padding: 0.3rem 0.5rem;
581
+ color: var(--sl-color-white);
582
+ background: var(--sl-color-bg-nav);
583
+ box-shadow: var(--sl-shadow-md);
584
+ font-size: var(--sl-text-xs);
585
+ font-weight: 500;
586
+ line-height: 1.3;
587
+ opacity: 0;
588
+ pointer-events: none;
589
+ transform: translateY(-50%) translateX(-0.2rem);
590
+ transition:
591
+ opacity 100ms ease,
592
+ transform 100ms ease;
593
+ }
594
+
595
+ .layout-compact-sidebar
596
+ details[data-compact]
597
+ > ul:not(.top-level)
598
+ > li
599
+ > a:is(:hover, :focus-visible)
600
+ > span {
601
+ opacity: 1;
602
+ transform: translateY(-50%) translateX(0);
603
+ }
604
+
605
+ .layout-compact-sidebar details[data-compact] > summary .caret {
606
+ transform: rotateZ(0);
607
+ }
608
+
609
+ [dir='rtl']
610
+ .layout-compact-sidebar
611
+ details[data-compact]
612
+ > summary
613
+ .caret {
614
+ transform: rotateZ(180deg);
449
615
  }
450
616
 
451
617
  .layout-navigation-toggle {
@@ -498,25 +664,83 @@
498
664
  }
499
665
 
500
666
  @media (min-width: 72rem) {
501
- :root {
502
- /* Mirrors Starlight's responsive right-column allocation. The main frame
503
- is the viewport minus the current navigation width, which can itself be
504
- the collapsed edge rail. */
505
- --layout-page-sidebar-open-width: max(
506
- var(--sl-sidebar-width),
507
- calc(
508
- var(--sl-sidebar-width) +
509
- (
510
- 100vw - var(--sl-content-inline-start, 0rem) -
511
- var(--sl-content-width) - var(--sl-sidebar-width)
512
- ) /
513
- 2
514
- )
515
- );
667
+ /* Starlight's own formula (`TwoColumnContent.astro`) makes this column at
668
+ least as wide as the left nav, plus half of whatever space is left over
669
+ past the article's own max width — on a wide monitor that can be a very
670
+ wide rail for a handful of short heading links. `width: max-content`
671
+ shrink-wraps it to exactly as wide as its longest single-line item
672
+ needs (browsers compute that unwrapped-line width unprompted — the same
673
+ thing that makes it wrap at any *narrower* width), and `max-width: 80ch`
674
+ caps that for a page with one genuinely long heading, at which point it
675
+ wraps rather than growing further. Both the resting state and the
676
+ hover-to-peek state (below, while collapsed to the edge rail) use the
677
+ same two declarations, so peeking never opens to a different width than
678
+ the rail would normally sit at. */
679
+ .right-sidebar-container {
680
+ width: max-content;
681
+ max-width: 80ch;
516
682
  }
517
683
 
518
684
  .right-sidebar {
685
+ width: max-content;
686
+ max-width: 80ch;
519
687
  background-color: var(--sl-color-bg);
688
+ /* Anchor to the viewport edge instead of Starlight's default static
689
+ position (wherever `.right-sidebar-container`'s reserved flex column
690
+ happens to land, which drifts left as `.main-pane` grows on a wide
691
+ screen — see below). This is the *same* anchor, `inset-inline-end: 0`,
692
+ the collapsed rail and its hover-to-peek state already use a few
693
+ rules down — matched exactly, not just similarly, so opening the
694
+ rail moves nothing: without this it jumped from flush against the
695
+ edge (peeking) to wherever `.main-pane` happened to end (open), often
696
+ a very different spot on a wide screen even though the box itself is
697
+ the same size both times, reading as a new, apparently larger panel
698
+ rather than the same one staying put. */
699
+ inset-inline-end: 0;
700
+ }
701
+
702
+ /* Starlight's own `.main-pane` width formula (`TwoColumnContent.astro`,
703
+ first two `min()` branches below, unchanged) grows the reading column
704
+ past its own comfortable measure (`--sl-content-width`) by handing it
705
+ half of whatever space is left over after the left nav — sized on the
706
+ assumption that the toc might need the *other* half. `.right-sidebar`
707
+ above no longer does (it shrinks to fit its content) and, now that it's
708
+ edge-anchored rather than positioned relative to `.main-pane`, growing
709
+ `.main-pane` too far really can run it into the toc instead of just
710
+ leaving unused space the way it would have under the old static
711
+ positioning (which always placed the toc *after* `.main-pane`,
712
+ whatever its width — the one thing that positioning had going for it).
713
+ The third branch is that safety net: never let `.main-pane` extend
714
+ within `--toc-gap` of where the (edge-anchored) toc's own left edge
715
+ is. `--toc-actual-width` is `LayoutHeader.astro`'s live measurement of
716
+ `.right-sidebar`'s real rendered width, set once it's actually on
717
+ screen — plain CSS has no way to know that ahead of time, since a
718
+ `position: fixed` element's size normally can't feed back into a
719
+ sibling's layout the way an ordinary flex child's would. Until that
720
+ measurement lands (or without JS), `--toc-reserved-fallback` covers
721
+ the worst case the toc could ever actually be: its own 80ch cap above,
722
+ plus the same margin. Because the toc is now independently
723
+ edge-anchored (not positioned from `.main-pane` any more), measuring
724
+ it doesn't close a loop back into this formula the way it would have
725
+ under the old positioning. */
726
+ :root {
727
+ --toc-gap: 2rem;
728
+ --toc-reserved-fallback: calc(80ch + 3rem);
729
+ }
730
+
731
+ html[data-has-sidebar][data-has-toc]:not([data-page-sidebar-hidden])
732
+ .main-pane {
733
+ width: min(
734
+ calc(100% - var(--sl-sidebar-width)),
735
+ calc(
736
+ var(--sl-content-width) +
737
+ (100% - var(--sl-content-width) - var(--sl-sidebar-width)) / 2
738
+ ),
739
+ calc(
740
+ 100% - var(--toc-gap) -
741
+ var(--toc-actual-width, var(--toc-reserved-fallback))
742
+ )
743
+ );
520
744
  }
521
745
 
522
746
  .layout-page-sidebar {
@@ -572,7 +796,8 @@
572
796
 
573
797
  html[data-page-sidebar-hidden]:not([data-page-sidebar-peek-suppressed])
574
798
  .right-sidebar:hover {
575
- width: var(--layout-page-sidebar-open-width);
799
+ width: max-content;
800
+ max-width: 80ch;
576
801
  overflow-y: auto;
577
802
  box-shadow: var(--sl-shadow-lg);
578
803
  }
@@ -664,16 +889,32 @@
664
889
  }
665
890
  }
666
891
 
892
+ /* Native browser tooltips: `<span title="…">` in Markdown content. A dotted
893
+ underline and hover highlight — same visual treatment as abbreviation terms
894
+ (N14), distinguishing them from plain text and signalling a hover event. */
895
+ .sl-markdown-content span[title] {
896
+ text-decoration: underline dotted;
897
+ text-underline-offset: 0.2em;
898
+ cursor: help;
899
+ }
900
+
901
+ .sl-markdown-content span[title]:hover,
902
+ .sl-markdown-content span[title]:focus-visible {
903
+ background: var(--sl-color-accent-low);
904
+ color: var(--sl-color-text-accent);
905
+ }
906
+
667
907
  /* Equation identity (D7): a keyed `$$…$$` is wrapped by the remark adapter in
668
908
  `.keyed-equation` with an `id="eq-<key>"`, a `data-eq-number`, and a
669
- focusable number link. Prose refers to it with `[[eq:key]]` →
670
- `a.equation-ref`. The deep-link highlight is a pure-CSS `:target` animation
671
- with a reduced-motion fallback; `KeyedFloatEnhancer.astro` only re-triggers it
672
- on a repeated activation and moves focus. */
909
+ `.keyed-equation__controls` pair — a focusable number link, and a "Copy
910
+ LaTeX" button. Prose refers to it with `[[eq:key]]` → `a.equation-ref`. The
911
+ deep-link highlight is a pure-CSS `:target` animation with a
912
+ reduced-motion fallback; `KeyedFloatEnhancer.astro` only re-triggers it on
913
+ a repeated activation and moves focus. */
673
914
  .sl-markdown-content .keyed-equation {
674
915
  position: relative;
675
916
  scroll-margin-block-start: 5rem;
676
- padding-inline-end: 3.5rem;
917
+ padding-inline-end: 5.25rem;
677
918
  border-radius: 0.3rem;
678
919
  }
679
920
 
@@ -681,11 +922,17 @@
681
922
  margin-block: 0;
682
923
  }
683
924
 
684
- .sl-markdown-content a.keyed-equation__number {
925
+ .sl-markdown-content .keyed-equation__controls {
685
926
  position: absolute;
686
927
  inset-inline-end: 0.25rem;
687
928
  inset-block-start: 50%;
688
929
  transform: translateY(-50%);
930
+ display: inline-flex;
931
+ align-items: center;
932
+ gap: 0.4rem;
933
+ }
934
+
935
+ .sl-markdown-content a.keyed-equation__number {
689
936
  font-variant-numeric: tabular-nums;
690
937
  font-size: var(--sl-text-sm);
691
938
  color: var(--sl-color-text-accent);
@@ -698,9 +945,43 @@
698
945
  transition: opacity 120ms ease-out;
699
946
  }
700
947
 
948
+ .sl-markdown-content .keyed-equation__copy {
949
+ display: inline-flex;
950
+ align-items: center;
951
+ justify-content: center;
952
+ width: 1.5rem;
953
+ height: 1.5rem;
954
+ padding: 0;
955
+ border: 1px solid var(--sl-color-gray-5);
956
+ border-radius: 999px;
957
+ color: var(--sl-color-gray-1);
958
+ background: transparent;
959
+ font: inherit;
960
+ cursor: pointer;
961
+ /* Quiet by default for the same reason as the number above — it fades in
962
+ alongside it, not the equation, so print (which drops it entirely, see
963
+ below) never has to reconcile the two. */
964
+ opacity: 0.45;
965
+ transition:
966
+ opacity 120ms ease-out,
967
+ border-color 120ms ease-out,
968
+ color 120ms ease-out;
969
+ }
970
+
971
+ .sl-markdown-content .keyed-equation__copy svg {
972
+ width: 0.85rem;
973
+ height: 0.85rem;
974
+ fill: none;
975
+ stroke: currentColor;
976
+ stroke-width: 1.75;
977
+ stroke-linecap: round;
978
+ stroke-linejoin: round;
979
+ }
980
+
701
981
  @media (hover: none) {
702
- /* No hover to reveal it on touch, so keep the number legible there. */
703
- .sl-markdown-content a.keyed-equation__number {
982
+ /* No hover to reveal them on touch, so keep both legible there. */
983
+ .sl-markdown-content a.keyed-equation__number,
984
+ .sl-markdown-content .keyed-equation__copy {
704
985
  opacity: 1;
705
986
  }
706
987
  }
@@ -713,14 +994,111 @@
713
994
  text-decoration: underline;
714
995
  }
715
996
 
997
+ .sl-markdown-content .keyed-equation:hover .keyed-equation__copy,
998
+ .sl-markdown-content .keyed-equation:focus-within .keyed-equation__copy,
999
+ .sl-markdown-content .keyed-equation:target .keyed-equation__copy,
1000
+ .sl-markdown-content .keyed-equation__copy:focus-visible {
1001
+ opacity: 1;
1002
+ }
1003
+
1004
+ .sl-markdown-content .keyed-equation__copy:hover,
1005
+ .sl-markdown-content .keyed-equation__copy:focus-visible {
1006
+ border-color: var(--sl-color-accent);
1007
+ color: var(--sl-color-text-accent);
1008
+ }
1009
+
1010
+ .sl-markdown-content .keyed-equation__copy:focus-visible {
1011
+ outline: 2px solid var(--sl-color-accent);
1012
+ outline-offset: 2px;
1013
+ }
1014
+
1015
+ /* The click-to-copy gesture (`KeyedFloatEnhancer.astro`) works on any part of
1016
+ the equation that isn't a symbol with its own notation card — signal that
1017
+ with a "copy" cursor there, while an explained symbol keeps its existing
1018
+ "help" cursor (`NotationLayer.astro`). The second rule must out-specificity
1019
+ the first regardless of which stylesheet loads later, so it repeats the
1020
+ `.katex-html` step rather than relying on cascade order between files. */
1021
+ .sl-markdown-content .keyed-equation .katex-html,
1022
+ .sl-markdown-content .inline-equation .katex-html {
1023
+ cursor: copy;
1024
+ }
1025
+
1026
+ .sl-markdown-content .keyed-equation .katex-html [data-notation-key],
1027
+ .sl-markdown-content .inline-equation .katex-html [data-notation-key] {
1028
+ cursor: help;
1029
+ }
1030
+
1031
+ /* Hovering specifically an unexplained part (not a notation symbol) of a
1032
+ display equation highlights its copy button, so a reader can tell what
1033
+ clicking there will do before they click. `:has()` is well-supported by
1034
+ now; nothing breaks without it, the highlight just never appears. */
1035
+ .sl-markdown-content
1036
+ .keyed-equation:has(.katex-html:hover):not(:has([data-notation-key]:hover))
1037
+ .keyed-equation__copy {
1038
+ opacity: 1;
1039
+ border-color: var(--sl-color-accent);
1040
+ color: var(--sl-color-text-accent);
1041
+ }
1042
+
1043
+ /* `data-copied` is set briefly by the click handler in `KeyedFloatEnhancer.astro`,
1044
+ the same "flash a tooltip, then remove the attribute" pattern as the
1045
+ glossary's copy-link button (`NotationGlossary.astro`) and the notation
1046
+ popup card's own copy button (`NotationLayer.astro`). */
1047
+ .sl-markdown-content .keyed-equation__copy[data-copied] {
1048
+ opacity: 1;
1049
+ }
1050
+
1051
+ .sl-markdown-content .keyed-equation__copy[data-copied]::after {
1052
+ content: 'Copied!';
1053
+ position: absolute;
1054
+ inset-block-end: calc(100% + 0.4rem);
1055
+ inset-inline-end: 0;
1056
+ padding: 0.15rem 0.45rem;
1057
+ border-radius: 0.35rem;
1058
+ background: var(--sl-color-accent-low);
1059
+ color: var(--sl-color-text-accent);
1060
+ font-size: var(--sl-text-xs);
1061
+ white-space: nowrap;
1062
+ pointer-events: none;
1063
+ }
1064
+
1065
+ /* An inline `$…$` equation (`inlineEquationNode` in `remark-notation.mjs`)
1066
+ carries the same click-to-copy gesture and cursor treatment as a display
1067
+ equation above, but deliberately no visible chrome of its own — a button
1068
+ here would break the reading line, which is the whole reason this exists
1069
+ instead of just reusing `.keyed-equation__copy`. `position: relative` only
1070
+ so its own "Copied!" flash (the click handler has no button to flash
1071
+ instead) has something to anchor to. */
1072
+ .sl-markdown-content .inline-equation {
1073
+ position: relative;
1074
+ }
1075
+
1076
+ .sl-markdown-content .inline-equation[data-copied]::after {
1077
+ content: 'Copied!';
1078
+ position: absolute;
1079
+ inset-block-end: 100%;
1080
+ inset-inline-start: 50%;
1081
+ translate: -50% 0;
1082
+ margin-block-end: 0.25rem;
1083
+ padding: 0.15rem 0.45rem;
1084
+ border-radius: 0.35rem;
1085
+ background: var(--sl-color-accent-low);
1086
+ color: var(--sl-color-text-accent);
1087
+ font-size: var(--sl-text-xs);
1088
+ white-space: nowrap;
1089
+ pointer-events: none;
1090
+ }
1091
+
716
1092
  @media (prefers-reduced-motion: reduce) {
717
- .sl-markdown-content a.keyed-equation__number {
1093
+ .sl-markdown-content a.keyed-equation__number,
1094
+ .sl-markdown-content .keyed-equation__copy {
718
1095
  transition: none;
719
1096
  }
720
1097
  }
721
1098
 
722
1099
  @media (forced-colors: active) {
723
- .sl-markdown-content a.keyed-equation__number {
1100
+ .sl-markdown-content a.keyed-equation__number,
1101
+ .sl-markdown-content .keyed-equation__copy {
724
1102
  opacity: 1;
725
1103
  }
726
1104
  }
@@ -813,27 +1191,46 @@
813
1191
  }
814
1192
 
815
1193
  @media print {
816
- .sl-markdown-content a.keyed-equation__number {
1194
+ .sl-markdown-content .keyed-equation__controls {
817
1195
  position: static;
818
1196
  display: block;
819
1197
  transform: none;
820
1198
  text-align: end;
1199
+ }
1200
+ .sl-markdown-content a.keyed-equation__number {
821
1201
  color: inherit;
822
1202
  opacity: 1;
823
1203
  }
1204
+ /* A clipboard button means nothing on paper. */
1205
+ .sl-markdown-content .keyed-equation__copy {
1206
+ display: none;
1207
+ }
824
1208
  .sl-markdown-content .keyed-equation {
825
1209
  padding-inline-end: 0;
826
1210
  }
827
1211
  }
828
1212
 
829
- /* `<Table>`/`<Figure>` keyed floats (F-tables-figures) — the same deep-link
1213
+ /* The generic prose sibling-spacing rule must not insert a blank toolbar-like
1214
+ row between a diagram title and its body. This comes after that generic rule
1215
+ in the cascade: the expand control is overlaid on the title line, so the
1216
+ canvas begins immediately below it. */
1217
+ .sl-markdown-content .diagram__summary {
1218
+ margin-block-end: 0;
1219
+ }
1220
+
1221
+ .sl-markdown-content .diagram__summary + .diagram__body {
1222
+ margin-block-start: 0;
1223
+ }
1224
+
1225
+ /* `<Table>`/`<Figure>`/`<Diagram>` keyed floats — the same deep-link
830
1226
  and hover-highlight treatment as equations above, via `KeyedFloatEnhancer`
831
1227
  and `FloatRefLayer.astro`. Unlike an equation's number (quiet until
832
1228
  hovered, F4), a table/figure number lives in its always-visible caption,
833
1229
  so there is no opacity dance to mirror here — only the `:target` flash
834
1230
  (reusing the `eq-flash` keyframe above) and the ref-highlight. */
835
1231
  .sl-markdown-content a.table-ref,
836
- .sl-markdown-content a.figure-ref {
1232
+ .sl-markdown-content a.figure-ref,
1233
+ .sl-markdown-content a.diagram-ref {
837
1234
  font-variant-numeric: tabular-nums;
838
1235
  font-weight: 600;
839
1236
  text-decoration: underline;
@@ -847,19 +1244,23 @@
847
1244
  .sl-markdown-content a.table-ref:hover,
848
1245
  .sl-markdown-content a.table-ref:focus-visible,
849
1246
  .sl-markdown-content a.figure-ref:hover,
850
- .sl-markdown-content a.figure-ref:focus-visible {
1247
+ .sl-markdown-content a.figure-ref:focus-visible,
1248
+ .sl-markdown-content a.diagram-ref:hover,
1249
+ .sl-markdown-content a.diagram-ref:focus-visible {
851
1250
  background: var(--sl-color-accent-low);
852
1251
  }
853
1252
 
854
1253
  .sl-markdown-content .keyed-table:target,
855
- .sl-markdown-content .keyed-figure:target {
1254
+ .sl-markdown-content .keyed-figure:target,
1255
+ .sl-markdown-content .diagram:target {
856
1256
  animation: eq-flash 5s ease-out both;
857
1257
  border-radius: 0.3rem;
858
1258
  }
859
1259
 
860
1260
  @media (prefers-reduced-motion: reduce) {
861
1261
  .sl-markdown-content .keyed-table:target,
862
- .sl-markdown-content .keyed-figure:target {
1262
+ .sl-markdown-content .keyed-figure:target,
1263
+ .sl-markdown-content .diagram:target {
863
1264
  animation: none;
864
1265
  background: var(--sl-color-accent-low);
865
1266
  outline: 2px solid var(--sl-color-accent);
@@ -872,7 +1273,8 @@
872
1273
  highlight it in place instead of opening a preview card
873
1274
  (`FloatRefLayer.astro` decides which) — mirrors `.eq-ref-highlight`. */
874
1275
  .sl-markdown-content .keyed-table.tab-ref-highlight,
875
- .sl-markdown-content .keyed-figure.fig-ref-highlight {
1276
+ .sl-markdown-content .keyed-figure.fig-ref-highlight,
1277
+ .sl-markdown-content .diagram.dia-ref-highlight {
876
1278
  background: var(--sl-color-accent-low);
877
1279
  outline: 2px solid var(--sl-color-accent);
878
1280
  outline-offset: 0.2rem;
@@ -885,8 +1287,10 @@
885
1287
  @media (forced-colors: active) {
886
1288
  .sl-markdown-content .keyed-table:target,
887
1289
  .sl-markdown-content .keyed-figure:target,
1290
+ .sl-markdown-content .diagram:target,
888
1291
  .sl-markdown-content .keyed-table.tab-ref-highlight,
889
- .sl-markdown-content .keyed-figure.fig-ref-highlight {
1292
+ .sl-markdown-content .keyed-figure.fig-ref-highlight,
1293
+ .sl-markdown-content .diagram.dia-ref-highlight {
890
1294
  animation: none;
891
1295
  outline: 2px solid Highlight;
892
1296
  outline-offset: 0.2rem;
@@ -894,7 +1298,9 @@
894
1298
  .sl-markdown-content a.table-ref:hover,
895
1299
  .sl-markdown-content a.table-ref:focus-visible,
896
1300
  .sl-markdown-content a.figure-ref:hover,
897
- .sl-markdown-content a.figure-ref:focus-visible {
1301
+ .sl-markdown-content a.figure-ref:focus-visible,
1302
+ .sl-markdown-content a.diagram-ref:hover,
1303
+ .sl-markdown-content a.diagram-ref:focus-visible {
898
1304
  outline: 2px solid Highlight;
899
1305
  }
900
1306
  }
@@ -951,3 +1357,672 @@
951
1357
  outline-offset: 0.2rem;
952
1358
  }
953
1359
  }
1360
+
1361
+ /* Collapsible sections (docs/plans/collapsible-disclosure.md Part 3),
1362
+ wrapped at runtime by `CollapsibleSections.astro` around headings an
1363
+ author opted in with `{collapsible}`. Starlight's own `markdown.css` gives
1364
+ every `<details>` a rotating-chevron `<summary>` marker for free — kept
1365
+ (repositioned below) — but also a left accent border +
1366
+ `padding-inline-start: 1rem` meant for a short, occasional aside-style
1367
+ disclosure. On a wrapped section that padding measurably narrows the
1368
+ content column, which was enough on at least one lesson to push a
1369
+ previously-fitting display equation into horizontal overflow and fail
1370
+ axe's `scrollable-region-focusable` check — so it's stripped back to
1371
+ nothing here, the same way `Figure.astro` already overrides that default
1372
+ border rule for its own box. */
1373
+ .sl-markdown-content .sl-section-collapsible {
1374
+ position: relative; /* anchors the peeking body's `position: absolute` */
1375
+ border-inline-start: 0;
1376
+ padding-inline-start: 0;
1377
+ }
1378
+
1379
+ /* `anchor-links.css`'s `.sl-heading-wrapper > :first-child { display: inline;
1380
+ padding-inline-end: var(--sl-anchor-icon-space); }` rule was written for
1381
+ the heading element that used to sit there, reserving line-end room for
1382
+ the anchor icon. Now that `<details class="sl-section-collapsible">` is
1383
+ `.sl-heading-wrapper`'s first child instead, that same padding lands on
1384
+ `<details>` itself — shrinking every section's *entire body* by that
1385
+ amount, not just one line of heading text. That measured out to ~38px on
1386
+ one lesson, just enough to push a table that fit exactly into horizontal
1387
+ overflow and fail axe's `scrollable-region-focusable` check. Both
1388
+ properties are overridden back off here (the anchor no longer needs
1389
+ inline end-of-line room anyway — it's absolutely positioned now, see
1390
+ below). */
1391
+ .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar > .sl-section-collapsible {
1392
+ display: block;
1393
+ padding-inline-end: 0;
1394
+ /* `.sl-heading-wrapper { font-size: var(--sl-text-hN); line-height:
1395
+ var(--sl-line-height-headings) }` (`anchor-links.css`) was sized for a
1396
+ wrapper holding only the heading line + its anchor icon. Now that the
1397
+ section's whole body — a table, an equation, anything — lives inside
1398
+ this `<details>` as a *descendant* of that same wrapper, it would
1399
+ inherit both the heading's larger font-size AND its tighter
1400
+ (headings-only) line-height, inflating/cramping every cell or line of
1401
+ body text. The font-size half of this was caught first (it measurably
1402
+ pushed a table into horizontal overflow); the line-height half is more
1403
+ subtle — it doesn't change layout width, only row/line spacing — which
1404
+ is how it went unnoticed until `TableOverflowExpander.astro`'s expand
1405
+ view (which moves a table *out* of this wrapper) started rendering
1406
+ visibly taller rows than the same table collapsed in place. Both are
1407
+ reset back to normal body text metrics here — the heading itself keeps
1408
+ its own size/spacing regardless, since `markdown.css`/`anchor-links.css`
1409
+ set those directly on `h1`-`h6`/`.sl-heading-wrapper`, not inherited
1410
+ from this rule. */
1411
+ font-size: var(--sl-text-body);
1412
+ line-height: var(--sl-line-height);
1413
+ }
1414
+
1415
+ .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar {
1416
+ position: relative;
1417
+ }
1418
+
1419
+ /* The anchor icon's reveal-on-hover must be scoped to *this* section's own
1420
+ title line, not "anywhere inside this now much taller wrapper" — the
1421
+ wrapper's rendered box spans the whole section (title + body), so a plain
1422
+ `.sl-heading-wrapper:hover` (Starlight's own default rule, still in
1423
+ effect from `anchor-links.css`) fires whenever the pointer is anywhere in
1424
+ that box, including over a *nested* subsection's own title further down —
1425
+ revealing this section's anchor while hovering a child's heading, and
1426
+ vice versa. `:has(> .sl-section-collapsible > summary:hover)` only matches
1427
+ when *this* wrapper's own direct-child `<details>`'s own direct-child
1428
+ `<summary>` is hovered — a nested child section's `<summary>` is several
1429
+ levels deeper (inside its own separate `.sl-section-collapsible`), so it
1430
+ never satisfies this selector. Both rules are unlayered, so they win over
1431
+ `anchor-links.css`'s `@layer starlight.content` version regardless of
1432
+ specificity — this replaces that default behavior for sections rather
1433
+ than adding to it. */
1434
+ .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar > .sl-anchor-link {
1435
+ position: absolute;
1436
+ top: 0.2em;
1437
+ inset-inline-end: 0;
1438
+ margin-inline-start: 0;
1439
+ opacity: 0;
1440
+ }
1441
+
1442
+ .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar:has(> .sl-section-collapsible > summary:hover)
1443
+ > .sl-anchor-link,
1444
+ .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar:has(> .sl-section-collapsible > summary:focus-visible)
1445
+ > .sl-anchor-link,
1446
+ .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar > .sl-anchor-link:hover,
1447
+ .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar > .sl-anchor-link:focus-visible {
1448
+ opacity: 1;
1449
+ }
1450
+
1451
+ /* The chevron sits to the *left* of the title without shifting the title's
1452
+ own position: `markdown.css`'s default marker is inline, right before the
1453
+ text, pushed left slightly by a negative margin — meaning the title
1454
+ itself sits further right than an ordinary (non-collapsible) heading.
1455
+ Taking the marker fully out of flow with `position: absolute` and a
1456
+ negative `left` puts it in the gutter instead, so `.sl-section-summary`'s
1457
+ own text always starts at the same position a plain heading's would. */
1458
+ .sl-markdown-content .sl-section-summary {
1459
+ position: relative;
1460
+ /* Room for the absolutely-positioned anchor icon at the end of the line,
1461
+ same reasoning as `.keyed-figure__summary` in `Figure.astro`. */
1462
+ padding-inline-end: 1.5em;
1463
+ }
1464
+
1465
+ .sl-markdown-content .sl-section-summary::before {
1466
+ position: absolute;
1467
+ left: -1.6em;
1468
+ top: 50%;
1469
+ margin: 0; /* cancel `markdown.css`'s inline-flow margin trick */
1470
+ transform: translateY(-50%);
1471
+ opacity: 0;
1472
+ /* Fade in immediately on hover/focus, but linger for a beat after the
1473
+ pointer leaves before fading back out — a quick flick past the title
1474
+ shouldn't make the chevron flash in and out. This only ever applies to
1475
+ an `open`/`peeking` section, though: a *closed* one overrides `opacity`
1476
+ back to a flat `1` below, with no fade at all — its `>` is the only
1477
+ visible signal that there's hidden content here, so it has to stay put
1478
+ whether or not the reader is currently hovering. */
1479
+ transition:
1480
+ opacity 150ms ease 700ms,
1481
+ transform 150ms ease;
1482
+ }
1483
+
1484
+ .sl-markdown-content details.sl-section-collapsible[open] > .sl-section-summary::before {
1485
+ transform: translateY(-50%) rotateZ(90deg);
1486
+ }
1487
+
1488
+ .sl-markdown-content details.sl-section-collapsible[data-state='closed']
1489
+ > .sl-section-summary::before {
1490
+ opacity: 1;
1491
+ transition: none;
1492
+ }
1493
+
1494
+ .sl-markdown-content .sl-section-summary:hover::before,
1495
+ .sl-markdown-content .sl-section-summary:focus-visible::before {
1496
+ opacity: 1;
1497
+ transition: opacity 100ms ease;
1498
+ }
1499
+
1500
+ /* The section body: normally native `<details>` hiding/showing is all that's
1501
+ needed (nothing renders while closed), but `peeking` (a collapsed section
1502
+ hovered/focused — see `CollapsibleSections.astro`) has to force the
1503
+ `<details>` open to render its real content at all, then relies on this
1504
+ CSS to cap it at 10em and lift it out of flow so it overlays whatever
1505
+ comes next instead of shifting it — and `open` (a real, committed expand)
1506
+ puts it back in normal flow so following content *is* pushed down. Both
1507
+ the peek-to-real-expand growth and the plain open/close animate through
1508
+ `max-height`, driven from `CollapsibleSections.astro`; this transition is
1509
+ what makes those changes smooth instead of instant. */
1510
+ .sl-markdown-content .sl-section-body {
1511
+ transition: max-height 220ms ease;
1512
+ }
1513
+
1514
+ .sl-markdown-content .sl-section-collapsible[data-state='peeking'] > .sl-section-body {
1515
+ --peek-inline-pad: 0.85rem;
1516
+ --peek-border-width: 1px;
1517
+ position: absolute;
1518
+ top: 100%;
1519
+ /* The card's border + padding must NOT narrow the content itself — a
1520
+ table or equation that reflows narrower while peeking than it will once
1521
+ genuinely expanded would visibly re-layout the instant a click promotes
1522
+ the peek to a real expand. `box-sizing: border-box` plus bleeding `left`/
1523
+ `right` outward by exactly (padding + border) keeps the *content box*
1524
+ the same width as the non-collapsed, no-border/no-padding `open` state
1525
+ below — only the decorative card extends past it. */
1526
+ left: calc(-1 * (var(--peek-inline-pad) + var(--peek-border-width)));
1527
+ right: calc(-1 * (var(--peek-inline-pad) + var(--peek-border-width)));
1528
+ box-sizing: border-box;
1529
+ z-index: 4;
1530
+ margin-block-start: 0.35rem;
1531
+ padding: 0.6rem var(--peek-inline-pad);
1532
+ border: var(--peek-border-width) solid var(--sl-color-gray-5);
1533
+ border-radius: 0.4rem;
1534
+ background: var(--sl-color-bg);
1535
+ box-shadow: 0 0.6rem 1.75rem rgba(0, 0, 0, 0.18);
1536
+ max-height: 10em;
1537
+ overflow: auto;
1538
+ }
1539
+
1540
+ .sl-markdown-content .sl-section-collapsible[data-state='open'] > .sl-section-body {
1541
+ position: static;
1542
+ max-height: none;
1543
+ overflow: visible;
1544
+ }
1545
+
1546
+ @media (prefers-reduced-motion: reduce) {
1547
+ .sl-markdown-content .sl-section-summary::before,
1548
+ .sl-markdown-content .sl-section-body {
1549
+ transition: none;
1550
+ }
1551
+ }
1552
+
1553
+ @media print {
1554
+ .sl-markdown-content .sl-section-summary::before {
1555
+ display: none !important;
1556
+ }
1557
+
1558
+ .sl-markdown-content .sl-heading-wrapper.sl-section-titlebar > .sl-anchor-link {
1559
+ display: none;
1560
+ }
1561
+
1562
+ /* Forcing every section open for print is NOT done here: the HTML spec
1563
+ has the browser skip rendering a closed <details>'s non-summary
1564
+ children outright — a rule no author CSS can override (`display`,
1565
+ `content-visibility`, `!important` included). Only the `open`
1566
+ *attribute* controls it, so `CollapsibleSections.astro` forces every
1567
+ `.sl-section-collapsible` open (and `data-state="open"`) via a
1568
+ `beforeprint`/`matchMedia('print')` listener instead, restoring on
1569
+ `afterprint`. */
1570
+ }
1571
+
1572
+ /* Table column narrowing + expand-on-overflow
1573
+ (docs/plans/collapsible-disclosure.md Part 4), driven from
1574
+ `TableOverflowExpander.astro` — see that file's module comment for the
1575
+ full design rationale (why the scroll wrapper is necessary, why the
1576
+ container node itself moves to `<body>` on expand, why its expanded width
1577
+ is only as wide as the table needs, etc.). */
1578
+
1579
+ /* Narrower than Starlight's own default (`markdown.css`'s `padding: 0.5rem
1580
+ 1rem` on every `th`/`td`, plus the ambient body font size) — with real
1581
+ data in them, tables read as too spaced-out at that generosity, and a
1582
+ long one takes noticeably longer to scan a row at a time. Unlayered, so
1583
+ (like the rules below) it wins over `markdown.css`'s `@layer
1584
+ starlight.content` regardless of selector specificity; scoped the same
1585
+ way, so a `not-content`-wrapped table (e.g. inside a lab island) is left
1586
+ at its own sizing. */
1587
+ .sl-markdown-content table:not(:where(.not-content *)) {
1588
+ font-size: 90%;
1589
+ }
1590
+
1591
+ .sl-markdown-content :is(th, td):not(:where(.not-content *)) {
1592
+ padding: 0.3rem 0.75rem;
1593
+ }
1594
+
1595
+ /* Every markdown table's data rows get subtle zebra striping and a hover
1596
+ highlight — plain CSS, so it applies with or without
1597
+ `TableOverflowExpander.astro`'s script, and to every table on the site,
1598
+ not just ones wide enough to overflow. `color-mix` against `--sl-color-bg`
1599
+ (not a fixed gray token) keeps the stripe theme-correct automatically —
1600
+ the same technique `.lab-shell`'s background above uses, and for the same
1601
+ reason: a token like the old `--sl-color-gray-7` is only defined for one
1602
+ theme. The hover rule is unlayered and comes after the stripe rule, so it
1603
+ wins over an even/odd row's own stripe regardless of specificity. */
1604
+ .sl-markdown-content table:not(:where(.not-content *)) tbody tr:nth-child(even) {
1605
+ background: color-mix(in srgb, var(--sl-color-bg) 85%, var(--sl-color-gray-5) 15%);
1606
+ }
1607
+
1608
+ .sl-markdown-content table:not(:where(.not-content *)) tbody tr:hover {
1609
+ background: var(--sl-color-accent-low);
1610
+ }
1611
+
1612
+ /* `.keyed-table__body`/`.table-overflow-body` are a flex row of
1613
+ `[.table-overflow-scroll, .table-overflow-expand-btn]` — the scroll clip
1614
+ flexes to fill the available width (`min-width: 0` lets it actually
1615
+ shrink below the table's intrinsic width instead of forcing the row to
1616
+ overflow), and the button, only present once the table overflows, is a
1617
+ plain flex sibling — `align-items: stretch` (the flex default) is what
1618
+ makes it exactly as tall as the table for free, no JS height math
1619
+ needed. */
1620
+ .keyed-table__body,
1621
+ .table-overflow-body {
1622
+ display: flex;
1623
+ }
1624
+
1625
+ .table-overflow-scroll {
1626
+ flex: 1 1 auto;
1627
+ min-width: 0;
1628
+ overflow-x: auto;
1629
+ }
1630
+
1631
+ /* A scrollable region must be reachable by keyboard (WCAG 2.1.1) — the
1632
+ script gives it `tabindex="0"` only while it is actually scrollable, so it
1633
+ needs a visible focus style for exactly that state. */
1634
+ .table-overflow-scroll:focus-visible {
1635
+ outline: 2px solid var(--sl-color-accent);
1636
+ outline-offset: -2px;
1637
+ }
1638
+
1639
+ /* Starlight's `markdown.css` gives *every* `<table>` under
1640
+ `.sl-markdown-content` its own `display: block; overflow: auto` — a
1641
+ second, independent scroll container. Left in place inside
1642
+ `.table-overflow-scroll`, that inner rule would win first and clip/scroll
1643
+ the table before this wrapper ever saw overflow, leaving
1644
+ `scrollWidth > clientWidth` on it permanently false. This selector is
1645
+ unlayered, so — like `.sl-section-collapsible` above — it wins over that
1646
+ `@layer starlight.content` rule regardless of specificity. Scoped to
1647
+ *inside* the wrapper only: a table with no JS (so no
1648
+ `.table-overflow-scroll` around it yet) keeps Starlight's default
1649
+ per-table scroll as an unmodified fallback. Column *sizing* needed no
1650
+ fix: `table-layout` is `auto` either way, so columns were already sized
1651
+ to content, not to a fixed/equal split. */
1652
+ .table-overflow-scroll table {
1653
+ display: table;
1654
+ overflow: visible;
1655
+ /* Centered within the scroll clip when narrower than it (this wrapper
1656
+ itself always fills the available row width via `flex: 1 1 auto`
1657
+ above), same centering `Table.astro`'s own fallback rule gives a
1658
+ keyed table before this wrapper exists — this is what covers a bare
1659
+ markdown table's `.table-overflow-body`, which carries no such rule of
1660
+ its own. Auto margins resolve to 0 once the table overflows, so this is
1661
+ safe unconditionally. */
1662
+ margin-block: 0;
1663
+ margin-inline: auto;
1664
+ }
1665
+
1666
+ .table-overflow-expand-btn {
1667
+ /* Starlight's own vertical-rhythm rule (`.sl-markdown-content :not(a,
1668
+ strong, …) + :not(a, strong, …) { margin-top: var(--sl-content-gap-y) }`
1669
+ at the top of this file) adds a ~1rem top margin to any block sibling
1670
+ following another — both this button and its expanded-state twin (see
1671
+ `TableOverflowExpander.astro`) are exactly that. Left in place, that
1672
+ margin eats into the flex row's stretch, leaving the button's rendered
1673
+ box shorter than the table by that same margin instead of genuinely
1674
+ spanning it top to bottom. */
1675
+ margin: 0;
1676
+ flex: none;
1677
+ width: 1.5rem;
1678
+ display: flex;
1679
+ align-items: center;
1680
+ justify-content: center;
1681
+ padding: 0;
1682
+ border: 0;
1683
+ color: var(--sl-color-gray-2);
1684
+ background: var(--sl-color-bg);
1685
+ font-size: 0.85rem;
1686
+ line-height: 1;
1687
+ cursor: pointer;
1688
+ }
1689
+
1690
+ /* A continuation of the table's own row-separator lines, not a
1691
+ differently-styled floating overlay — same border color, same background
1692
+ as the page/table. Collapsed, this row is `[scroll, button]` — the button
1693
+ is always `:last-child`, divider on its leading (start) side. Expanded,
1694
+ a second button is prepended (`[button, scroll, button]`, the "0th and
1695
+ n+1th column" — see `TableOverflowExpander.astro`); each keeps the
1696
+ divider on whichever side actually borders the table. */
1697
+ .table-overflow-expand-btn:last-child {
1698
+ border-inline-start: 1px solid var(--sl-color-gray-5);
1699
+ }
1700
+
1701
+ .table-overflow-expand-btn:first-child {
1702
+ border-inline-end: 1px solid var(--sl-color-gray-5);
1703
+ }
1704
+
1705
+ /* Same same-specificity/cascade-order gotcha `.hover-panel[hidden]` guards
1706
+ against in `HoverPanel.astro`: without this, `display: flex` above would
1707
+ win over the UA stylesheet's `[hidden] { display: none }` and defeat the
1708
+ `hidden` attribute the script relies on to hide a non-overflowing table's
1709
+ button. */
1710
+ .table-overflow-expand-btn[hidden] {
1711
+ display: none;
1712
+ }
1713
+
1714
+ .table-overflow-expand-btn:hover {
1715
+ color: var(--sl-color-text);
1716
+ background: var(--sl-color-accent-low);
1717
+ }
1718
+
1719
+ .table-overflow-expand-btn:focus-visible {
1720
+ outline: 2px solid var(--sl-color-accent);
1721
+ outline-offset: -2px;
1722
+ }
1723
+
1724
+ .table-overflow-expanded {
1725
+ position: fixed;
1726
+ /* Above the side rails, but below Starlight's sticky site header. */
1727
+ z-index: calc(var(--sl-z-index-navbar) - 1);
1728
+ /* `target` (see `TableOverflowExpander.astro`) is sometimes the whole
1729
+ `<figure class="keyed-table">`, whose own Astro-scoped style sets
1730
+ `margin-block: 1.5rem` — a scoped selector compiles to an attribute
1731
+ selector (`.keyed-table[data-astro-cid-…]`), which outranks this plain
1732
+ class on specificity alone despite this file's usual unlayered-always-
1733
+ wins convention (that convention only beats *layered* Starlight CSS,
1734
+ not other equally-unlayered author CSS). A `position: fixed` element's
1735
+ `top`/`left` position its margin box, so left in place that margin
1736
+ would silently offset the whole computed position — `!important` is the
1737
+ honest fix, since `target` isn't always `.keyed-table` (a bare table's
1738
+ `.table-overflow-body` carries no such rule to out-rank in the first
1739
+ place, so this is never fighting anything except that one case). */
1740
+ margin: 0 !important;
1741
+ /* No border/border-radius "card" — an expanded table should still read as
1742
+ the same table, just wider. A background is still necessary (it now
1743
+ sits, `position: fixed`, over whatever content is behind it) and a
1744
+ shadow gives just enough depth to read as raised above the page. Tall
1745
+ content (table + caption together, since the whole figure moves — see
1746
+ `TableOverflowExpander.astro`) scrolls inside `max-height` rather than
1747
+ breaking out of the box. */
1748
+ overflow-y: auto;
1749
+ background: var(--sl-color-bg);
1750
+ box-shadow: var(--sl-shadow-lg);
1751
+ }
1752
+
1753
+ @media print {
1754
+ .table-overflow-expand-btn {
1755
+ display: none !important;
1756
+ }
1757
+ }
1758
+
1759
+ /* Browser-native lesson printing. The print controller opens real native
1760
+ disclosures before the browser lays out pages; these rules own only the
1761
+ paper presentation and therefore also remain a useful no-JS fallback. */
1762
+ @media print {
1763
+ :root[data-theme],
1764
+ :root {
1765
+ color-scheme: light;
1766
+ --sl-color-bg: #fff;
1767
+ --sl-color-bg-nav: #fff;
1768
+ --sl-color-bg-sidebar: #fff;
1769
+ --sl-color-text: #111;
1770
+ --sl-color-white: #111;
1771
+ --sl-color-gray-1: #111;
1772
+ --sl-color-gray-2: #333;
1773
+ --sl-color-gray-3: #555;
1774
+ --sl-color-gray-4: #777;
1775
+ --sl-color-gray-5: #aaa;
1776
+ --sl-color-gray-6: #ddd;
1777
+ --sl-color-gray-7: #fff;
1778
+ --sl-color-accent: #08775b;
1779
+ --sl-color-accent-low: #fff;
1780
+ --sl-color-accent-high: #111;
1781
+ --sl-color-text-accent: #08775b;
1782
+ }
1783
+
1784
+ html,
1785
+ body {
1786
+ overflow: visible !important;
1787
+ color: #111 !important;
1788
+ background: #fff !important;
1789
+ }
1790
+
1791
+ header.header,
1792
+ .sidebar,
1793
+ .right-sidebar-container,
1794
+ .pagination-links,
1795
+ .layout-edge-toggle,
1796
+ [data-explico-export-chrome],
1797
+ [data-popup-root],
1798
+ [data-citation-panel],
1799
+ [data-float-ref-panel],
1800
+ .hover-panel,
1801
+ .notation-popup,
1802
+ .table-overflow-placeholder,
1803
+ .diagram-expand-placeholder,
1804
+ .table-overflow-expand-btn,
1805
+ .keyed-table__pagination-links,
1806
+ .keyed-table__more-row,
1807
+ .diagram-toolbar,
1808
+ .sl-anchor-link,
1809
+ .keyed-figure__title-anchor,
1810
+ .diagram__title-anchor,
1811
+ .memo-block__title-anchor,
1812
+ .short-note__collapse,
1813
+ .assessment-actions,
1814
+ .assessment-chevron,
1815
+ .assessment-feedback.is-idle,
1816
+ .notation-pin,
1817
+ .notation-enhancement-help,
1818
+ .lab-controls {
1819
+ display: none !important;
1820
+ }
1821
+
1822
+ .main-frame,
1823
+ .main-pane,
1824
+ .content-panel,
1825
+ main[data-pagefind-body],
1826
+ main[data-pagefind-body] > .sl-container,
1827
+ .sl-markdown-content {
1828
+ position: static !important;
1829
+ display: block !important;
1830
+ width: auto !important;
1831
+ min-width: 0 !important;
1832
+ max-width: none !important;
1833
+ height: auto !important;
1834
+ min-height: 0 !important;
1835
+ max-height: none !important;
1836
+ margin: 0 !important;
1837
+ padding-inline: 0 !important;
1838
+ overflow: visible !important;
1839
+ transform: none !important;
1840
+ }
1841
+
1842
+ main[data-pagefind-body] {
1843
+ padding-block: 0 !important;
1844
+ }
1845
+
1846
+ .sl-markdown-content *,
1847
+ .sl-markdown-content *::before,
1848
+ .sl-markdown-content *::after {
1849
+ animation: none !important;
1850
+ transition: none !important;
1851
+ }
1852
+
1853
+ .sl-markdown-content :is(h1, h2, h3, h4, h5, h6),
1854
+ .keyed-figure__summary,
1855
+ .diagram__summary,
1856
+ .memo-block__summary,
1857
+ .assessment-item-heading {
1858
+ break-after: avoid-page;
1859
+ page-break-after: avoid;
1860
+ }
1861
+
1862
+ .sl-markdown-content p,
1863
+ .sl-markdown-content li {
1864
+ orphans: 3;
1865
+ widows: 3;
1866
+ }
1867
+
1868
+ .keyed-equation,
1869
+ .keyed-table,
1870
+ .keyed-figure,
1871
+ .diagram,
1872
+ .memo-block-frame,
1873
+ .compact-example,
1874
+ .assessment-item,
1875
+ .lab-result,
1876
+ .lab-chart-figure,
1877
+ .sl-markdown-content pre,
1878
+ .sl-markdown-content blockquote {
1879
+ break-inside: avoid-page;
1880
+ page-break-inside: avoid;
1881
+ }
1882
+
1883
+ .keyed-table tr,
1884
+ .lab-data-table tr {
1885
+ break-inside: avoid-page;
1886
+ page-break-inside: avoid;
1887
+ }
1888
+
1889
+ .keyed-table thead,
1890
+ .lab-data-table thead {
1891
+ display: table-header-group;
1892
+ }
1893
+
1894
+ .keyed-table tfoot,
1895
+ .lab-data-table tfoot {
1896
+ display: table-footer-group;
1897
+ }
1898
+
1899
+ .keyed-table__body,
1900
+ .table-overflow-body,
1901
+ .table-overflow-scroll,
1902
+ .lab-table-scroll {
1903
+ position: static !important;
1904
+ display: block !important;
1905
+ width: auto !important;
1906
+ min-width: 0 !important;
1907
+ max-width: none !important;
1908
+ max-height: none !important;
1909
+ overflow: visible !important;
1910
+ box-shadow: none !important;
1911
+ }
1912
+
1913
+ .sl-markdown-content table,
1914
+ .table-overflow-scroll table,
1915
+ .lab-table-scroll table,
1916
+ .lab-data-table {
1917
+ display: table !important;
1918
+ width: 100% !important;
1919
+ min-width: 0 !important;
1920
+ max-width: 100% !important;
1921
+ margin-inline: 0 !important;
1922
+ overflow: visible !important;
1923
+ font-size: 8.5pt;
1924
+ table-layout: auto;
1925
+ }
1926
+
1927
+ .sl-markdown-content :is(th, td) {
1928
+ padding: 0.18rem 0.35rem;
1929
+ overflow-wrap: anywhere;
1930
+ }
1931
+
1932
+ .keyed-table__body tr[hidden] {
1933
+ display: table-row !important;
1934
+ }
1935
+
1936
+ .assessment-panel[hidden],
1937
+ .short-note[hidden],
1938
+ .compact-example[hidden] {
1939
+ display: block !important;
1940
+ }
1941
+
1942
+ .diagram,
1943
+ .diagram__titlebar,
1944
+ .diagram__details,
1945
+ .diagram__body,
1946
+ .diagram-stage,
1947
+ .diagram-scroll {
1948
+ position: static !important;
1949
+ width: auto !important;
1950
+ min-width: 0 !important;
1951
+ max-width: 100% !important;
1952
+ height: auto !important;
1953
+ min-height: 0 !important;
1954
+ max-height: none !important;
1955
+ overflow: visible !important;
1956
+ contain: none !important;
1957
+ box-shadow: none !important;
1958
+ transform: none !important;
1959
+ }
1960
+
1961
+ .diagram-canvas {
1962
+ width: 100% !important;
1963
+ max-width: 100% !important;
1964
+ height: auto !important;
1965
+ aspect-ratio: var(--diagram-print-ratio);
1966
+ }
1967
+
1968
+ .diagram-canvas > svg {
1969
+ display: block;
1970
+ width: 100% !important;
1971
+ max-width: 100% !important;
1972
+ height: 100% !important;
1973
+ }
1974
+
1975
+ .lab-chart,
1976
+ .lab-math,
1977
+ .katex-display {
1978
+ min-height: 0 !important;
1979
+ max-width: 100% !important;
1980
+ overflow: visible !important;
1981
+ }
1982
+
1983
+ .lab-chart svg,
1984
+ .keyed-figure img,
1985
+ .sl-markdown-content img,
1986
+ .sl-markdown-content svg {
1987
+ max-width: 100% !important;
1988
+ height: auto;
1989
+ }
1990
+
1991
+ .assessment-panel {
1992
+ padding-block-start: 0.25rem;
1993
+ }
1994
+
1995
+ .assessment-disclosure {
1996
+ padding-block: 0.4rem;
1997
+ color: #111;
1998
+ cursor: default;
1999
+ }
2000
+
2001
+ .assessment-status {
2002
+ color: #333 !important;
2003
+ }
2004
+
2005
+ :is(
2006
+ .lab-shell,
2007
+ .lab-result,
2008
+ .assessment-shell,
2009
+ .assessment-item,
2010
+ .memo-block-frame,
2011
+ .keyed-figure__details,
2012
+ .compact-examples,
2013
+ .diagram__details
2014
+ ) {
2015
+ color: #111 !important;
2016
+ border-color: #777 !important;
2017
+ background: #fff !important;
2018
+ box-shadow: none !important;
2019
+ }
2020
+
2021
+ .sl-markdown-content a[href] {
2022
+ color: #08775b !important;
2023
+ text-decoration: underline;
2024
+ print-color-adjust: exact;
2025
+ -webkit-print-color-adjust: exact;
2026
+ }
2027
+
2028
+ }