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
@@ -1,8 +1,9 @@
1
1
  ---
2
2
  /**
3
- * Progressive enhancement for `[[eq-key]]` / `[[tab-key]]` / `[[fig-key]]`
3
+ * Progressive enhancement for `[[eq-key]]` / `[[tab-key]]` / `[[fig-key]]` /
4
+ * `[[dia-key]]`
4
5
  * references in prose (F4, extended F-tables-figures) — one hover/pin/jump
5
- * engine shared by all three keyed floats. This generalizes the original
6
+ * engine shared by all four keyed floats. This generalizes the original
6
7
  * equation-only `EquationRefLayer`; the positioning, mutual-exclusion, and
7
8
  * pin/jump/Escape mechanics are unchanged, parameterized over `KIND_CONFIG`
8
9
  * below so a hover on any of the three trigger classes is handled the same
@@ -93,7 +94,10 @@ import HoverPanel from '../HoverPanel.astro';
93
94
  clone.removeAttribute('data-eq-key');
94
95
  clone.removeAttribute('data-eq-auto');
95
96
  clone.classList.remove('keyed-equation', 'eq-ref-highlight');
96
- clone.querySelector('.keyed-equation__number')?.remove();
97
+ // The number link and the "Copy LaTeX" button both belong on the real
98
+ // equation, not a hover preview of it — same principle as the table
99
+ // preview below never offering its live pagination/expand controls.
100
+ clone.querySelector('.keyed-equation__controls')?.remove();
97
101
  return clone;
98
102
  }
99
103
 
@@ -102,7 +106,104 @@ import HoverPanel from '../HoverPanel.astro';
102
106
  return body ? document.importNode(body, true) : null;
103
107
  }
104
108
 
105
- const KINDS: Record<'equation' | 'table' | 'figure', KindConfig> = {
109
+ // A table preview card is a static snapshot, not another place to keep
110
+ // paginating/expanding from — regardless of the live table's own current
111
+ // state (minimized, fully expanded, mid-scroll…) the clone always shows
112
+ // at most `KEEP_EACH_END * 2` rows with the "… N more rows …" separator
113
+ // centered, and never offers a way to reveal the rest or widen the table
114
+ // from inside the card (`Table.astro`'s own row-pagination and
115
+ // `TableOverflowExpander.astro`'s horizontal expand button, respectively —
116
+ // both belong on the real table, not a hover preview of it).
117
+ const TABLE_PREVIEW_KEEP_EACH_END = 3;
118
+
119
+ function capTablePreview(clone: HTMLElement): void {
120
+ const tbody = clone.querySelector('table')?.tBodies[0];
121
+ if (!tbody) return;
122
+
123
+ const moreRow = tbody.querySelector<HTMLElement>('.keyed-table__more-row');
124
+ const dataRows = Array.from(tbody.rows).filter((row) => row !== moreRow);
125
+ const totalRows = dataRows.length;
126
+
127
+ if (moreRow && totalRows > TABLE_PREVIEW_KEEP_EACH_END * 2) {
128
+ dataRows.forEach((row, index) => {
129
+ const inMiddle =
130
+ index >= TABLE_PREVIEW_KEEP_EACH_END &&
131
+ index < totalRows - TABLE_PREVIEW_KEEP_EACH_END;
132
+ row.hidden = inMiddle;
133
+ });
134
+ moreRow.hidden = false;
135
+ // `importNode` clones attributes (role/tabindex/class) but never JS
136
+ // listeners, so this row is already functionally inert here — strip
137
+ // the affordance too so it doesn't *look* clickable with nothing
138
+ // behind it.
139
+ moreRow.classList.remove('keyed-table__more-row--interactive');
140
+ moreRow.removeAttribute('role');
141
+ moreRow.removeAttribute('tabindex');
142
+ moreRow.removeAttribute('aria-label');
143
+ } else {
144
+ dataRows.forEach((row) => row.removeAttribute('hidden'));
145
+ moreRow?.remove();
146
+ }
147
+
148
+ // The pagination toggle/"Show less"/"Minimize" controls live outside
149
+ // `.keyed-table__body` (in the caption and after the body respectively),
150
+ // so cloning just the body already excludes them — only the horizontal
151
+ // overflow-expand button (`TableOverflowExpander.astro`) lives inside it.
152
+ clone
153
+ .querySelectorAll('.table-overflow-expand-btn')
154
+ .forEach((button) => button.remove());
155
+ }
156
+
157
+ let diagramCloneSerial = 0;
158
+
159
+ /**
160
+ * A diagram preview contains SVG fragment references (`url(#marker-id)`).
161
+ * Prefix every cloned id and rewrite those references so the preview cannot
162
+ * steal the moved/original diagram's arrow markers — or vice versa.
163
+ */
164
+ function cloneDiagramBody(target: HTMLElement): HTMLElement | null {
165
+ const clone = cloneBody(target, 'diagram-scroll');
166
+ if (!clone) return null;
167
+ clone.removeAttribute('id');
168
+ clone.removeAttribute('tabindex');
169
+ clone.removeAttribute('aria-labelledby');
170
+ clone.setAttribute('aria-label', 'Diagram preview');
171
+
172
+ const prefix = `float-ref-diagram-${++diagramCloneSerial}-`;
173
+ const ids = new Map<string, string>();
174
+ for (const element of clone.querySelectorAll<HTMLElement>('[id]')) {
175
+ const oldId = element.id;
176
+ const nextId = `${prefix}${oldId}`;
177
+ ids.set(oldId, nextId);
178
+ element.id = nextId;
179
+ }
180
+ const referenceAttributes = [
181
+ 'marker-start',
182
+ 'marker-mid',
183
+ 'marker-end',
184
+ 'clip-path',
185
+ 'mask',
186
+ 'filter',
187
+ 'fill',
188
+ 'stroke',
189
+ 'href',
190
+ 'xlink:href',
191
+ ];
192
+ for (const element of clone.querySelectorAll<HTMLElement>('*')) {
193
+ for (const attribute of referenceAttributes) {
194
+ const value = element.getAttribute(attribute);
195
+ if (!value) continue;
196
+ let rewritten = value;
197
+ for (const [oldId, nextId] of ids) {
198
+ rewritten = rewritten.split(`#${oldId}`).join(`#${nextId}`);
199
+ }
200
+ if (rewritten !== value) element.setAttribute(attribute, rewritten);
201
+ }
202
+ }
203
+ return clone;
204
+ }
205
+
206
+ const KINDS: Record<'equation' | 'table' | 'figure' | 'diagram', KindConfig> = {
106
207
  equation: {
107
208
  triggerClass: 'equation-ref',
108
209
  hrefPrefix: '#eq-',
@@ -128,6 +229,7 @@ import HoverPanel from '../HoverPanel.astro';
128
229
  revealOnPin: false,
129
230
  fillContent(target, contentEl) {
130
231
  const clone = cloneBody(target, 'keyed-table__body');
232
+ if (clone) capTablePreview(clone);
131
233
  contentEl.replaceChildren(...(clone ? [clone] : []));
132
234
  },
133
235
  captionFor(target) {
@@ -155,6 +257,22 @@ import HoverPanel from '../HoverPanel.astro';
155
257
  );
156
258
  },
157
259
  },
260
+ diagram: {
261
+ triggerClass: 'diagram-ref',
262
+ hrefPrefix: '#dia-',
263
+ numberClass: 'diagram__number',
264
+ numberDataKey: 'diaNumber',
265
+ highlightClass: 'dia-ref-highlight',
266
+ jumpLabel: 'diagram',
267
+ revealOnPin: false,
268
+ fillContent(target, contentEl) {
269
+ const clone = cloneDiagramBody(target);
270
+ contentEl.replaceChildren(...(clone ? [clone] : []));
271
+ },
272
+ captionFor(target) {
273
+ return target.querySelector('.diagram__caption-text')?.textContent ?? '';
274
+ },
275
+ },
158
276
  };
159
277
 
160
278
  const TRIGGER_SELECTOR = Object.values(KINDS)
@@ -197,6 +315,11 @@ import HoverPanel from '../HoverPanel.astro';
197
315
  */
198
316
  const pageCache = new Map<string, Promise<Document>>();
199
317
 
318
+ /** A standalone lesson deliberately has no other course pages to fetch. */
319
+ const standalone = document.documentElement.hasAttribute(
320
+ 'data-explico-standalone',
321
+ );
322
+
200
323
  function fetchDoc(page: string): Promise<Document> {
201
324
  let pending = pageCache.get(page);
202
325
  if (!pending) {
@@ -232,6 +355,21 @@ import HoverPanel from '../HoverPanel.astro';
232
355
  const panel = document.querySelector<HTMLElement>('[data-float-ref-panel]')!;
233
356
  if (!panel || panel.dataset.floatRefReady === 'true') return;
234
357
 
358
+ // `.main-pane` (Starlight's `TwoColumnContent.astro`) sets
359
+ // `isolation: isolate`, which traps this panel's z-index inside it — the
360
+ // right "On this page" rail sits outside that isolation as a
361
+ // `position: fixed` sibling with no z-index of its own, and CSS still
362
+ // paints any positioned box ahead of a plain in-flow one regardless of
363
+ // z-index or DOM order. No z-index set here could ever win against that
364
+ // from inside `.main-pane`, so the panel is allowed to cover the rail
365
+ // (per its own design) by moving it out to `<body>` instead — the same
366
+ // fix the notation layer's popups already use for this exact trap (see
367
+ // `PopupStackConfig.container` in `popup-stack.ts`). `position: fixed`
368
+ // means the DOM parent doesn't affect its layout.
369
+ if (panel.parentElement !== document.body) {
370
+ document.body.appendChild(panel);
371
+ }
372
+
235
373
  const refs = Array.from(
236
374
  document.querySelectorAll<HTMLAnchorElement>(
237
375
  Object.values(KINDS)
@@ -292,6 +430,32 @@ import HoverPanel from '../HoverPanel.astro';
292
430
  target.querySelector(`.${kind.numberClass}`)?.classList.add('eq-number-revealed');
293
431
  };
294
432
 
433
+ /**
434
+ * How much horizontal room the card may use: from the right edge of the
435
+ * left sidebar nav to the viewport edge. Unlike the old article-column
436
+ * clamp, this deliberately does not stop at the right "On this page"
437
+ * rail — a wide equation (e.g. an OLS estimator) needs the room more
438
+ * than the card needs to respect the ToC's column, and readers can
439
+ * still reach the ToC once the card closes.
440
+ *
441
+ * `.sidebar-pane` is `position: fixed` and reports its full-viewport
442
+ * geometry even while hidden below the sidebar breakpoint (Starlight
443
+ * hides it with `visibility`, not `display`), so that has to be checked
444
+ * explicitly rather than trusted from the rect alone.
445
+ */
446
+ function horizontalRegion(edge: number): { start: number; end: number } {
447
+ const sidebarPane = document.querySelector<HTMLElement>('.sidebar-pane');
448
+ const sidebarVisible =
449
+ !!sidebarPane && getComputedStyle(sidebarPane).visibility !== 'hidden';
450
+ const sidebarRect = sidebarVisible
451
+ ? sidebarPane!.getBoundingClientRect()
452
+ : null;
453
+ return {
454
+ start: Math.max(edge, sidebarRect?.right ?? edge),
455
+ end: window.innerWidth - edge,
456
+ };
457
+ }
458
+
295
459
  function position(trigger: HTMLElement) {
296
460
  const edge = 12;
297
461
  const gap = 10;
@@ -300,6 +464,16 @@ import HoverPanel from '../HoverPanel.astro';
300
464
  const regionRect = safeRegion.getBoundingClientRect();
301
465
  const anchorRect = trigger.getBoundingClientRect();
302
466
 
467
+ // Set before any width-dependent measurement below so the card has
468
+ // already grown (or not) to its final width by the time its rect is
469
+ // read for placement.
470
+ const { start: horizontalStart, end: horizontalEnd } =
471
+ horizontalRegion(edge);
472
+ panel.style.setProperty(
473
+ '--float-ref-max-width',
474
+ `${Math.max(horizontalEnd - horizontalStart, 0)}px`,
475
+ );
476
+
303
477
  const headerBottom =
304
478
  document
305
479
  .querySelector<HTMLElement>('.header')
@@ -320,14 +494,6 @@ import HoverPanel from '../HoverPanel.astro';
320
494
  : regionRect.top - gap - rect.height;
321
495
  let left = anchorRect.left + anchorRect.width / 2 - rect.width / 2;
322
496
 
323
- const article = document.querySelector<HTMLElement>('.sl-markdown-content');
324
- const articleRect = article?.getBoundingClientRect();
325
- const horizontalStart = Math.max(edge, articleRect?.left ?? edge);
326
- const horizontalEnd = Math.min(
327
- window.innerWidth - edge,
328
- articleRect?.right ?? window.innerWidth - edge,
329
- );
330
-
331
497
  top = Math.max(verticalStart, Math.min(top, verticalEnd - rect.height));
332
498
  left =
333
499
  horizontalEnd - horizontalStart >= rect.width
@@ -391,6 +557,7 @@ import HoverPanel from '../HoverPanel.astro';
391
557
 
392
558
  const crossPage = crossPageHref(trigger, kind);
393
559
  if (crossPage) {
560
+ if (standalone) return;
394
561
  void showCrossPage(trigger, kind, crossPage);
395
562
  return;
396
563
  }
@@ -533,6 +700,14 @@ import HoverPanel from '../HoverPanel.astro';
533
700
 
534
701
  const isCrossPage = crossPageHref(trigger, kind) !== null;
535
702
 
703
+ // A diagram reference remains an ordinary navigation link. Hover may
704
+ // preview an off-screen diagram, but clicking must dismiss that card
705
+ // and take the reader to the real diagram in one gesture.
706
+ if (kind.jumpLabel === 'diagram' && !isCrossPage) {
707
+ clearAll();
708
+ return;
709
+ }
710
+
536
711
  // A card that is already open for this trigger — same-page *or*
537
712
  // cross-page — pins in place instead of navigating. The only way to
538
713
  // the float itself is now the card's own number/"Jump to …" link.
@@ -543,6 +718,12 @@ import HoverPanel from '../HoverPanel.astro';
543
718
  }
544
719
 
545
720
  if (isCrossPage) {
721
+ // The exporter rewrites this to the canonical course URL. Leave it
722
+ // as an ordinary link; an offline hover/click must never fetch it.
723
+ if (standalone) {
724
+ clearAll();
725
+ return;
726
+ }
546
727
  // No card open yet (keyboard, or a click that beat the hover): open
547
728
  // it — pinned — rather than leave the page.
548
729
  event.preventDefault();
@@ -599,7 +780,15 @@ import HoverPanel from '../HoverPanel.astro';
599
780
  window.addEventListener(
600
781
  'scroll',
601
782
  () => {
602
- if (mode === 'panel' && activeTrigger) position(activeTrigger);
783
+ if (mode !== 'panel' || !activeTrigger) return;
784
+ if (
785
+ activeTarget?.ownerDocument === document &&
786
+ isFullyVisible(activeTarget)
787
+ ) {
788
+ clearAll();
789
+ return;
790
+ }
791
+ position(activeTrigger);
603
792
  },
604
793
  { passive: true, capture: true },
605
794
  );
@@ -613,19 +802,28 @@ import HoverPanel from '../HoverPanel.astro';
613
802
  @layer starlight.components {
614
803
  .float-ref-panel-body {
615
804
  display: grid;
805
+ /* Grid items default to a content-based automatic minimum size, which
806
+ would let a wide equation inflate this track (and the card itself)
807
+ outward instead of scrolling inside `.float-ref-panel-content`. */
808
+ min-width: 0;
616
809
  gap: 0.6rem;
617
810
  }
618
811
 
619
812
  .float-ref-panel-main {
620
813
  display: flex;
621
- align-items: baseline;
814
+ min-width: 0;
815
+ /* Center, not baseline: a tall equation's own baseline sits wherever
816
+ its glyphs put it, which for a fraction or multi-row expression can
817
+ be well above center — baseline-aligning the number to that point
818
+ would leave it looking pinned near the top of the card, seemingly
819
+ above the equation, instead of beside it. */
820
+ align-items: center;
622
821
  justify-content: space-between;
623
822
  gap: 0.75rem;
624
823
  }
625
824
 
626
825
  .float-ref-panel-number {
627
826
  flex: none;
628
- align-self: flex-start;
629
827
  margin: 0;
630
828
  color: var(--sl-color-gray-3);
631
829
  font-weight: 600;
@@ -642,6 +840,12 @@ import HoverPanel from '../HoverPanel.astro';
642
840
  .float-ref-panel-content {
643
841
  min-width: 0;
644
842
  overflow-x: auto;
843
+ /* `overflow-x: auto` alone makes the block axis compute to `auto` too
844
+ (CSS Overflow §3), which would let a merely-tall equation grow its
845
+ own vertical scrollbar independent of the card's. Equations must
846
+ never scroll vertically on their own — only sideways, and only the
847
+ card as a whole. */
848
+ overflow-y: hidden;
645
849
  color: var(--sl-color-text);
646
850
  }
647
851
 
@@ -655,6 +859,20 @@ import HoverPanel from '../HoverPanel.astro';
655
859
 
656
860
  .float-ref-panel-content :global(table) {
657
861
  margin: 0;
862
+ /* 60% of the real table's own font-size (`global.css`'s
863
+ `.sl-markdown-content table` rule puts that at 90% of body text) —
864
+ this card sits outside `.sl-markdown-content` (moved to `<body>` at
865
+ runtime), so it's anchored to `--sl-text-base` directly rather than
866
+ inheriting that 90% rule. */
867
+ font-size: calc(var(--sl-text-base) * 0.9 * 0.6);
868
+ }
869
+
870
+ /* Same zebra striping formula as the real table
871
+ (`global.css`'s `.sl-markdown-content table … tbody tr:nth-child(even)`
872
+ rule), reproduced here because that rule is scoped to
873
+ `.sl-markdown-content`, which this cloned table sits outside of. */
874
+ .float-ref-panel-content :global(table tbody tr:nth-child(even)) {
875
+ background: color-mix(in srgb, var(--sl-color-bg) 85%, var(--sl-color-gray-5) 15%);
658
876
  }
659
877
 
660
878
  .float-ref-panel-caption {