@erclx/canon 4.109.0 → 4.111.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 (33) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/{canon-feedback-file → canon-feedback}/REQUIREMENT.md +7 -6
  3. package/claude/skills/{canon-feedback-file → canon-feedback}/SKILL.md +9 -19
  4. package/claude/skills/canon-feedback-triage/REQUIREMENT.md +1 -1
  5. package/claude/skills/canon-feedback-triage/SKILL.md +1 -1
  6. package/claude/skills/canon-operator/REQUIREMENT.md +2 -2
  7. package/claude/skills/canon-operator/SKILL.md +3 -4
  8. package/claude/skills/draft-wiki/REQUIREMENT.md +42 -0
  9. package/claude/skills/draft-wiki/SKILL.md +83 -0
  10. package/claude/skills/git-issue/REQUIREMENT.md +1 -1
  11. package/claude/skills/git-issue/SKILL.md +1 -1
  12. package/claude/skills/target-check/references/domains.md +2 -2
  13. package/docs/agents/install-and-sync.md +4 -13
  14. package/docs/agents/teach.md +4 -2
  15. package/docs/target-projects.md +0 -2
  16. package/docs/workflow/ai-workflow.md +8 -11
  17. package/package.json +1 -1
  18. package/scripts/core/capture-home.sh +59 -0
  19. package/src/claude/cases/authoring.ts +5 -0
  20. package/src/claude/cases/setup.ts +3 -24
  21. package/src/commands/sync.ts +0 -16
  22. package/src/design/components.ts +459 -147
  23. package/src/shipped/references.ts +1 -2
  24. package/src/sync/reverse.ts +3 -74
  25. package/src/teach/nav.ts +410 -58
  26. package/claude/skills/migration-claude-md/REQUIREMENT.md +0 -40
  27. package/claude/skills/migration-claude-md/SKILL.md +0 -79
  28. package/claude/skills/migration-context/REQUIREMENT.md +0 -36
  29. package/claude/skills/migration-context/SKILL.md +0 -97
  30. package/claude/skills/migration-standards-drop/REQUIREMENT.md +0 -55
  31. package/claude/skills/migration-standards-drop/SKILL.md +0 -113
  32. package/claude/skills/migration-superseded/REQUIREMENT.md +0 -44
  33. package/claude/skills/migration-superseded/SKILL.md +0 -115
@@ -200,10 +200,20 @@ export const COMPONENTS: readonly Component[] = [
200
200
  HAND_DRAWN_FIGURE,
201
201
  ]
202
202
 
203
+ /**
204
+ * Where the course sidebar stops being a column and becomes an overlay.
205
+ *
206
+ * The media query below and both `matchMedia` calls in `@/teach/nav` read this
207
+ * one value. Three literals is what it was, and each had a test asserting its
208
+ * own copy, so a half-finished move passed green while the scripts switched at
209
+ * one width and the layout at another.
210
+ */
211
+ export const TEACH_SIDEBAR_BREAKPOINT = 1100
212
+
203
213
  const TEACH_CHROME: Component = {
204
214
  name: 'teach-chrome',
205
215
  note: [
206
- 'The masthead, breadcrumb, jump menu, theme toggle, progress track, and footer',
216
+ 'The masthead, breadcrumb, jump menu, theme toggle, course sidebar, and footer',
207
217
  'navigation shared by every teach page. Recovered from two gitignored',
208
218
  'course.css files that predate a regression that dropped this layer from the',
209
219
  "generator, and rewritten onto this module's tokens rather than the",
@@ -213,6 +223,7 @@ const TEACH_CHROME: Component = {
213
223
  reads: [
214
224
  '--color-background',
215
225
  '--color-surface',
226
+ '--color-chrome',
216
227
  '--color-border',
217
228
  '--color-text',
218
229
  '--color-text-body',
@@ -222,7 +233,6 @@ const TEACH_CHROME: Component = {
222
233
  '--teach-sans',
223
234
  '--teach-mono',
224
235
  '--teach-measure',
225
- '--teach-chrome',
226
236
  '--teach-shadow',
227
237
  '--color-teach-accent-bg',
228
238
  '--type-body-family',
@@ -230,14 +240,14 @@ const TEACH_CHROME: Component = {
230
240
  '--t3',
231
241
  '--t4',
232
242
  '--t5',
243
+ '--t6',
233
244
  ],
234
245
  rules: `:root {
235
246
  --teach-sans: var(--type-body-family);
236
247
  --teach-hand: 'Virgil', 'Excalifont', cursive;
237
248
  --teach-mono: var(--type-code-family);
238
249
  --teach-measure: 52rem;
239
- --teach-chrome: 52rem;
240
- --teach-mast-h: 4.4rem;
250
+ --teach-mast-h: 3.5rem;
241
251
  --teach-shadow: 0 1px 2px rgba(20, 20, 20, 0.04);
242
252
  --color-teach-accent-bg: color-mix(in srgb, var(--color-accent) 14%, var(--color-background));
243
253
 
@@ -267,11 +277,16 @@ const TEACH_CHROME: Component = {
267
277
 
268
278
  html { scroll-padding-top: calc(var(--teach-mast-h) + 1rem); }
269
279
 
270
- /* No horizontal padding on the body, so the sticky bar reaches both window
271
- edges. The gutter moves onto \`main\`, which is the only thing that needed it. */
280
+ /* No padding at all on the body, so the sticky bar reaches both window edges
281
+ and the sidebar can travel the full height. The gutter moves onto \`main\` and
282
+ the bottom room onto \`.pane\`: a flex item's sticky containing block is the
283
+ flex container's content box, so padding here is space the sidebar could
284
+ never reach and it would ride up by exactly that much near the foot. */
272
285
  body {
273
286
  margin: 0;
274
- padding: 0 0 7rem;
287
+ padding: 0;
288
+ display: flex;
289
+ align-items: flex-start;
275
290
  background: var(--color-background);
276
291
  color: var(--color-text-body);
277
292
  font-family: var(--teach-sans);
@@ -287,6 +302,24 @@ body {
287
302
  main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); margin: 0 auto; padding: 2.25rem 1.5rem 0; }
288
303
  .wide-body { max-width: calc(var(--teach-measure) + 3rem); }
289
304
 
305
+ /* Everything but the sidebar. \`flow-root\` rather than a bare block, because a
306
+ bottom margin collapsing out of here makes the document taller than the flex
307
+ container and a sticky sidebar can only travel inside its containing block,
308
+ so it would ride up by exactly the escaped margin on a tall window. The
309
+ border box is what keeps \`min-height: 100vh\` plus the bottom room from
310
+ scrolling every short page by that padding. */
311
+ .pane {
312
+ flex: 1 1 auto;
313
+ min-width: 0;
314
+ display: flow-root;
315
+ box-sizing: border-box;
316
+ min-height: 100vh;
317
+ padding-bottom: 7rem;
318
+ border-left: 1px solid var(--color-border);
319
+ }
320
+
321
+ html.sb-shut .pane { border-left: 0; }
322
+
290
323
  .bar {
291
324
  position: sticky;
292
325
  top: 0;
@@ -294,37 +327,44 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
294
327
  background: var(--color-background);
295
328
  border-bottom: 1px solid var(--color-border);
296
329
  padding: 0 1.5rem;
330
+ cursor: default;
297
331
  }
298
332
 
333
+ /* The reading bar rides the masthead's own bottom edge rather than arriving as
334
+ a second element. It reports position inside the lesson, which nothing
335
+ reported, where the retired track reported position in the course, which the
336
+ sidebar now reports. */
337
+ .bar::after {
338
+ content: "";
339
+ position: absolute;
340
+ left: 0;
341
+ bottom: -1px;
342
+ height: 2px;
343
+ width: var(--read, 0%);
344
+ background: var(--color-accent);
345
+ }
346
+
347
+ /* The height is the token rather than the sum of two paddings, so the sidebar's
348
+ own top row lands on the same seam the bar's bottom border draws. The row
349
+ spans the window rather than the reading measure: capped, the toggle and the
350
+ theme control floated in from the edges by an amount that changed per page. */
299
351
  .mast {
300
- max-width: var(--teach-chrome);
301
- margin: 0 auto;
302
- padding: 0.8rem 0 0.7rem;
352
+ max-width: none;
353
+ margin: 0;
354
+ min-height: var(--teach-mast-h);
355
+ padding: 0 0.9rem;
303
356
  display: flex;
304
357
  justify-content: space-between;
305
358
  align-items: center;
306
- gap: 1rem;
359
+ gap: 0.55rem;
307
360
  font-size: var(--t5);
308
361
  color: var(--color-muted);
362
+ cursor: default;
309
363
  }
310
364
 
311
365
  .mast a { color: var(--color-accent); text-decoration: none; font-weight: 600; }
312
366
  .mast a:hover { text-decoration: underline; }
313
367
 
314
- .theme {
315
- font-family: var(--teach-sans);
316
- font-size: 0.75rem;
317
- font-weight: 600;
318
- color: var(--color-muted);
319
- background: transparent;
320
- border: 1px solid var(--color-border);
321
- border-radius: 99px;
322
- padding: 0.25rem 0.7rem;
323
- cursor: pointer;
324
- }
325
-
326
- .theme:hover { border-color: var(--color-accent); color: var(--color-accent); }
327
-
328
368
  .nav {
329
369
  width: 100%;
330
370
  margin: 0;
@@ -371,7 +411,7 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
371
411
  restates either. */
372
412
  /* A breadcrumb rather than a row of links. The gap tightens because the
373
413
  separators now carry the spacing the gap used to. */
374
- .mast-left { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
414
+ .mast-left { display: flex; align-items: center; gap: 0.3rem; min-width: 0; flex-wrap: wrap; cursor: default; }
375
415
 
376
416
  /* \`--rule\` measured 1.27:1 here. It is a border token, and a border is a
377
417
  shape a reader infers rather than a glyph they resolve, so it is the wrong
@@ -379,6 +419,7 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
379
419
  .crumb-sep {
380
420
  color: var(--color-muted);
381
421
  font-size: var(--t4);
422
+ margin: 0 0.1rem;
382
423
  user-select: none;
383
424
  }
384
425
 
@@ -391,27 +432,55 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
391
432
 
392
433
  .crumb { display: inline-flex; align-items: center; }
393
434
 
394
- /* The label and its caret are one segment visually and two controls in fact:
395
- the label navigates, the caret opens the menu. */
396
- .crumb-item { display: inline-flex; align-items: center; gap: 0.1rem; }
435
+ /* The label and its caret are one component with two zones, like a split
436
+ button: the label navigates, the caret opens the menu, and one surface fills
437
+ for both so the pair reads as a single chip rather than two loose pieces.
438
+ The pointer covers the gap between them, which is interactive on both sides. */
439
+ .crumb-item {
440
+ display: inline-flex;
441
+ align-items: center;
442
+ gap: 0;
443
+ border-radius: 7px;
444
+ padding: 0.1rem 0.15rem 0.1rem 0.35rem;
445
+ cursor: pointer;
446
+ }
447
+
448
+ .crumb-item:has(details.jump):hover,
449
+ .crumb-item:has(details.jump[open]) { background: var(--color-chrome); }
397
450
 
398
- .crumb-here { color: var(--color-text); font-weight: 600; }
451
+ /* Accent reports where you are. A crumb is navigation you can take, so it is
452
+ neutral, and \`.mast a\` keeps the accent for any other bar link. */
453
+ .mast a.crumb { color: var(--color-text-secondary); padding: 0.1rem 0.15rem 0.1rem 0; }
454
+ .mast a.crumb:hover { text-decoration: none; }
455
+ .crumb-item:hover a.crumb { color: var(--color-text); }
399
456
 
400
- /* The caret is now the whole control, so it carries the 24 pixel minimum on
401
- its own rather than inheriting a label's width. */
457
+ .crumb-here { color: var(--color-text); font-weight: 650; }
458
+
459
+ /* \`align-items: center\` centres boxes, and a label's line box carries
460
+ half-leading above the cap and descender room below, so its capitals sit
461
+ above its own centre by an amount that scales with the font size. Trimming
462
+ the label to its cap band fixes the box rather than nudging the mark. It
463
+ lands on a span holding the text alone, since the link's own \`inline-flex\`
464
+ ignores it. Firefox ships no \`text-box\` and keeps the caret a pixel low. */
465
+ .crumb-t, .crumb-here, .sb-ws .ws-name {
466
+ display: block;
467
+ text-box: trim-both cap alphabetic;
468
+ }
469
+
470
+ /* The caret is the whole control, so it carries the 24 pixel minimum on its
471
+ own rather than inheriting a label's width. It fills nothing itself, since
472
+ the chip around it does. */
402
473
  .crumb-item > .jump > summary {
403
474
  min-width: 1.5rem;
404
475
  min-height: 1.5rem;
405
476
  display: inline-flex;
406
477
  align-items: center;
407
478
  justify-content: center;
408
- padding: 0;
479
+ padding: 0.15rem 0.3rem 0.15rem 0;
409
480
  border: 0;
410
481
  background: transparent;
411
482
  }
412
483
 
413
- .crumb-item > .jump > summary:hover { background: var(--color-surface); border-radius: 5px; }
414
-
415
484
  /* A bar link is its own tap target rather than a line of text, so it carries
416
485
  the 24 pixel minimum the a11y rule sets. */
417
486
  .mast a,
@@ -427,23 +496,25 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
427
496
  color: var(--color-muted);
428
497
  }
429
498
 
430
- .mast-right { display: flex; align-items: center; gap: 0.85rem; }
499
+ .mast-right { display: flex; align-items: center; gap: 0.2rem; margin-left: auto; }
431
500
 
501
+ /* No border, at the fold control's size and radius, so every control on the
502
+ bar is drawn one way. */
432
503
  .theme {
433
- width: 1.9rem;
434
- height: 1.9rem;
504
+ width: 1.75rem;
505
+ height: 1.75rem;
435
506
  display: inline-flex;
436
507
  align-items: center;
437
508
  justify-content: center;
438
509
  padding: 0;
439
- border: 1px solid var(--color-border);
440
- border-radius: 99px;
510
+ border: 0;
511
+ border-radius: 6px;
441
512
  background: transparent;
442
513
  color: var(--color-muted);
443
514
  cursor: pointer;
444
515
  }
445
516
 
446
- .theme:hover { border-color: var(--color-accent); color: var(--color-accent); }
517
+ .theme:hover { color: var(--color-text); background: var(--color-chrome); }
447
518
  .theme svg { width: 0.95rem; height: 0.95rem; display: block; }
448
519
  .theme .moon { display: none; }
449
520
  :root[data-theme="dark"] .theme .sun { display: none; }
@@ -454,28 +525,225 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
454
525
  :root:not([data-theme="light"]) .theme .moon { display: block; }
455
526
  }
456
527
 
457
- /* ---- Track: where you are in the course ---- */
528
+ /* ---- The course sidebar ---- */
529
+
530
+ /* A segmented progress strip used to sit here, one \`flex: 1\` segment per
531
+ lesson, and it stopped working at the scale a reader most needs it: fifty
532
+ lessons rendered as a row of dots. The sidebar reports course position
533
+ instead, at any length, and the bar reports position inside the lesson. */
534
+
535
+ /* The toggle in the masthead, left of the breadcrumb. */
536
+ .sb-fold {
537
+ flex: none;
538
+ width: 1.75rem;
539
+ height: 1.75rem;
540
+ margin-right: 0.35rem;
541
+ font: inherit;
542
+ font-size: var(--t5);
543
+ line-height: 1;
544
+ color: var(--color-muted);
545
+ background: transparent;
546
+ border: 0;
547
+ border-radius: 6px;
548
+ cursor: pointer;
549
+ }
550
+
551
+ .sb-fold:hover { color: var(--color-text); background: var(--color-chrome); }
552
+
553
+ .sb {
554
+ flex: 0 0 var(--sb-w, 16rem);
555
+ width: var(--sb-w, 16rem);
556
+ position: sticky;
557
+ top: 0;
558
+ align-self: flex-start;
559
+ height: 100vh;
560
+ display: flex;
561
+ flex-direction: column;
562
+ overflow: visible;
563
+ background: transparent;
564
+ font-family: var(--teach-sans);
565
+ transition: flex-basis 0.14s, width 0.14s;
566
+ }
458
567
 
459
- /* The track rides inside the bar, so it needs no sticky position, no seam
460
- overlap, and no stacking context of its own. */
568
+ html.sb-shut .sb { flex-basis: 0; width: 0; overflow: hidden; }
461
569
 
462
- .track {
570
+ .sb-top {
571
+ flex: none;
463
572
  display: flex;
464
- gap: 0.25rem;
465
- max-width: var(--teach-chrome);
466
- margin: 0 auto;
467
- padding: 0 0 0.75rem;
573
+ align-items: center;
574
+ height: var(--teach-mast-h);
575
+ padding: 0 0.95rem;
576
+ border-bottom: 1px solid var(--color-border);
468
577
  }
469
578
 
470
- .track i {
471
- flex: 1;
472
- height: 3px;
579
+ .sb-ws { position: relative; }
580
+
581
+ .sb-ws > summary {
582
+ list-style: none;
583
+ cursor: pointer;
584
+ display: inline-flex;
585
+ align-items: center;
586
+ gap: 0.35rem;
587
+ padding: 0.25rem 0.3rem;
588
+ border-radius: 6px;
589
+ font-size: var(--t4);
590
+ font-weight: 650;
591
+ color: var(--color-text);
592
+ }
593
+
594
+ .sb-ws > summary::-webkit-details-marker { display: none; }
595
+ .sb-ws > summary:hover, .sb-ws[open] > summary { background: var(--color-chrome); color: var(--color-text); }
596
+ .sb-ws .car { color: var(--color-muted); display: inline-flex; align-items: center; }
597
+
598
+ .sb-wl {
599
+ position: absolute;
600
+ top: calc(100% + 0.35rem);
601
+ left: 0;
602
+ z-index: 40;
603
+ min-width: 13rem;
604
+ background: var(--color-background);
605
+ border: 1px solid var(--color-border);
606
+ border-radius: 9px;
607
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
608
+ padding: 0.3rem;
609
+ }
610
+
611
+ :root[data-theme="dark"] .sb-wl { box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); }
612
+
613
+ /* The popover chrome comes from \`.sb-wl\`, so the list nested inside it drops
614
+ its own rather than drawing a second border inside the first. */
615
+ .sb-wl .jump-list {
616
+ position: static;
617
+ min-width: 0;
618
+ max-width: none;
619
+ border: 0;
620
+ box-shadow: none;
621
+ padding: 0;
622
+ background: transparent;
623
+ }
624
+
625
+ .sb-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-bottom: 1rem; }
626
+
627
+ .sb-meta {
628
+ padding: 1rem 1.25rem 0.55rem;
629
+ color: var(--color-muted);
630
+ font-size: var(--t6);
631
+ font-variant-numeric: tabular-nums;
632
+ }
633
+
634
+ .sb-filter { padding: 0 0.95rem 0.6rem; }
635
+
636
+ .sb-filter input {
637
+ width: 100%;
638
+ box-sizing: border-box;
639
+ font: inherit;
640
+ font-size: var(--t6);
641
+ color: var(--color-text);
642
+ background: transparent;
643
+ border: 1px solid var(--color-border);
644
+ border-radius: 7px;
645
+ padding: 0.3rem 0.55rem;
646
+ }
647
+
648
+ .sb-filter input:focus { outline: none; border-color: var(--color-accent); }
649
+
650
+ .sb-list { list-style: none; margin: 0; padding: 0 0.7rem; }
651
+ .sb-list li { margin: 0; }
652
+ .sb-list li.hide { display: none; }
653
+
654
+ .sb-l {
655
+ position: relative;
656
+ display: flex;
657
+ gap: 0.55rem;
658
+ align-items: baseline;
659
+ padding: 0.38rem 0.55rem;
660
+ border-radius: 6px;
661
+ text-decoration: none;
662
+ color: var(--color-text-body);
663
+ font-size: var(--t5);
664
+ line-height: 1.45;
665
+ }
666
+
667
+ .sb-l:hover { background: var(--color-chrome); color: var(--color-text); }
668
+
669
+ .sb-n {
670
+ flex: none;
671
+ font-family: var(--teach-mono);
672
+ font-size: var(--t6);
673
+ color: var(--color-muted);
674
+ font-variant-numeric: tabular-nums;
675
+ }
676
+
677
+ .sb-on { color: var(--color-text); font-weight: 650; }
678
+
679
+ .sb-on::before {
680
+ content: "";
681
+ position: absolute;
682
+ left: -0.6rem;
683
+ top: 0.28rem;
684
+ bottom: 0.28rem;
685
+ width: 2px;
473
686
  border-radius: 2px;
687
+ background: var(--color-accent);
688
+ }
689
+
690
+ .sb-on .sb-n { color: var(--color-accent); }
691
+
692
+ .sb-empty { padding: 0.3rem 1.2rem; color: var(--color-muted); font-size: var(--t6); }
693
+
694
+ .sb-foot {
695
+ flex: none;
696
+ padding: 0.7rem 1.25rem;
697
+ border-top: 1px solid var(--color-border);
698
+ color: var(--color-muted);
699
+ font-size: var(--t6);
700
+ font-variant-numeric: tabular-nums;
701
+ }
702
+
703
+ /* The panel is resizable between a width that still fits a lesson title and one
704
+ that would start eating the reading measure. */
705
+ .sb-grip {
706
+ position: absolute;
707
+ top: 0;
708
+ bottom: 0;
709
+ right: -3px;
710
+ width: 7px;
711
+ z-index: 25;
712
+ padding: 0;
713
+ border: 0;
714
+ background: transparent;
715
+ cursor: col-resize;
716
+ touch-action: none;
717
+ }
718
+
719
+ .sb-grip::before {
720
+ content: "";
721
+ position: absolute;
722
+ left: 3px;
723
+ top: 50%;
724
+ transform: translateY(-50%);
725
+ width: 1px;
726
+ height: 2.2rem;
727
+ border-radius: 1px;
474
728
  background: var(--color-border);
729
+ opacity: 0;
730
+ transition: opacity 0.12s;
475
731
  }
476
732
 
477
- .track i.done { background: var(--color-accent); }
478
- .track i.here { background: var(--color-accent); box-shadow: 0 0 0 2px var(--color-teach-accent-bg); }
733
+ .sb-grip:hover::before, .sb-grip:focus-visible::before, html.sb-drag .sb-grip::before {
734
+ opacity: 1;
735
+ background: var(--color-accent);
736
+ }
737
+
738
+ .sb-grip:focus-visible { outline: none; }
739
+ html.sb-drag { cursor: col-resize; user-select: none; }
740
+ html.sb-shut .sb-grip { display: none; }
741
+
742
+ /* Both belong to the overlay and the script appends both on every page, so
743
+ each is hidden until the breakpoint hands it a job. Without this the close
744
+ control renders as an unstyled button stretched across the column, and the
745
+ scrim becomes a flex item of the body. */
746
+ .sb-close, .sb-scrim { display: none; }
479
747
 
480
748
  /* ---- Footer navigation, the only place it appears ---- */
481
749
 
@@ -516,48 +784,58 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
516
784
  }
517
785
 
518
786
  .jump summary::-webkit-details-marker { display: none; }
519
- .jump summary:hover { border-color: var(--color-accent); color: var(--color-accent); }
520
- /* The caret says "this is a menu" and nothing else. It used to flip to say
521
- "the menu is open", which the open panel underneath already says, so the
522
- motion carried no information a reader could not already see. */
523
- .jump summary .caret { opacity: 0.55; }
524
- .jump summary:hover .caret { opacity: 1; }
525
- .jump[open] summary { border-color: var(--color-accent); color: var(--color-accent); }
526
- .jump[open] summary .caret { opacity: 1; }
787
+ /* The caret says "this is a menu" and nothing else, so it is drawn at full
788
+ strength in every state and stays muted. A reveal on hover hid the only
789
+ route into the menu from a reader who cannot hover, and an affordance is not
790
+ a state, so it takes no accent on hover or while open. */
791
+ .jump summary .caret { color: var(--color-muted); }
527
792
 
528
793
  .jump-list {
529
794
  position: absolute;
530
795
  top: calc(100% + 0.4rem);
531
796
  left: 0;
532
797
  z-index: 20;
533
- min-width: 21rem;
534
798
  background: var(--color-background);
535
799
  border: 1px solid var(--color-border);
536
800
  border-radius: 9px;
537
801
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
538
- padding: 0.35rem;
802
+ padding: 0.25rem;
539
803
  list-style: none;
540
804
  margin: 0;
541
805
  }
542
806
 
543
807
  :root[data-theme="dark"] .jump-list { box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); }
544
808
 
809
+ /* One row recipe wherever the menu is mounted, on the steps the lesson list
810
+ beside it already reads: the label at \`--t5\` and the numeral at \`--t6\` mono.
811
+ The weight is restated because \`.mast a\` sets 600 for the bar's own links. */
545
812
  .jump-list a {
546
813
  display: grid;
547
814
  grid-template-columns: 1.65rem 1fr auto;
548
- gap: 0.7rem;
815
+ gap: 0.55rem;
549
816
  align-items: center;
550
- padding: 0.45rem 0.55rem;
817
+ padding: 0.38rem 0.55rem;
551
818
  border-radius: 6px;
552
819
  text-decoration: none;
553
820
  color: var(--color-text-body);
554
- font-size: 0.875rem;
821
+ font-size: var(--t5);
822
+ font-weight: 400;
823
+ line-height: 1.45;
824
+ }
825
+
826
+ .jump-list a:hover { background: var(--color-chrome); }
827
+ .jump-list .n { font-family: var(--teach-mono); font-size: var(--t6); color: var(--color-muted); }
828
+ /* The third column used to hold a status dot that read the same on every row,
829
+ so it reported nothing. It is a per-entry trailing slot now: the workspace
830
+ menu puts a lesson count there and the lesson menu leaves it empty. */
831
+ .jump-list .ct {
832
+ font-family: var(--teach-mono);
833
+ font-size: var(--t6);
834
+ color: var(--color-muted);
835
+ font-variant-numeric: tabular-nums;
836
+ text-align: right;
555
837
  }
556
838
 
557
- .jump-list a:hover { background: var(--color-surface); }
558
- .jump-list .n { font-family: var(--teach-mono); font-size: 0.75rem; color: var(--color-muted); }
559
- .jump-list .dot { width: 6px; height: 6px; border-radius: 99px; background: var(--color-border); }
560
- .jump-list .dot.done { background: var(--color-accent); }
561
839
  .jump-list li.at a { background: var(--color-teach-accent-bg); }
562
840
  .jump-list li.at .n { color: var(--color-accent); }
563
841
  .jump-list li.soon a { color: var(--color-muted); }
@@ -568,11 +846,11 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
568
846
  .jump-list li.all a { color: var(--color-accent); }
569
847
  .jump-list li.all .n { color: var(--color-accent); }
570
848
 
571
- /* The menu is \`min-width: 21rem\` and anchored to its summary, which puts its
572
- right edge 35 pixels past a 390 pixel viewport. Clamping the width and
573
- letting a long row wrap keeps it on screen without a breakpoint. */
849
+ /* The menu fits its content above an 11rem floor rather than padding three
850
+ short titles out to a wide panel. It is anchored to its summary, so the
851
+ clamp keeps a long row wrapping on screen without a breakpoint. */
574
852
  .jump-list {
575
- min-width: min(21rem, calc(100vw - 2rem));
853
+ min-width: min(11rem, calc(100vw - 2rem));
576
854
  max-width: calc(100vw - 2rem);
577
855
  }
578
856
 
@@ -593,8 +871,7 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
593
871
  @media (max-width: 640px) {
594
872
  .bar { padding: 0 1rem; }
595
873
  main { padding-left: 1rem; padding-right: 1rem; }
596
- .mast { font-size: 0.75rem; gap: 0.5rem; }
597
- .mast-left { gap: 0.5rem; row-gap: 0.35rem; }
874
+ .mast { font-size: 0.75rem; }
598
875
  }
599
876
 
600
877
  @media (max-width: 640px) {
@@ -606,6 +883,94 @@ main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); mar
606
883
  min-width: 0;
607
884
  max-width: none;
608
885
  }
886
+ }
887
+
888
+ /* ---- What gives way below 1100px ---- */
889
+
890
+ /* Hiding the sidebar outright leaves the toggle painted exactly where it cannot
891
+ act, and it deletes the filter and the per-lesson outline, which the
892
+ breadcrumb caret does not carry. The panel becomes an overlay instead, and it
893
+ states its own ground because \`.sb\` is transparent by design for a column
894
+ sitting on the page rather than floating over the lesson. */
895
+ @media (max-width: ${TEACH_SIDEBAR_BREAKPOINT}px) {
896
+ .pane { border-left: 0; }
897
+
898
+ .sb {
899
+ position: fixed;
900
+ top: 0;
901
+ left: 0;
902
+ bottom: 0;
903
+ z-index: 60;
904
+ width: min(21rem, 86vw);
905
+ height: 100dvh;
906
+ overflow-y: auto;
907
+ background: var(--color-background);
908
+ border-right: 1px solid var(--color-border);
909
+ box-shadow: 0 0 40px rgb(0 0 0 / 35%);
910
+ transform: translateX(-100%);
911
+ transition: transform 0.18s;
912
+ }
913
+
914
+ html:not(.sb-shut) .sb { transform: none; }
915
+
916
+ /* The wide rule collapses a shut panel to \`width: 0\` and outranks a bare
917
+ \`.sb\` in here, which produced a one-pixel panel whose contents spilled out
918
+ and intercepted every click on the toggle. The shut state restates the
919
+ width so the transform is what moves the panel off screen. */
920
+ html.sb-shut .sb { width: min(21rem, 86vw); flex-basis: auto; overflow-y: auto; }
921
+
922
+ /* A transform moves the panel out of sight and leaves every control inside it
923
+ in the tab order, so the first Tab from a shut page landed on the workspace
924
+ switcher inside an invisible panel. Visibility takes it out of the
925
+ sequence, delayed on the way out so the slide still shows. */
926
+ html.sb-shut .sb { visibility: hidden; transition: transform 0.18s, visibility 0s linear 0.18s; }
927
+ html:not(.sb-shut) .sb { visibility: visible; transition: transform 0.18s, visibility 0s; }
928
+
929
+ .sb-grip { display: none; }
930
+
931
+ /* The panel covers the masthead and the toggle that opened it, so it carries
932
+ its own way out rather than leaving the scrim as the only route on a phone.
933
+ It carries no border, at 1.75rem to match \`.sb-fold\` and \`.theme\`, which also clears
934
+ the 24 pixel pointer target floor. */
935
+ .sb-close {
936
+ display: inline-flex;
937
+ align-items: center;
938
+ justify-content: center;
939
+ position: absolute;
940
+ top: 0.55rem;
941
+ right: 0.6rem;
942
+ z-index: 61;
943
+ appearance: none;
944
+ width: 1.75rem;
945
+ height: 1.75rem;
946
+ font: inherit;
947
+ font-size: var(--t5);
948
+ line-height: 1;
949
+ color: var(--color-muted);
950
+ background: transparent;
951
+ border: 0;
952
+ border-radius: 6px;
953
+ cursor: pointer;
954
+ }
955
+
956
+ .sb-close:hover { color: var(--color-text); background: var(--color-chrome); }
957
+
958
+ .sb-scrim {
959
+ display: block;
960
+ position: fixed;
961
+ inset: 0;
962
+ z-index: 55;
963
+ background: rgb(0 0 0 / 45%);
964
+ opacity: 0;
965
+ pointer-events: none;
966
+ transition: opacity 0.18s;
967
+ }
968
+
969
+ html:not(.sb-shut) .sb-scrim { opacity: 1; pointer-events: auto; }
970
+ }
971
+
972
+ @media (prefers-reduced-motion: reduce) {
973
+ .sb, .sb-scrim, .sb-grip::before { transition: none; }
609
974
  }`,
610
975
  }
611
976
 
@@ -1148,83 +1513,30 @@ ol.succ li { margin-bottom: 0.4rem; }
1148
1513
  const TEACH_OUTLINE: Component = {
1149
1514
  name: 'teach-outline',
1150
1515
  note: [
1151
- 'The in-lesson outline rail in the gutter the reading measure leaves over, and',
1152
- 'the workspace-switcher row inside the lesson jump menu.',
1516
+ 'The in-lesson outline, folded under the lesson being read in the course',
1517
+ 'sidebar. It used to be a fixed rail in the gutter the reading measure left',
1518
+ 'over, which reported the same thing twice once the sidebar carried the',
1519
+ 'course and disappeared entirely below 1420px.',
1153
1520
  ].join('\n '),
1154
- reads: [
1155
- '--color-border',
1156
- '--color-text-body',
1157
- '--color-muted',
1158
- '--color-accent',
1159
- '--teach-sans',
1160
- '--teach-measure',
1161
- '--teach-mast-h',
1162
- '--t5',
1163
- ],
1164
- rules: `/* ---- In-lesson outline, in the gutter the measure leaves over ---- */
1165
-
1166
- .outline {
1167
- position: fixed;
1168
- top: calc(var(--teach-mast-h) + 2.2rem);
1169
- /* Four pixels of padding with the position pulled back by the same amount,
1170
- so a focus ring at the rail's edge has somewhere to draw. \`overflow-y\`
1171
- clips horizontally too, which took a bite out of every ring in here. */
1172
- left: calc(50% + var(--teach-measure) / 2 + 2.5rem - 4px);
1173
- padding: 0 4px;
1174
- width: calc(13rem + 8px);
1175
- max-height: calc(100vh - var(--teach-mast-h) - 4rem);
1176
- overflow-y: auto;
1177
- font-size: var(--t5);
1178
- line-height: 1.45;
1179
- }
1180
-
1181
- /* The heading is a control rather than a label, because returning to the top
1182
- of a long lesson is the one thing the rail could not do. */
1183
- .outline .to-top {
1184
- display: flex;
1185
- align-items: center;
1186
- gap: 0.4rem;
1187
- width: 100%;
1188
- font-family: var(--teach-sans);
1189
- font-size: 0.6563rem;
1190
- font-weight: 700;
1191
- letter-spacing: 0.09em;
1192
- text-transform: uppercase;
1193
- color: var(--color-muted);
1194
- background: none;
1195
- border: 0;
1196
- border-bottom: 1px solid var(--color-border);
1197
- border-radius: 3px 3px 0 0;
1198
- margin: 0 0 0.6rem;
1199
- padding: 0.2rem 0 0.55rem;
1200
- cursor: pointer;
1201
- text-align: left;
1202
- }
1521
+ reads: ['--color-text', '--color-text-secondary', '--color-accent', '--t6'],
1522
+ rules: `/* ---- In-lesson outline, folded under the lesson being read ---- */
1203
1523
 
1204
- .outline .to-top:hover { color: var(--color-accent); border-bottom-color: var(--color-border); }
1524
+ /* The rail this replaces was \`position: fixed\` in the right-hand gutter, which
1525
+ only existed above 1420px and reported nothing at all below it. Folding the
1526
+ headings under the current lesson puts the course and the lesson in one
1527
+ column, so a reader tracks both in one place at any window width. */
1528
+ .sb-out { list-style: none; margin: 0.1rem 0 0.55rem; padding: 0 0 0 1.45rem; }
1205
1529
 
1206
- .outline .to-top::before {
1207
- content: "";
1208
- width: 11px;
1209
- height: 8px;
1210
- background:
1211
- linear-gradient(var(--color-muted) 0 0) 0 0 / 11px 1.5px no-repeat,
1212
- linear-gradient(var(--color-muted) 0 0) 0 3.2px / 8px 1.5px no-repeat,
1213
- linear-gradient(var(--color-muted) 0 0) 0 6.5px / 11px 1.5px no-repeat;
1214
- }
1215
-
1216
- .outline a {
1530
+ .sb-out a {
1217
1531
  display: block;
1218
- padding: 0.3rem 0 0.3rem 0.7rem;
1219
- border-left: 2px solid var(--color-border);
1220
- color: var(--color-muted);
1532
+ padding: 0.18rem 0.5rem;
1221
1533
  text-decoration: none;
1534
+ color: var(--color-text-secondary);
1535
+ font-size: var(--t6);
1222
1536
  }
1223
1537
 
1224
- .outline a:hover { color: var(--color-text-body); border-left-color: var(--color-border); }
1225
- .outline a.on { color: var(--color-accent); border-left-color: var(--color-accent); }
1226
-
1227
- @media (max-width: 1420px) { .outline { display: none; } }`,
1538
+ .sb-out a:hover { color: var(--color-text); }
1539
+ .sb-out a.on { color: var(--color-accent); }`,
1228
1540
  }
1229
1541
 
1230
1542
  const TEACH_REFERENCES: Component = {