explico 0.5.0 → 0.7.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 (88) hide show
  1. package/CHANGELOG.md +295 -7
  2. package/README.md +16 -2
  3. package/dist/compiler/astro.d.ts +13 -2
  4. package/dist/compiler/astro.js +39 -0
  5. package/dist/compiler/cli.d.ts +22 -3
  6. package/dist/compiler/cli.js +71 -10
  7. package/dist/compiler/course-config.d.ts +31 -1
  8. package/dist/compiler/course-config.js +7 -0
  9. package/dist/compiler/exports/standalone-html.js +13 -0
  10. package/dist/compiler/logging.d.ts +109 -0
  11. package/dist/compiler/logging.js +107 -0
  12. package/dist/compiler/manifest.d.ts +16 -4
  13. package/dist/compiler/manifest.js +7 -3
  14. package/dist/reference/base-abbreviations.mjs +8 -2
  15. package/dist/reference/consistency.d.ts +8 -10
  16. package/dist/reference/consistency.js +3 -47
  17. package/dist/reference/delimiter-size.d.ts +39 -0
  18. package/dist/reference/delimiter-size.js +177 -0
  19. package/dist/reference/diagrams-validate.d.ts +1 -1
  20. package/dist/reference/equations-validate.d.ts +2 -2
  21. package/dist/reference/equations-validate.js +1 -1
  22. package/dist/reference/figures-validate.d.ts +1 -1
  23. package/dist/reference/gate-heading-length.d.ts +12 -0
  24. package/dist/reference/gate-heading-length.js +79 -0
  25. package/dist/reference/gate-math.d.ts +8 -2
  26. package/dist/reference/gate-math.js +30 -7
  27. package/dist/reference/keyed-float-validate.d.ts +2 -2
  28. package/dist/reference/keyed-float-validate.js +1 -1
  29. package/dist/reference/math-glyphs.d.mts +12 -0
  30. package/dist/reference/math-glyphs.mjs +183 -42
  31. package/dist/reference/references.js +2 -1
  32. package/dist/reference/registry.js +1 -1
  33. package/dist/reference/rehype-base-links.d.mts +23 -0
  34. package/dist/reference/rehype-base-links.mjs +40 -0
  35. package/dist/reference/remark-abbreviations.mjs +10 -12
  36. package/dist/reference/remark-citation.d.mts +5 -1
  37. package/dist/reference/remark-citation.mjs +0 -0
  38. package/dist/reference/render-lab-math.d.ts +18 -1
  39. package/dist/reference/render-lab-math.js +10 -3
  40. package/dist/reference/tables-validate.d.ts +1 -1
  41. package/dist/reference/types.d.ts +1 -1
  42. package/docs/VERSIONING.md +24 -4
  43. package/docs/authoring/README.md +14 -2
  44. package/docs/authoring/agents.md +7 -3
  45. package/docs/authoring/components.md +4 -1
  46. package/docs/authoring/migrating.md +477 -8
  47. package/docs/authoring/notation.md +194 -7
  48. package/docs/authoring/prose.md +64 -0
  49. package/docs/authoring/structure-rules.md +69 -0
  50. package/docs/authoring/visualizations.md +2 -2
  51. package/package.json +1 -1
  52. package/src/compiler/astro.ts +89 -0
  53. package/src/compiler/cli.ts +144 -12
  54. package/src/compiler/course-config.ts +40 -0
  55. package/src/compiler/exports/standalone-html.ts +15 -0
  56. package/src/compiler/logging.ts +248 -0
  57. package/src/compiler/manifest.ts +20 -3
  58. package/src/components/diagram/Diagram.astro +0 -55
  59. package/src/components/memo/Memo.astro +2 -5
  60. package/src/components/memo/ShortNote.astro +2 -5
  61. package/src/components/notation/NotationFormula.astro +199 -10
  62. package/src/components/notation/NotationGlossary.astro +89 -58
  63. package/src/components/notation/NotationLayer.astro +238 -156
  64. package/src/components/scroll-animation.ts +160 -0
  65. package/src/components/starlight/AnchorScrollEnhancer.astro +102 -0
  66. package/src/components/starlight/HeadingAnchorEnhancer.astro +2 -4
  67. package/src/components/starlight/KeyedFloatEnhancer.astro +17 -16
  68. package/src/components/starlight/LessonFooter.astro +5 -1
  69. package/src/content-config.ts +16 -1
  70. package/src/reference/base-abbreviations.mjs +8 -2
  71. package/src/reference/consistency.ts +10 -66
  72. package/src/reference/delimiter-size.ts +301 -0
  73. package/src/reference/diagrams-validate.ts +1 -1
  74. package/src/reference/equations-validate.ts +3 -3
  75. package/src/reference/figures-validate.ts +1 -1
  76. package/src/reference/gate-heading-length.ts +131 -0
  77. package/src/reference/gate-math.ts +48 -8
  78. package/src/reference/keyed-float-validate.ts +3 -3
  79. package/src/reference/math-glyphs.mjs +183 -42
  80. package/src/reference/references.ts +2 -1
  81. package/src/reference/registry.ts +1 -1
  82. package/src/reference/rehype-base-links.mjs +40 -0
  83. package/src/reference/remark-abbreviations.mjs +10 -12
  84. package/src/reference/remark-citation.mjs +0 -0
  85. package/src/reference/render-lab-math.ts +24 -1
  86. package/src/reference/tables-validate.ts +1 -1
  87. package/src/reference/types.ts +1 -1
  88. package/src/styles/global.css +120 -15
@@ -42,6 +42,10 @@ export interface Props {
42
42
  * links to reveal an explanation on hover.
43
43
  */
44
44
  presentation?: 'full' | 'panel';
45
+ /** Author-chosen state for a reader with nothing saved yet. Default: `false` (closed) — see `docs/authoring` on `CourseConfig.disclosureDefaults`. */
46
+ defaultOpen?: boolean;
47
+ /** Persist the reader's open/closed choice across visits, per page. Default: `true`. */
48
+ rememberState?: boolean;
45
49
  }
46
50
 
47
51
  import NotationMath from './NotationMath.astro';
@@ -55,6 +59,8 @@ const {
55
59
  label = 'Notation used on this page',
56
60
  glossaryHref = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/glossary/`,
57
61
  presentation = 'full',
62
+ defaultOpen = false,
63
+ rememberState = true,
58
64
  } = Astro.props;
59
65
 
60
66
  const panelOnly = presentation === 'panel';
@@ -111,16 +117,16 @@ for (const gloss of glossItems) {
111
117
  class:list={['notation-layer', panelOnly && 'notation-layer--panel-only']}
112
118
  data-notation-layer
113
119
  data-notation-layer-id={id}
114
- aria-labelledby={panelOnly ? undefined : `${id}-heading`}
115
- aria-label={panelOnly ? 'Notation explanations' : undefined}
120
+ aria-label={panelOnly ? 'Notation explanations' : label}
116
121
  >
117
- {!panelOnly && (
118
- <h2 id={`${id}-heading`} class="notation-layer-heading">
119
- Page notation
120
- </h2>
121
- )}
122
-
123
- <details class="notation-list" hidden={panelOnly}>
122
+ <div class="notation-list-titlebar">
123
+ <details
124
+ class="notation-list"
125
+ id={id}
126
+ hidden={panelOnly}
127
+ data-remember-state={String(rememberState)}
128
+ open={defaultOpen}
129
+ >
124
130
  <summary>
125
131
  {label} <span class="notation-count">({items.length})</span>
126
132
  </summary>
@@ -183,16 +189,6 @@ for (const gloss of glossItems) {
183
189
  <strong>Units:</strong> {definition.units}
184
190
  </p>
185
191
  )}
186
- <button
187
- class="notation-pin"
188
- type="button"
189
- data-notation-control={definition.key}
190
- aria-expanded="false"
191
- aria-pressed="false"
192
- hidden
193
- >
194
- Pin explanation
195
- </button>
196
192
  </dd>
197
193
  </div>
198
194
  ))}
@@ -216,14 +212,32 @@ for (const gloss of glossItems) {
216
212
  </div>
217
213
  ))}
218
214
  </details>
215
+ {!panelOnly && (
216
+ <a
217
+ class="notation-list-anchor"
218
+ href={`#${id}`}
219
+ aria-label={`${label}, titled section`}
220
+ >
221
+ <svg
222
+ aria-hidden="true"
223
+ width="16"
224
+ height="16"
225
+ viewBox="0 0 24 24"
226
+ fill="currentColor"
227
+ ><path
228
+ d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"
229
+ ></path></svg
230
+ >
231
+ </a>
232
+ )}
233
+ </div>
219
234
 
220
235
  {!panelOnly && (
221
236
  <p class="notation-enhancement-help" data-notation-help hidden>
222
237
  Point to a marked symbol for a quick explanation, or use the list
223
238
  above — pointing at a symbol inside an open card explains that one
224
- too, alongside it. Click a symbol, or select “Pin explanation”, to
225
- keep a card open; click the card (or its “×”) to close it, or press
226
- Escape.
239
+ too, alongside it. Click a symbol to keep its card open; click the
240
+ card (or its “×”) to close it, or press Escape.
227
241
  </p>
228
242
  )}
229
243
 
@@ -249,14 +263,6 @@ for (const gloss of glossItems) {
249
263
  </header>
250
264
  <p data-slot-summary hidden />
251
265
  <div data-slot-formula hidden />
252
- <button
253
- class="notation-popup-copy"
254
- type="button"
255
- data-popup-copy
256
- hidden
257
- >
258
- Copy LaTeX
259
- </button>
260
266
  <p class="notation-units" data-slot-units hidden />
261
267
  <a data-slot-link hidden>
262
268
  Open the full definition
@@ -270,6 +276,7 @@ for (const gloss of glossItems) {
270
276
 
271
277
  <script>
272
278
  import { createPopupStack, closestElement } from '../popup-stack';
279
+ import { wirePersistedDetails } from '../disclosure/persisted-disclosure';
273
280
 
274
281
  const triggerSelector = '[data-notation-key]';
275
282
 
@@ -296,10 +303,6 @@ for (const gloss of glossItems) {
296
303
  );
297
304
  if (!template) return;
298
305
 
299
- const controls = Array.from(
300
- root.querySelectorAll<HTMLButtonElement>('[data-notation-control]'),
301
- );
302
-
303
306
  // Static triggers, computed once: markers embedded in prose or an
304
307
  // equation, and the symbol glyphs in the "Notation used on this page"
305
308
  // list. These get the always-on `.notation-trigger` cursor/hover
@@ -324,20 +327,32 @@ for (const gloss of glossItems) {
324
327
  for (const trigger of highlightTargets) {
325
328
  trigger.classList.add('notation-trigger');
326
329
  }
327
- for (const control of controls) control.hidden = false;
328
330
  const help = root.querySelector<HTMLElement>('[data-notation-help]');
329
331
  if (help) help.hidden = false;
330
332
 
331
- const keyFor = (element: HTMLElement | null) =>
332
- element?.dataset.notationKey ??
333
- element?.dataset.notationControl ??
334
- element?.dataset.notationLink ??
335
- null;
333
+ // The visible list's own symbol glyphs are the only keyboard path to
334
+ // pinning a card from this list (the old "Pin explanation" button was
335
+ // the other one, and is gone) — make them a real, labelled, Enter/Space-
336
+ // operable control rather than a mouse-only `<span>`. `symbolTriggers`
337
+ // (defined above) is already scoped to entries inside this list.
338
+ for (const trigger of symbolTriggers) {
339
+ const key = trigger.dataset.notationKey ?? '';
340
+ const entryLabel =
341
+ entries.get(key)?.querySelector<HTMLElement>('[data-notation-link]')
342
+ ?.textContent?.trim() ?? key;
343
+ trigger.setAttribute('role', 'button');
344
+ trigger.tabIndex = 0;
345
+ trigger.setAttribute('aria-pressed', 'false');
346
+ trigger.setAttribute('aria-label', `Pin the ${entryLabel} explanation`);
347
+ trigger.addEventListener('keydown', (event) => {
348
+ if (event.key !== 'Enter' && event.key !== ' ') return;
349
+ event.preventDefault();
350
+ trigger.click();
351
+ });
352
+ }
336
353
 
337
- const controlFrom = (node: EventTarget | null) => {
338
- const candidate = closestElement(node, '[data-notation-control]');
339
- return candidate && root.contains(candidate) ? candidate : null;
340
- };
354
+ const keyFor = (element: HTMLElement | null) =>
355
+ element?.dataset.notationKey ?? element?.dataset.notationLink ?? null;
341
356
 
342
357
  const staticLinkFrom = (node: EventTarget | null) => {
343
358
  const candidate = closestElement(node, '[data-notation-link]');
@@ -357,7 +372,7 @@ for (const gloss of glossItems) {
357
372
  };
358
373
 
359
374
  const triggerFrom = (node: EventTarget | null) =>
360
- controlFrom(node) ?? staticLinkFrom(node) ?? symbolFrom(node);
375
+ staticLinkFrom(node) ?? symbolFrom(node);
361
376
 
362
377
  const labelOf = (entry: HTMLElement, key: string) =>
363
378
  entry
@@ -382,18 +397,7 @@ for (const gloss of glossItems) {
382
397
  const units = fragment.querySelector<HTMLElement>('[data-slot-units]');
383
398
  const link =
384
399
  fragment.querySelector<HTMLAnchorElement>('[data-slot-link]');
385
- const copyButton = fragment.querySelector<HTMLButtonElement>(
386
- '[data-popup-copy]',
387
- );
388
- if (
389
- !el ||
390
- !symbolHost ||
391
- !title ||
392
- !summary ||
393
- !formulaHost ||
394
- !units ||
395
- !copyButton
396
- ) {
400
+ if (!el || !symbolHost || !title || !summary || !formulaHost || !units) {
397
401
  return null;
398
402
  }
399
403
 
@@ -420,34 +424,6 @@ for (const gloss of glossItems) {
420
424
  );
421
425
  formulaHost.hidden = sourceFormula === null;
422
426
 
423
- // The formula's own `data-latex` carries the plain, authored LaTeX —
424
- // not the `\explain{key}{…}`-wrapped source KaTeX actually rendered —
425
- // so what a reader copies is what they'd write themselves, not an
426
- // internal marker macro. See `render-notation-math.ts:renderNotationFormula`.
427
- const formulaLatex = sourceFormula?.dataset.latex ?? '';
428
- copyButton.hidden = formulaLatex.length === 0;
429
- if (formulaLatex.length > 0) {
430
- copyButton.addEventListener('click', (event) => {
431
- // Otherwise this bubbles to the document click handler in
432
- // `popup-stack.ts`, which treats any click inside a popup that
433
- // isn't a trigger or a link as "close it" — that would remove the
434
- // card before the "Copied!" flash below ever paints.
435
- event.stopPropagation();
436
- if (!navigator.clipboard) return;
437
- navigator.clipboard.writeText(formulaLatex).then(
438
- () => {
439
- copyButton.dataset.copied = 'true';
440
- window.setTimeout(() => {
441
- delete copyButton.dataset.copied;
442
- }, 1200);
443
- },
444
- () => {
445
- /* Clipboard blocked: nothing more we can do here. */
446
- },
447
- );
448
- });
449
- }
450
-
451
427
  setOptionalText(
452
428
  units,
453
429
  entry
@@ -465,21 +441,6 @@ for (const gloss of glossItems) {
465
441
  return el;
466
442
  };
467
443
 
468
- const updateControls = (
469
- open: ReadonlySet<string>,
470
- pinned: ReadonlySet<string>,
471
- ) => {
472
- for (const control of controls) {
473
- const key = control.dataset.notationControl ?? '';
474
- const selected = pinned.has(key);
475
- control.setAttribute('aria-pressed', String(selected));
476
- control.setAttribute('aria-expanded', String(open.has(key)));
477
- control.textContent = selected
478
- ? 'Unpin explanation'
479
- : 'Pin explanation';
480
- }
481
- };
482
-
483
444
  createPopupStack({
484
445
  // Popups are `position: fixed` and float above everything regardless
485
446
  // of DOM placement — *except* z-index only ever wins within its own
@@ -493,7 +454,6 @@ for (const gloss of glossItems) {
493
454
  triggerFrom,
494
455
  keyFor,
495
456
  createPopup,
496
- pinControlFrom: controlFrom,
497
457
  onChange: (open, pinned) => {
498
458
  for (const trigger of highlightTargets) {
499
459
  trigger.classList.toggle(
@@ -501,16 +461,123 @@ for (const gloss of glossItems) {
501
461
  open.has(trigger.dataset.notationKey ?? ''),
502
462
  );
503
463
  }
504
- updateControls(open, pinned);
464
+ for (const trigger of symbolTriggers) {
465
+ trigger.setAttribute(
466
+ 'aria-pressed',
467
+ String(pinned.has(trigger.dataset.notationKey ?? '')),
468
+ );
469
+ }
505
470
  },
506
471
  });
507
472
  }
508
473
 
474
+ function setupNotationDisclosure(details: HTMLDetailsElement) {
475
+ if (details.dataset.disclosureWired === 'true') return;
476
+ details.dataset.disclosureWired = 'true';
477
+
478
+ // Same convention as `Figure.astro`: the SSR-rendered `open` attribute
479
+ // already carries the author's `defaultOpen` prop verbatim.
480
+ const defaultOpen = details.open;
481
+ if (details.dataset.rememberState === 'true') {
482
+ wirePersistedDetails(details, `notation:${details.id}`, defaultOpen);
483
+ }
484
+
485
+ // Deep-linking to `#page-notation` (the anchor link, or a shared URL)
486
+ // must reveal it even if a reader left it collapsed.
487
+ const openIfTargeted = () => {
488
+ if (location.hash === `#${details.id}`) details.open = true;
489
+ };
490
+ openIfTargeted();
491
+ window.addEventListener('hashchange', openIfTargeted);
492
+ }
493
+
509
494
  for (const root of document.querySelectorAll<HTMLElement>(
510
495
  '[data-notation-layer]',
511
496
  )) {
512
497
  setupNotationLayer(root);
498
+ const details = root.querySelector<HTMLDetailsElement>(
499
+ ':scope > .notation-list-titlebar > .notation-list[id]',
500
+ );
501
+ if (details) setupNotationDisclosure(details);
513
502
  }
503
+
504
+ // "Copy LaTeX" for a notation formula (a glossary entry's own numbered
505
+ // formula, or one named inline via `\explain{}` inside a popup/`Diagram`),
506
+ // two ways: its own button (only a numbered formula has one —
507
+ // `data-notation-formula-copy`, `NotationFormula.astro`), or a click
508
+ // anywhere else in the box that isn't the number link or an explained
509
+ // symbol — the same region `.notation-formula-math:hover` highlights
510
+ // (`NotationFormula.astro`), which includes the reserved gutter between
511
+ // the equation and its number, not just the rendered `.katex-html`. This
512
+ // listener lives here, not in `NotationFormula.astro` or
513
+ // `KeyedFloatEnhancer.astro`, because this component loads on every page —
514
+ // it's the Starlight `Footer` override — while `KeyedFloatEnhancer` is
515
+ // lesson-only and a formula can appear inside a hover popup mounted
516
+ // outside `.sl-markdown-content`.
517
+ document.addEventListener('click', (event) => {
518
+ const origin = event.target instanceof Element ? event.target : null;
519
+ if (!origin) return;
520
+
521
+ const ownButton = origin.closest<HTMLButtonElement>(
522
+ '[data-notation-formula-copy]',
523
+ );
524
+ const container = ownButton
525
+ ? null
526
+ : origin.closest<HTMLElement>('[data-notation-formula]');
527
+ if (!ownButton) {
528
+ if (origin.closest('[data-notation-key], .notation-formula-number')) {
529
+ return;
530
+ }
531
+ if (!container) return;
532
+ }
533
+
534
+ const feedbackTarget =
535
+ ownButton ??
536
+ container?.querySelector<HTMLElement>('[data-notation-formula-copy]') ??
537
+ container;
538
+ const latex =
539
+ ownButton?.dataset.latex ?? feedbackTarget?.dataset.latex ?? '';
540
+ if (!feedbackTarget || latex.length === 0 || !navigator.clipboard) return;
541
+
542
+ navigator.clipboard.writeText(latex).then(
543
+ () => {
544
+ feedbackTarget.dataset.copied = 'true';
545
+ window.setTimeout(() => {
546
+ delete feedbackTarget.dataset.copied;
547
+ }, 1200);
548
+ },
549
+ () => {
550
+ /* Clipboard blocked: nothing more we can do here. */
551
+ },
552
+ );
553
+ });
554
+
555
+ // A tooltip for the click gesture above, and only for the part of the
556
+ // equation it actually applies to — an explained symbol has its own hover
557
+ // card (created above) and needs no tooltip layered on top of it, and the
558
+ // copy button already carries a static `title`. `mouseover`/`mouseout` (not
559
+ // `mouseenter`/`mouseleave`) because they bubble, which this document-level
560
+ // delegation needs.
561
+ document.addEventListener('mouseover', (event) => {
562
+ const origin = event.target instanceof Element ? event.target : null;
563
+ if (!origin || origin.getAttribute('title') === 'Copy LaTeX') return;
564
+ if (
565
+ origin.closest(
566
+ '[data-notation-key], [data-notation-formula-copy], .notation-formula-number',
567
+ )
568
+ ) {
569
+ return;
570
+ }
571
+ if (!origin.closest('[data-notation-formula]')) return;
572
+ origin.setAttribute('title', 'Copy LaTeX');
573
+ });
574
+
575
+ document.addEventListener('mouseout', (event) => {
576
+ const origin = event.target instanceof Element ? event.target : null;
577
+ if (origin?.getAttribute('title') === 'Copy LaTeX') {
578
+ origin.removeAttribute('title');
579
+ }
580
+ });
514
581
  </script>
515
582
 
516
583
  <style>
@@ -526,9 +593,13 @@ for (const gloss of glossItems) {
526
593
  margin: 0;
527
594
  }
528
595
 
529
- .notation-layer-heading {
530
- margin: 0 0 0.65rem;
531
- font-size: var(--sl-text-lg);
596
+ /* Positioning context for `.notation-list-anchor`, mirroring
597
+ `Figure.astro`'s `.keyed-figure__titlebar` — the anchor link is a DOM
598
+ sibling of `<details>`, not nested inside `<summary>` (a focusable `<a>`
599
+ inside `<summary>` trips axe's `nested-interactive` check the same way
600
+ it would there). */
601
+ .notation-list-titlebar {
602
+ position: relative;
532
603
  }
533
604
 
534
605
  .notation-list {
@@ -539,8 +610,52 @@ for (const gloss of glossItems) {
539
610
 
540
611
  .notation-list > summary {
541
612
  padding: 0.8rem 1rem;
613
+ padding-inline-end: 2.2rem;
542
614
  cursor: pointer;
543
615
  font-weight: 600;
616
+ font-size: var(--sl-text-lg);
617
+ }
618
+
619
+ .notation-list-anchor {
620
+ position: absolute;
621
+ top: 0.9rem;
622
+ inset-inline-end: 1rem;
623
+ display: inline-flex;
624
+ align-items: center;
625
+ justify-content: center;
626
+ width: 1em;
627
+ height: 1em;
628
+ font-size: var(--sl-text-lg);
629
+ color: var(--sl-color-gray-3);
630
+ text-decoration: none;
631
+ -webkit-user-select: none;
632
+ user-select: none;
633
+ }
634
+
635
+ .notation-list-anchor svg {
636
+ width: 0.7em;
637
+ height: 0.7em;
638
+ }
639
+
640
+ .notation-list-anchor:hover,
641
+ .notation-list-anchor:focus-visible {
642
+ color: var(--sl-color-text-accent);
643
+ }
644
+
645
+ .notation-list-anchor:focus-visible {
646
+ outline: 2px solid var(--sl-color-accent);
647
+ outline-offset: 0.15rem;
648
+ }
649
+
650
+ @media (hover: hover) {
651
+ .notation-list-anchor {
652
+ opacity: 0;
653
+ }
654
+
655
+ .notation-list-anchor:focus-visible,
656
+ .notation-list-titlebar:hover .notation-list-anchor {
657
+ opacity: 1;
658
+ }
544
659
  }
545
660
 
546
661
  .notation-count,
@@ -594,14 +709,28 @@ for (const gloss of glossItems) {
594
709
  cursor: help;
595
710
  }
596
711
 
712
+ /* Entries in the visible list are keyboard-operable (Enter/Space pins
713
+ the same card a click would) now that the list has no separate "Pin
714
+ explanation" button of its own — see the setup script below. */
715
+ .notation-entry .notation-symbol-trigger {
716
+ cursor: pointer;
717
+ }
718
+
597
719
  .notation-symbol-trigger:hover,
720
+ .notation-symbol-trigger:focus-visible,
598
721
  .notation-symbol-trigger.is-notation-active {
599
722
  background: var(--sl-color-accent-low);
600
723
  box-shadow: 0 0 0 0.12em var(--sl-color-accent-low);
601
724
  }
602
725
 
726
+ .notation-symbol-trigger:focus-visible {
727
+ outline: 2px solid var(--sl-color-accent);
728
+ outline-offset: 0.1em;
729
+ }
730
+
603
731
  @media (forced-colors: active) {
604
732
  .notation-symbol-trigger:hover,
733
+ .notation-symbol-trigger:focus-visible,
605
734
  .notation-symbol-trigger.is-notation-active {
606
735
  outline: 2px solid Highlight;
607
736
  color: HighlightText;
@@ -624,6 +753,10 @@ for (const gloss of glossItems) {
624
753
  font-size: var(--sl-text-sm);
625
754
  }
626
755
 
756
+ /* Cursor, hover highlight, and "Copied!" flash for this plain-span
757
+ formula fallback (a definition with no `formulaScope`) come from the
758
+ generic `[data-notation-formula]` rules in `global.css` — shared with
759
+ every other producer of that attribute, not reimplemented here. */
627
760
  .notation-formula {
628
761
  overflow-x: auto;
629
762
  /* `overflow-x: auto` alone makes the block axis compute to `auto` too
@@ -634,24 +767,6 @@ for (const gloss of glossItems) {
634
767
  color: var(--sl-color-text);
635
768
  }
636
769
 
637
- .notation-pin {
638
- justify-self: start;
639
- padding: 0.25rem 0.55rem;
640
- border: 1px solid var(--sl-color-gray-5);
641
- border-radius: 0.35rem;
642
- color: var(--sl-color-text-accent);
643
- background: var(--sl-color-bg);
644
- font: inherit;
645
- font-size: var(--sl-text-sm);
646
- cursor: pointer;
647
- }
648
-
649
- .notation-pin:hover,
650
- .notation-pin[aria-pressed='true'] {
651
- border-color: var(--sl-color-accent);
652
- background: var(--sl-color-accent-low);
653
- }
654
-
655
770
  .notation-enhancement-help {
656
771
  margin: 0.55rem 0 0;
657
772
  font-size: var(--sl-text-xs);
@@ -743,39 +858,6 @@ for (const gloss of glossItems) {
743
858
  line-height: 1.5;
744
859
  }
745
860
 
746
- .notation-popup-copy {
747
- position: relative;
748
- margin-block: 0.4rem 0.6rem;
749
- padding: 0.2rem 0.55rem;
750
- border: 1px solid var(--sl-color-gray-5);
751
- border-radius: 0.35rem;
752
- color: var(--sl-color-text-accent);
753
- background: var(--sl-color-bg);
754
- font: inherit;
755
- font-size: var(--sl-text-xs);
756
- cursor: pointer;
757
- }
758
-
759
- .notation-popup-copy:hover {
760
- border-color: var(--sl-color-accent);
761
- background: var(--sl-color-accent-low);
762
- }
763
-
764
- .notation-popup-copy[data-copied]::after {
765
- content: 'Copied!';
766
- position: absolute;
767
- inset-block-end: calc(100% + 0.35rem);
768
- inset-inline-start: 50%;
769
- translate: -50% 0;
770
- padding: 0.15rem 0.45rem;
771
- border-radius: 0.35rem;
772
- background: var(--sl-color-accent-low);
773
- color: var(--sl-color-text-accent);
774
- font-size: var(--sl-text-xs);
775
- white-space: nowrap;
776
- pointer-events: none;
777
- }
778
-
779
861
  .notation-panel-body [data-slot-formula] {
780
862
  overflow-x: auto;
781
863
  /* See `.notation-formula` above: pairs with `overflow-x` to stop the