@erclx/canon 4.66.0 → 4.67.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.
@@ -6,10 +6,14 @@
6
6
  * where the tokens were already right and the surface still read as unrelated
7
7
  * to itself, so a token file alone would not have caught either.
8
8
  *
9
- * It has two members and it is provisional at two. Two repairs is thin evidence
10
- * for an abstraction, and the honest position is a layer that says it has two
11
- * rather than one padded out with speculative members to look settled. A third
12
- * real instance is what decides whether the shape below holds.
9
+ * It held at a third real instance: teach's recovered chrome. That instance
10
+ * did not join `COMPONENTS` itself, since teach is the only consumer and
11
+ * folding a masthead, a quiz, and a glossary into the generic default would
12
+ * ship them into every project that installs the `design` domain and never
13
+ * asked for teach. It reads instead as `TEACH_COMPONENTS`, a sibling array a
14
+ * caller opts into by passing an explicit component list to
15
+ * `buildDesignCss`'s `components` option, which is what widening that option
16
+ * from a boolean to `boolean | readonly Component[]` bought.
13
17
  *
14
18
  * It does not live in `.claude/DESIGN.md`. `standards/design.md` keeps CSS class
15
19
  * names out of that record and says they live in code, which is here.
@@ -116,3 +120,1152 @@ const SCROLLBAR: Component = {
116
120
  }
117
121
 
118
122
  export const COMPONENTS: readonly Component[] = [STATUS, SCROLLBAR]
123
+
124
+ const TEACH_CHROME: Component = {
125
+ name: 'teach-chrome',
126
+ note: [
127
+ 'The masthead, breadcrumb, jump menu, theme toggle, progress track, and footer',
128
+ 'navigation shared by every teach page. Recovered from two gitignored',
129
+ 'course.css files that predate a regression that dropped this layer from the',
130
+ "generator, and rewritten onto this module's tokens rather than the",
131
+ 'incompatible palette they carried. See .claude/wireframes/teach/chrome.md for',
132
+ 'the shape.',
133
+ ].join('\n '),
134
+ reads: [
135
+ '--color-background',
136
+ '--color-surface',
137
+ '--color-border',
138
+ '--color-text',
139
+ '--color-text-body',
140
+ '--color-text-secondary',
141
+ '--color-muted',
142
+ '--color-accent',
143
+ '--teach-sans',
144
+ '--teach-mono',
145
+ '--teach-measure',
146
+ '--teach-chrome',
147
+ '--teach-shadow',
148
+ '--color-teach-accent-bg',
149
+ ],
150
+ rules: `:root {
151
+ --teach-sans: 'Nunito', ui-rounded, 'Segoe UI', system-ui, sans-serif;
152
+ --teach-hand: 'Virgil', 'Nunito', cursive;
153
+ --teach-mono: 'Cascadia Code', ui-monospace, monospace;
154
+ --teach-measure: 52rem;
155
+ --teach-chrome: 52rem;
156
+ --teach-wide: 64rem;
157
+ --teach-mast-h: 4.4rem;
158
+ --teach-shadow: 0 1px 2px rgba(20, 20, 20, 0.04);
159
+ --color-teach-accent-bg: color-mix(in srgb, var(--color-accent) 14%, var(--color-background));
160
+
161
+ /* Aliases for the retired teach palette. Every already-written lesson draws
162
+ its hand-authored diagrams with fill and stroke values pinned to these
163
+ names directly in the SVG markup, which this rewrite cannot reach without
164
+ editing lesson content the same pass that recovered the chrome was asked
165
+ not to touch. */
166
+ --panel: var(--color-surface);
167
+ --rule: var(--color-border);
168
+ --ink: var(--color-text-body);
169
+ --ink-soft: var(--color-text-secondary);
170
+ --ink-faint: var(--color-muted);
171
+ --accent: var(--color-accent);
172
+ --accent-bg: var(--color-teach-accent-bg);
173
+ }
174
+
175
+ @media (prefers-color-scheme: dark) {
176
+ :root:not([data-theme='light']) {
177
+ --teach-shadow: none;
178
+ }
179
+ }
180
+
181
+ :root[data-theme='dark'] {
182
+ --teach-shadow: none;
183
+ }
184
+
185
+ /* No horizontal padding on the body, so the sticky bar reaches both window
186
+ edges. The gutter moves onto \`main\`, which is the only thing that needed it. */
187
+ body {
188
+ margin: 0;
189
+ padding: 0 0 7rem;
190
+ background: var(--color-background);
191
+ color: var(--color-text-body);
192
+ font-family: var(--teach-sans);
193
+ font-size: 1.125rem;
194
+ line-height: 1.62;
195
+ -webkit-font-smoothing: antialiased;
196
+ }
197
+
198
+ /* The bar row and the text column resolve to the same 52rem content box, so a
199
+ listing page and a lesson start at the same left edge as the chrome above
200
+ them. A narrower \`.wide-body\` was what put the navigation and the content on
201
+ two different measures. */
202
+ main { max-width: calc(var(--teach-measure) + 3rem); margin: 0 auto; padding: 2.25rem 1.5rem 0; }
203
+ .wide-body { max-width: calc(var(--teach-measure) + 3rem); }
204
+
205
+ .bar {
206
+ position: sticky;
207
+ top: 0;
208
+ z-index: 30;
209
+ background: var(--color-background);
210
+ border-bottom: 1px solid var(--color-border);
211
+ padding: 0 1.5rem;
212
+ }
213
+
214
+ .mast {
215
+ max-width: var(--teach-chrome);
216
+ margin: 0 auto;
217
+ padding: 0.8rem 0 0.7rem;
218
+ display: flex;
219
+ justify-content: space-between;
220
+ align-items: center;
221
+ gap: 1rem;
222
+ font-size: 0.8125rem;
223
+ color: var(--color-muted);
224
+ }
225
+
226
+ .mast a { color: var(--color-accent); text-decoration: none; font-weight: 600; }
227
+ .mast a:hover { text-decoration: underline; }
228
+
229
+ .theme {
230
+ font-family: var(--teach-sans);
231
+ font-size: 0.75rem;
232
+ font-weight: 600;
233
+ color: var(--color-muted);
234
+ background: transparent;
235
+ border: 1px solid var(--color-border);
236
+ border-radius: 99px;
237
+ padding: 0.25rem 0.7rem;
238
+ cursor: pointer;
239
+ }
240
+
241
+ .theme:hover { border-color: var(--color-accent); color: var(--color-accent); }
242
+
243
+ .nav {
244
+ width: 100%;
245
+ margin: 0;
246
+ display: flex;
247
+ gap: 0.6rem;
248
+ }
249
+
250
+ .nav > a, .nav > span {
251
+ flex: 1;
252
+ padding: 0.65rem 0.95rem;
253
+ border: 1px solid var(--color-border);
254
+ border-radius: 8px;
255
+ text-decoration: none;
256
+ color: var(--color-text-body);
257
+ font-size: 0.9375rem;
258
+ font-weight: 600;
259
+ line-height: 1.3;
260
+ box-shadow: var(--teach-shadow);
261
+ transition: border-color 0.12s, background 0.12s;
262
+ }
263
+
264
+ .nav > a:hover { border-color: var(--color-accent); background: var(--color-teach-accent-bg); }
265
+ .nav > .end { color: var(--color-muted); border-style: dashed; font-weight: 400; box-shadow: none; }
266
+
267
+ .nav .lbl {
268
+ display: block;
269
+ font-size: 0.6563rem;
270
+ font-weight: 700;
271
+ letter-spacing: 0.09em;
272
+ text-transform: uppercase;
273
+ color: var(--color-muted);
274
+ margin-bottom: 0.15rem;
275
+ }
276
+
277
+ .nav > a:hover .lbl { color: var(--color-accent); }
278
+ .nav .to-next { text-align: right; }
279
+ .nav .to-index { flex: 0 0 auto; text-align: center; }
280
+ .nav-foot { margin: 4.5rem 0 0; }
281
+
282
+ /* ---- Masthead contents ---- */
283
+
284
+ /* Sticky position and stacking both live on \`.bar\` now, so nothing here
285
+ restates either. */
286
+ /* A breadcrumb rather than a row of links. The gap tightens because the
287
+ separators now carry the spacing the gap used to. */
288
+ .mast-left { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
289
+
290
+ /* \`--rule\` measured 1.27:1 here. It is a border token, and a border is a
291
+ shape a reader infers rather than a glyph they resolve, so it is the wrong
292
+ value for a character even a decorative one. */
293
+ .crumb-sep {
294
+ color: var(--color-muted);
295
+ font-size: 0.9375rem;
296
+ user-select: none;
297
+ }
298
+
299
+ /* One arrow, on the first segment, since that is the only one that goes up. */
300
+ .crumb-back {
301
+ margin-right: 0.3rem;
302
+ font-size: 0.9375rem;
303
+ line-height: 1;
304
+ }
305
+
306
+ .crumb { display: inline-flex; align-items: center; }
307
+
308
+ /* The label and its caret are one segment visually and two controls in fact:
309
+ the label navigates, the caret opens the menu. */
310
+ .crumb-item { display: inline-flex; align-items: center; gap: 0.1rem; }
311
+
312
+ .crumb-here { color: var(--color-text); font-weight: 600; }
313
+
314
+ /* The caret is now the whole control, so it carries the 24 pixel minimum on
315
+ its own rather than inheriting a label's width. */
316
+ .crumb-item > .jump > summary {
317
+ min-width: 1.5rem;
318
+ min-height: 1.5rem;
319
+ display: inline-flex;
320
+ align-items: center;
321
+ justify-content: center;
322
+ padding: 0;
323
+ border: 0;
324
+ background: transparent;
325
+ }
326
+
327
+ .crumb-item > .jump > summary:hover { background: var(--color-surface); border-radius: 5px; }
328
+
329
+ /* A bar link is its own tap target rather than a line of text, so it carries
330
+ the 24 pixel minimum the a11y rule sets. */
331
+ .mast a,
332
+ .mast summary {
333
+ min-height: 1.6rem;
334
+ display: inline-flex;
335
+ align-items: center;
336
+ }
337
+
338
+ .mast .pos {
339
+ font-family: var(--teach-mono);
340
+ font-size: 0.75rem;
341
+ color: var(--color-muted);
342
+ }
343
+
344
+ .mast-right { display: flex; align-items: center; gap: 0.85rem; }
345
+
346
+ .theme {
347
+ width: 1.9rem;
348
+ height: 1.9rem;
349
+ display: inline-flex;
350
+ align-items: center;
351
+ justify-content: center;
352
+ padding: 0;
353
+ border: 1px solid var(--color-border);
354
+ border-radius: 99px;
355
+ background: transparent;
356
+ color: var(--color-muted);
357
+ cursor: pointer;
358
+ }
359
+
360
+ .theme:hover { border-color: var(--color-accent); color: var(--color-accent); }
361
+ .theme svg { width: 0.95rem; height: 0.95rem; display: block; }
362
+ .theme .moon { display: none; }
363
+ :root[data-theme="dark"] .theme .sun { display: none; }
364
+ :root[data-theme="dark"] .theme .moon { display: block; }
365
+
366
+ @media (prefers-color-scheme: dark) {
367
+ :root:not([data-theme="light"]) .theme .sun { display: none; }
368
+ :root:not([data-theme="light"]) .theme .moon { display: block; }
369
+ }
370
+
371
+ /* ---- Track: where you are in the course ---- */
372
+
373
+ /* The track rides inside the bar, so it needs no sticky position, no seam
374
+ overlap, and no stacking context of its own. */
375
+
376
+ .track {
377
+ display: flex;
378
+ gap: 0.25rem;
379
+ max-width: var(--teach-chrome);
380
+ margin: 0 auto;
381
+ padding: 0 0 0.75rem;
382
+ }
383
+
384
+ .track i {
385
+ flex: 1;
386
+ height: 3px;
387
+ border-radius: 2px;
388
+ background: var(--color-border);
389
+ }
390
+
391
+ .track i.done { background: var(--color-accent); }
392
+ .track i.here { background: var(--color-accent); box-shadow: 0 0 0 2px var(--color-teach-accent-bg); }
393
+
394
+ /* ---- Footer navigation, the only place it appears ---- */
395
+
396
+ .nav { margin: 4rem 0 0; align-items: stretch; }
397
+
398
+ .nav > a, .nav > span { display: flex; flex-direction: column; justify-content: center; }
399
+
400
+ .nav .arrow {
401
+ font-family: var(--teach-mono);
402
+ color: var(--color-muted);
403
+ padding-right: 0.35rem;
404
+ }
405
+
406
+ .nav > a:hover .arrow { color: var(--color-accent); }
407
+
408
+ /* ---- Jump menu: random access, where the foot nav is sequential ---- */
409
+
410
+ .jump { position: relative; }
411
+
412
+ .jump summary {
413
+ list-style: none;
414
+ cursor: pointer;
415
+ font-family: var(--teach-mono);
416
+ font-size: 0.75rem;
417
+ color: var(--color-muted);
418
+ padding: 0.25rem 0.55rem;
419
+ border: 1px solid transparent;
420
+ border-radius: 6px;
421
+ display: inline-flex;
422
+ align-items: center;
423
+ gap: 0.4rem;
424
+ }
425
+
426
+ .jump summary::-webkit-details-marker { display: none; }
427
+ .jump summary:hover { border-color: var(--color-accent); color: var(--color-accent); }
428
+ /* The caret says "this is a menu" and nothing else. It used to flip to say
429
+ "the menu is open", which the open panel underneath already says, so the
430
+ motion carried no information a reader could not already see. */
431
+ .jump summary .caret { opacity: 0.55; }
432
+ .jump summary:hover .caret { opacity: 1; }
433
+ .jump[open] summary { border-color: var(--color-accent); color: var(--color-accent); }
434
+ .jump[open] summary .caret { opacity: 1; }
435
+
436
+ .jump-list {
437
+ position: absolute;
438
+ top: calc(100% + 0.4rem);
439
+ left: 0;
440
+ z-index: 20;
441
+ min-width: 21rem;
442
+ background: var(--color-background);
443
+ border: 1px solid var(--color-border);
444
+ border-radius: 9px;
445
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
446
+ padding: 0.35rem;
447
+ list-style: none;
448
+ margin: 0;
449
+ }
450
+
451
+ :root[data-theme="dark"] .jump-list { box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); }
452
+
453
+ .jump-list a {
454
+ display: grid;
455
+ grid-template-columns: 1.65rem 1fr auto;
456
+ gap: 0.7rem;
457
+ align-items: center;
458
+ padding: 0.45rem 0.55rem;
459
+ border-radius: 6px;
460
+ text-decoration: none;
461
+ color: var(--color-text-body);
462
+ font-size: 0.875rem;
463
+ }
464
+
465
+ .jump-list a:hover { background: var(--color-surface); }
466
+ .jump-list .n { font-family: var(--teach-mono); font-size: 0.75rem; color: var(--color-muted); }
467
+ .jump-list .dot { width: 6px; height: 6px; border-radius: 99px; background: var(--color-border); }
468
+ .jump-list .dot.done { background: var(--color-accent); }
469
+ .jump-list li.at a { background: var(--color-teach-accent-bg); }
470
+ .jump-list li.at .n { color: var(--color-accent); }
471
+ .jump-list li.soon a { color: var(--color-muted); }
472
+
473
+ .jump.ws summary { color: var(--color-text-secondary); }
474
+
475
+ .jump-list li.all { border-top: 1px solid var(--color-border); margin-top: 0.25rem; padding-top: 0.25rem; }
476
+ .jump-list li.all a { color: var(--color-accent); }
477
+ .jump-list li.all .n { color: var(--color-accent); }
478
+
479
+ /* The menu is \`min-width: 21rem\` and anchored to its summary, which puts its
480
+ right edge 35 pixels past a 390 pixel viewport. Clamping the width and
481
+ letting a long row wrap keeps it on screen without a breakpoint. */
482
+ .jump-list {
483
+ min-width: min(21rem, calc(100vw - 2rem));
484
+ max-width: calc(100vw - 2rem);
485
+ }
486
+
487
+ /* The route back to the full listing sits above the workspaces it lists. */
488
+ .jump-list li.head a { color: var(--color-text-secondary); font-weight: 600; }
489
+ .jump-list li.head { border-bottom: 1px solid var(--color-border); margin-bottom: 0.3rem; padding-bottom: 0.3rem; }
490
+
491
+ /* An unwritten lesson and a stub workspace are inert, so they render inert.
492
+ Reduced opacity alone reads as a rendering fault, so the cursor says it too. */
493
+ .jump-list li.soon a,
494
+ .toc li.soon a,
495
+ [aria-disabled="true"] {
496
+ opacity: 0.55;
497
+ cursor: not-allowed;
498
+ pointer-events: none;
499
+ }
500
+
501
+ @media (max-width: 640px) {
502
+ .bar { padding: 0 1rem; }
503
+ main { padding-left: 1rem; padding-right: 1rem; }
504
+ .mast { font-size: 0.75rem; gap: 0.5rem; }
505
+ .mast-left { gap: 0.5rem; row-gap: 0.35rem; }
506
+ }
507
+
508
+ @media (max-width: 640px) {
509
+ .jump-list {
510
+ position: fixed;
511
+ left: 1rem;
512
+ right: 1rem;
513
+ top: auto;
514
+ min-width: 0;
515
+ max-width: none;
516
+ }
517
+ }`,
518
+ }
519
+
520
+ const TEACH_ARTICLE: Component = {
521
+ name: 'teach-article',
522
+ note: [
523
+ 'The base reading-column styles a teach page renders prose in: type, panels,',
524
+ 'code, callouts, tables, the workspace contents listing, and the focus ring.',
525
+ 'Recovered alongside teach-chrome from the same two files.',
526
+ ].join('\n '),
527
+ reads: [
528
+ '--color-border',
529
+ '--color-surface',
530
+ '--color-text',
531
+ '--color-text-body',
532
+ '--color-text-secondary',
533
+ '--color-muted',
534
+ '--color-accent',
535
+ '--teach-hand',
536
+ '--teach-mono',
537
+ ],
538
+ rules: `/* ---- Type ---- */
539
+
540
+ h1 {
541
+ font-size: 2.15rem;
542
+ line-height: 1.14;
543
+ font-weight: 800;
544
+ letter-spacing: -0.022em;
545
+ margin: 0 0 0.55rem;
546
+ text-wrap: balance;
547
+ }
548
+
549
+ h2 {
550
+ font-size: 1.3rem;
551
+ font-weight: 700;
552
+ letter-spacing: -0.012em;
553
+ margin: 3rem 0 0.85rem;
554
+ }
555
+
556
+ h3 { font-size: 1.03rem; font-weight: 700; margin: 1.9rem 0 0.4rem; }
557
+
558
+ p { margin: 0 0 1.15rem; }
559
+
560
+ .lede {
561
+ font-size: 1.1875rem;
562
+ line-height: 1.5;
563
+ color: var(--color-text-secondary);
564
+ margin-bottom: 2rem;
565
+ }
566
+
567
+ a { color: var(--color-accent); }
568
+
569
+ em { font-style: italic; }
570
+
571
+ /* ---- Panels ---- */
572
+
573
+ .assumes, .progress {
574
+ font-size: 0.8438rem;
575
+ line-height: 1.7;
576
+ color: var(--color-text-secondary);
577
+ background: var(--color-surface);
578
+ border-radius: 7px;
579
+ padding: 0.9rem 1.15rem;
580
+ margin: 0 0 2.5rem;
581
+ }
582
+
583
+ .assumes b, .progress b {
584
+ display: block;
585
+ color: var(--color-muted);
586
+ font-weight: 700;
587
+ letter-spacing: 0.06em;
588
+ text-transform: uppercase;
589
+ font-size: 0.6875rem;
590
+ margin-bottom: 0.2rem;
591
+ }
592
+
593
+ /* ---- Code ---- */
594
+
595
+ code {
596
+ font-family: var(--teach-mono);
597
+ font-size: 0.85em;
598
+ background: var(--color-surface);
599
+ padding: 0.13em 0.38em;
600
+ border-radius: 4px;
601
+ }
602
+
603
+ pre {
604
+ font-family: var(--teach-mono);
605
+ font-size: 0.8125rem;
606
+ line-height: 1.7;
607
+ background: var(--color-surface);
608
+ padding: 1.15rem 1.3rem;
609
+ border-radius: 7px;
610
+ overflow-x: auto;
611
+ margin: 0 0 1.5rem;
612
+ }
613
+
614
+ pre code { background: none; padding: 0; font-size: 1em; }
615
+
616
+ /* ---- Callout ---- */
617
+
618
+ .hard {
619
+ background: var(--color-teach-accent-bg);
620
+ border-radius: 7px;
621
+ padding: 1.1rem 1.3rem;
622
+ margin: 2rem 0;
623
+ }
624
+
625
+ .hard-label {
626
+ font-family: var(--teach-hand);
627
+ font-size: 1.0625rem;
628
+ color: var(--color-accent);
629
+ display: block;
630
+ margin-bottom: 0.35rem;
631
+ }
632
+
633
+ .hard p:last-child { margin-bottom: 0; }
634
+
635
+ /* ---- Tables ---- */
636
+
637
+ table { width: 100%; border-collapse: collapse; font-size: 0.875rem; margin: 0 0 1.5rem; }
638
+ th, td { text-align: right; padding: 0.55rem 0.7rem; border-bottom: 1px solid var(--color-border); }
639
+ th:first-child, td:first-child { text-align: left; }
640
+
641
+ th {
642
+ font-weight: 700;
643
+ font-size: 0.75rem;
644
+ letter-spacing: 0.04em;
645
+ text-transform: uppercase;
646
+ color: var(--color-muted);
647
+ border-bottom-color: var(--color-border);
648
+ }
649
+
650
+ tbody tr.mark td { background: var(--color-teach-accent-bg); }
651
+ .scroll { overflow-x: auto; }
652
+
653
+ /* ---- Contents ---- */
654
+
655
+ .toc { list-style: none; padding: 0; margin: 0 0 2.5rem; }
656
+ .toc li + li { border-top: 1px solid var(--color-border); }
657
+
658
+ .toc a {
659
+ display: grid;
660
+ grid-template-columns: 2.25rem 1fr auto;
661
+ gap: 0 1.1rem;
662
+ align-items: baseline;
663
+ padding: 1rem 0.7rem;
664
+ text-decoration: none;
665
+ color: var(--color-text-body);
666
+ border-radius: 7px;
667
+ }
668
+
669
+ .toc a:hover { background: var(--color-surface); }
670
+ .toc .num { font-family: var(--teach-mono); font-size: 0.8125rem; color: var(--color-accent); }
671
+ .toc b { font-size: 1.0625rem; font-weight: 700; letter-spacing: -0.012em; }
672
+
673
+ .toc .blurb {
674
+ grid-column: 2;
675
+ font-size: 0.9375rem;
676
+ line-height: 1.5;
677
+ color: var(--color-text-secondary);
678
+ margin-top: 0.1rem;
679
+ }
680
+
681
+ /* A dot and a word, not a pill. The jump menus already say written and
682
+ planned with a filled or hollow dot, so the bordered uppercase badge was a
683
+ second vocabulary for a fact the design system already had one for. Sentence
684
+ case and no tracking, per the UI copy rule. */
685
+ .toc .state {
686
+ display: inline-flex;
687
+ align-items: center;
688
+ gap: 0.42rem;
689
+ font-size: 0.75rem;
690
+ font-weight: 500;
691
+ letter-spacing: 0;
692
+ text-transform: none;
693
+ color: var(--color-muted);
694
+ white-space: nowrap;
695
+ }
696
+
697
+ .toc .state::before {
698
+ content: "";
699
+ width: 6px;
700
+ height: 6px;
701
+ border-radius: 99px;
702
+ background: var(--color-border);
703
+ flex: none;
704
+ }
705
+
706
+ .toc .state.done { color: var(--color-text-secondary); }
707
+ .toc .state.done::before { background: var(--color-accent); }
708
+ .toc .state.next { color: var(--color-text-secondary); }
709
+ .toc .state.next::before { background: var(--color-accent); }
710
+
711
+ /* A link leaving the page says so with a mark beside it rather than with a
712
+ badge spelling out the sentence. */
713
+ .toc .ext {
714
+ font-size: 0.875rem;
715
+ color: var(--color-muted);
716
+ margin-left: 0.35rem;
717
+ }
718
+
719
+ .toc a:hover .ext { color: var(--color-accent); }
720
+
721
+ ol.succ {
722
+ margin: 0 0 2.5rem;
723
+ padding-left: 1.35rem;
724
+ font-size: 0.9375rem;
725
+ line-height: 1.6;
726
+ color: var(--color-text-secondary);
727
+ }
728
+
729
+ ol.succ li { margin-bottom: 0.4rem; }
730
+ .toc li.soon b { color: var(--color-muted); font-weight: 400; }
731
+
732
+ /* ---- Focus is one token, and it is the accent ---- */
733
+
734
+ :where(a, button, summary, input, .opt):focus-visible {
735
+ outline: 2px solid var(--color-accent);
736
+ outline-offset: 2px;
737
+ border-radius: 5px;
738
+ }
739
+
740
+ /* A summary carries its own border, so the ring sits outside it rather than
741
+ doubling the box. */
742
+ .jump summary:focus-visible { outline-offset: 1px; }
743
+
744
+ /* The browser default ring is white on a dark ground, which reads as a second
745
+ accent. Suppressing it only where the token above replaces it. */
746
+ :where(a, button, summary, input, .opt):focus:not(:focus-visible) { outline: none; }
747
+
748
+ @media (max-width: 640px) {
749
+ body { font-size: 1.0625rem; }
750
+ }
751
+
752
+ /* The week plan. Its markup shipped with no rule matching any of its five
753
+ classes, so it rendered as a run of inline spans with the ordinal, the
754
+ title, the description, and the duration all touching. */
755
+
756
+ .road { margin: 1.75rem 0 2.5rem; }
757
+
758
+ .road-row {
759
+ display: grid;
760
+ grid-template-columns: 1.75rem 1fr auto;
761
+ gap: 0.9rem;
762
+ align-items: baseline;
763
+ padding: 0.85rem 0;
764
+ border-bottom: 1px solid var(--color-border);
765
+ }
766
+
767
+ .road-row:last-child { border-bottom: 0; }
768
+
769
+ .road-n {
770
+ font-family: var(--teach-mono);
771
+ font-size: 0.8125rem;
772
+ color: var(--color-accent);
773
+ }
774
+
775
+ .road-b { display: block; }
776
+ .road-b b { display: block; margin-bottom: 0.2rem; }
777
+ .road-b span { color: var(--color-text-secondary); font-size: 0.9375rem; line-height: 1.55; }
778
+
779
+ .road-t {
780
+ font-family: var(--teach-mono);
781
+ font-size: 0.75rem;
782
+ color: var(--color-muted);
783
+ white-space: nowrap;
784
+ }
785
+
786
+ /* The closing line is a paragraph rather than a row, so it keeps the column
787
+ the rows set rather than sitting flush against the measure. */
788
+ .road + p > b:first-child { color: var(--color-text); }
789
+
790
+ @media (max-width: 640px) {
791
+ .road-row { grid-template-columns: 1.5rem 1fr; }
792
+ .road-t { grid-column: 2; }
793
+ }`,
794
+ }
795
+
796
+ const TEACH_QUIZ: Component = {
797
+ name: 'teach-quiz',
798
+ note: [
799
+ 'The recognition quiz a lesson embeds: the option list, its right/wrong/chosen',
800
+ 'states, and the feedback block. `canon teach nav` layers a small stepper block',
801
+ 'of its own on top of this for the non-legacy option shape.',
802
+ ].join('\n '),
803
+ reads: [
804
+ '--color-border',
805
+ '--color-surface',
806
+ '--color-text',
807
+ '--color-text-secondary',
808
+ '--color-muted',
809
+ '--color-accent',
810
+ '--teach-sans',
811
+ '--teach-mono',
812
+ '--teach-shadow',
813
+ '--color-teach-accent-bg',
814
+ ],
815
+ rules: `/* ---- Quiz ---- */
816
+
817
+ .quiz { margin-top: 3rem; border-top: 1px solid var(--color-border); padding-top: 1.75rem; }
818
+ .quiz > h2:first-child { margin-top: 0; }
819
+ .q { margin: 0 0 2.5rem; }
820
+ .q-stem { font-weight: 700; margin-bottom: 0.85rem; }
821
+
822
+ .opt {
823
+ display: flex;
824
+ gap: 0.75rem;
825
+ align-items: baseline;
826
+ border: 1px solid var(--color-border);
827
+ border-radius: 8px;
828
+ padding: 0.7rem 0.95rem;
829
+ margin-bottom: 0.45rem;
830
+ cursor: pointer;
831
+ font-family: var(--teach-sans);
832
+ font-size: 0.9375rem;
833
+ background: transparent;
834
+ width: 100%;
835
+ text-align: left;
836
+ color: inherit;
837
+ line-height: 1.45;
838
+ box-shadow: var(--teach-shadow);
839
+ }
840
+
841
+ .opt::before {
842
+ content: attr(data-k);
843
+ font-family: var(--teach-mono);
844
+ font-size: 0.7188rem;
845
+ color: var(--color-muted);
846
+ border: 1px solid var(--color-border);
847
+ border-radius: 5px;
848
+ padding: 0.05rem 0.38rem;
849
+ flex: 0 0 auto;
850
+ }
851
+
852
+ .opt:hover { border-color: var(--color-accent); background: var(--color-teach-accent-bg); }
853
+ .opt[data-state="right"] { border-color: var(--color-accent); background: var(--color-teach-accent-bg); }
854
+ .opt[data-state="right"]::before { color: var(--color-accent); border-color: var(--color-accent); }
855
+ .opt[data-state="wrong"] { opacity: 0.38; }
856
+
857
+ /* The option the learner actually picked stays legible and says so.
858
+ A wrong pick that fades like an option nobody considered destroys the
859
+ contrast between what they thought and what is true, which is the
860
+ moment the correction lands. */
861
+
862
+ .opt[data-state="chosen"] {
863
+ opacity: 1;
864
+ border-color: var(--color-muted);
865
+ border-style: dashed;
866
+ }
867
+
868
+ .opt[data-state="chosen"]::after {
869
+ content: "your answer";
870
+ margin-left: auto;
871
+ font-size: 0.6563rem;
872
+ font-weight: 700;
873
+ letter-spacing: 0.07em;
874
+ text-transform: uppercase;
875
+ color: var(--color-muted);
876
+ align-self: center;
877
+ }
878
+
879
+ .opt[data-state="right"]::after {
880
+ content: "correct";
881
+ margin-left: auto;
882
+ font-size: 0.6563rem;
883
+ font-weight: 700;
884
+ letter-spacing: 0.07em;
885
+ text-transform: uppercase;
886
+ color: var(--color-accent);
887
+ align-self: center;
888
+ }
889
+
890
+ .fb {
891
+ display: none;
892
+ font-size: 0.9375rem;
893
+ line-height: 1.55;
894
+ color: var(--color-text-secondary);
895
+ background: var(--color-surface);
896
+ border-radius: 7px;
897
+ padding: 0.85rem 1.05rem;
898
+ margin-top: 0.55rem;
899
+ }
900
+
901
+ .fb.show { display: block; }
902
+ .fb b { color: var(--color-text); font-weight: 700; }
903
+
904
+ footer {
905
+ margin-top: 4rem;
906
+ padding-top: 1.4rem;
907
+ border-top: 1px solid var(--color-border);
908
+ font-size: 0.8125rem;
909
+ line-height: 1.6;
910
+ color: var(--color-muted);
911
+ }`,
912
+ }
913
+
914
+ const TEACH_GLOSSARY: Component = {
915
+ name: 'teach-glossary',
916
+ note: [
917
+ 'The filterable glossary view a workspace root carries: the term count, the',
918
+ 'filter input, and the scrolling term list.',
919
+ ].join('\n '),
920
+ reads: [
921
+ '--color-background',
922
+ '--color-border',
923
+ '--color-text-body',
924
+ '--color-text-secondary',
925
+ '--color-muted',
926
+ '--color-accent',
927
+ '--teach-sans',
928
+ '--teach-mono',
929
+ ],
930
+ rules: `/* ---- Glossary view, generated from the markdown source ---- */
931
+
932
+ h2 .count {
933
+ font-family: var(--teach-mono);
934
+ font-size: 0.75rem;
935
+ font-weight: 400;
936
+ color: var(--color-muted);
937
+ border: 1px solid var(--color-border);
938
+ border-radius: 99px;
939
+ padding: 0.1rem 0.5rem;
940
+ margin-left: 0.5rem;
941
+ vertical-align: middle;
942
+ }
943
+
944
+ .filter {
945
+ width: 100%;
946
+ font-family: var(--teach-sans);
947
+ font-size: 0.9375rem;
948
+ color: var(--color-text-body);
949
+ background: var(--color-background);
950
+ border: 1px solid var(--color-border);
951
+ border-radius: 7px;
952
+ padding: 0.55rem 0.8rem;
953
+ margin: 0 0 1rem;
954
+ }
955
+
956
+ /* Scoped to pointer focus. Written as \`.filter:focus\` it outranked the
957
+ \`:where(...)\` focus token, whose specificity is zero, and the filter was the
958
+ one control on the page that a keyboard reached with no ring at all. */
959
+ .filter:focus { border-color: var(--color-accent); }
960
+ .filter:focus:not(:focus-visible) { outline: none; }
961
+ .filter::placeholder { color: var(--color-muted); }
962
+
963
+ /* A fixed scroll region rather than a collapsing list. Filtering a list that
964
+ sets page height makes the page jump under the reader, and the same fixed
965
+ region is what lets the glossary grow past what one screen shows without
966
+ the page growing with it. */
967
+
968
+ /* A scroll region is a tab stop in its own right, so it takes the same ring
969
+ as everything else rather than the browser's white default. */
970
+ .gloss:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; border-radius: 5px; }
971
+
972
+ .gloss {
973
+ height: clamp(14rem, 46vh, 32rem);
974
+ overflow-y: auto;
975
+ margin-bottom: 2.5rem;
976
+ border-bottom: 1px solid var(--color-border);
977
+ scrollbar-width: thin;
978
+ }
979
+
980
+ /* Scrollbar styling is global rather than per component. It was scoped to the
981
+ glossary, so the outline rail, the code blocks, and the scrolling tables all
982
+ fell back to the browser default, which paints a light track on a dark page
983
+ and is the one element on screen taking no color from these tokens. */
984
+
985
+ .gloss .empty {
986
+ display: none;
987
+ padding: 1.2rem 0;
988
+ color: var(--color-muted);
989
+ font-size: 0.9375rem;
990
+ }
991
+
992
+ .gloss.none .empty { display: block; }
993
+
994
+ .gterm {
995
+ padding: 0.7rem 0;
996
+ border-bottom: 1px solid var(--color-border);
997
+ font-size: 0.9375rem;
998
+ line-height: 1.55;
999
+ }
1000
+
1001
+ .gterm b { font-weight: 700; }
1002
+ /* The markdown source separates a term from its definition with a colon,
1003
+ and the house standard bans an em dash outright, so the view mirrors
1004
+ the source rather than inventing punctuation for it. */
1005
+ /* The space is part of the generated content, since the term and its
1006
+ definition are adjacent elements with no whitespace between them in the
1007
+ markup and the colon rendered flush against the first word. */
1008
+ .gterm b::after { content: ": "; color: var(--color-muted); font-weight: 400; white-space: pre; }
1009
+ .gterm span { color: var(--color-text-secondary); }
1010
+
1011
+ ol.succ {
1012
+ margin: 0 0 2.5rem;
1013
+ padding-left: 1.35rem;
1014
+ font-size: 0.9375rem;
1015
+ line-height: 1.6;
1016
+ color: var(--color-text-secondary);
1017
+ }
1018
+
1019
+ ol.succ li { margin-bottom: 0.4rem; }
1020
+ .toc li.soon b { color: var(--color-muted); font-weight: 400; }
1021
+
1022
+ /* A filter that matches nothing needs a way back, not just a sentence. */
1023
+ .gloss .empty .clear {
1024
+ font-family: var(--teach-sans);
1025
+ font-size: 0.875rem;
1026
+ color: var(--color-accent);
1027
+ background: none;
1028
+ border: 0;
1029
+ border-bottom: 1px solid currentColor;
1030
+ padding: 0;
1031
+ margin-left: 0.35rem;
1032
+ cursor: pointer;
1033
+ }`,
1034
+ }
1035
+
1036
+ const TEACH_OUTLINE: Component = {
1037
+ name: 'teach-outline',
1038
+ note: [
1039
+ 'The in-lesson outline rail in the gutter the reading measure leaves over, and',
1040
+ 'the workspace-switcher row inside the lesson jump menu.',
1041
+ ].join('\n '),
1042
+ reads: [
1043
+ '--color-border',
1044
+ '--color-text-body',
1045
+ '--color-muted',
1046
+ '--color-accent',
1047
+ '--teach-sans',
1048
+ '--teach-measure',
1049
+ '--teach-mast-h',
1050
+ ],
1051
+ rules: `/* ---- In-lesson outline, in the gutter the measure leaves over ---- */
1052
+
1053
+ .outline {
1054
+ position: fixed;
1055
+ top: calc(var(--teach-mast-h) + 2.2rem);
1056
+ /* Four pixels of padding with the position pulled back by the same amount,
1057
+ so a focus ring at the rail's edge has somewhere to draw. \`overflow-y\`
1058
+ clips horizontally too, which took a bite out of every ring in here. */
1059
+ left: calc(50% + var(--teach-measure) / 2 + 2.5rem - 4px);
1060
+ padding: 0 4px;
1061
+ width: calc(13rem + 8px);
1062
+ max-height: calc(100vh - var(--teach-mast-h) - 4rem);
1063
+ overflow-y: auto;
1064
+ font-size: 0.8125rem;
1065
+ line-height: 1.45;
1066
+ }
1067
+
1068
+ /* The heading is a control rather than a label, because returning to the top
1069
+ of a long lesson is the one thing the rail could not do. */
1070
+ .outline .to-top {
1071
+ display: flex;
1072
+ align-items: center;
1073
+ gap: 0.4rem;
1074
+ width: 100%;
1075
+ font-family: var(--teach-sans);
1076
+ font-size: 0.6563rem;
1077
+ font-weight: 700;
1078
+ letter-spacing: 0.09em;
1079
+ text-transform: uppercase;
1080
+ color: var(--color-muted);
1081
+ background: none;
1082
+ border: 0;
1083
+ border-bottom: 1px solid var(--color-border);
1084
+ border-radius: 3px 3px 0 0;
1085
+ margin: 0 0 0.6rem;
1086
+ padding: 0.2rem 0 0.55rem;
1087
+ cursor: pointer;
1088
+ text-align: left;
1089
+ }
1090
+
1091
+ .outline .to-top:hover { color: var(--color-accent); border-bottom-color: var(--color-border); }
1092
+
1093
+ .outline .to-top::before {
1094
+ content: "";
1095
+ width: 11px;
1096
+ height: 8px;
1097
+ background:
1098
+ linear-gradient(var(--color-muted) 0 0) 0 0 / 11px 1.5px no-repeat,
1099
+ linear-gradient(var(--color-muted) 0 0) 0 3.2px / 8px 1.5px no-repeat,
1100
+ linear-gradient(var(--color-muted) 0 0) 0 6.5px / 11px 1.5px no-repeat;
1101
+ }
1102
+
1103
+ .outline a {
1104
+ display: block;
1105
+ padding: 0.3rem 0 0.3rem 0.7rem;
1106
+ border-left: 2px solid var(--color-border);
1107
+ color: var(--color-muted);
1108
+ text-decoration: none;
1109
+ }
1110
+
1111
+ .outline a:hover { color: var(--color-text-body); border-left-color: var(--color-border); }
1112
+ .outline a.on { color: var(--color-accent); border-left-color: var(--color-accent); }
1113
+
1114
+ @media (max-width: 1420px) { .outline { display: none; } }`,
1115
+ }
1116
+
1117
+ const TEACH_FIGURES: Component = {
1118
+ name: 'teach-figures',
1119
+ note: [
1120
+ 'The hand-drawn SVG diagram figure and its caption, sized wider than the',
1121
+ 'reading measure and cleared past the outline rail on a wide viewport.',
1122
+ ].join('\n '),
1123
+ reads: ['--color-muted', '--teach-hand', '--teach-measure', '--teach-wide'],
1124
+ rules: `/* ---- Figures: hand-drawn, and wider than the measure ---- */
1125
+
1126
+ figure {
1127
+ margin: 2.75rem 0;
1128
+ width: var(--teach-wide);
1129
+ max-width: 92vw;
1130
+ margin-left: 50%;
1131
+ transform: translateX(-50%);
1132
+ }
1133
+
1134
+ figure svg { width: 100%; height: auto; display: block; }
1135
+
1136
+ /* CSS beats an SVG presentation attribute, so the diagrams pick up the
1137
+ hand face without editing a single lesson. */
1138
+ figure svg text { font-family: var(--teach-hand); }
1139
+
1140
+ figcaption {
1141
+ font-family: var(--teach-hand);
1142
+ font-size: 1rem;
1143
+ color: var(--color-muted);
1144
+ margin: 1rem auto 0;
1145
+ line-height: 1.5;
1146
+ max-width: var(--teach-measure);
1147
+ }
1148
+
1149
+ /* A figure breaks the measure and must still clear the outline rail. The rail
1150
+ starts at \`50% + 26rem + 2.5rem\`, so a figure centred on the same axis may
1151
+ reach 27rem from centre and no further, which is 54rem wide with a 1.5rem
1152
+ gap left over. Deriving it from the viewport was the earlier attempt and it
1153
+ does not hold: the rail is positioned from the centre, not from the edge, so
1154
+ a wider window moved both and kept the overlap. */
1155
+ @media (min-width: 1421px) {
1156
+ figure { max-width: 54rem; }
1157
+ }
1158
+
1159
+ @media (max-width: 640px) {
1160
+ figure { width: 100%; max-width: 100%; margin-left: 0; transform: none; }
1161
+ }`,
1162
+ }
1163
+
1164
+ const TEACH_REFERENCES: Component = {
1165
+ name: 'teach-references',
1166
+ note: [
1167
+ 'The numbered citation marks and reference list a lesson closes with, and the',
1168
+ 'rendered view of a promoted reference page.',
1169
+ ].join('\n '),
1170
+ reads: [
1171
+ '--color-background',
1172
+ '--color-border',
1173
+ '--color-text-secondary',
1174
+ '--color-muted',
1175
+ '--color-accent',
1176
+ '--teach-mono',
1177
+ '--color-teach-accent-bg',
1178
+ ],
1179
+ rules: `footer {
1180
+ margin-top: 4rem;
1181
+ padding-top: 1.4rem;
1182
+ border-top: 1px solid var(--color-border);
1183
+ font-size: 0.8125rem;
1184
+ line-height: 1.6;
1185
+ color: var(--color-muted);
1186
+ }
1187
+
1188
+ /* ---- References, numbered rather than narrated ---- */
1189
+
1190
+ sup.cite {
1191
+ font-family: var(--teach-mono);
1192
+ font-size: 0.6875em;
1193
+ line-height: 0;
1194
+ margin-left: 0.1em;
1195
+ }
1196
+
1197
+ sup.cite a {
1198
+ text-decoration: none;
1199
+ padding: 0.05em 0.25em;
1200
+ border-radius: 3px;
1201
+ background: var(--color-teach-accent-bg);
1202
+ color: var(--color-accent);
1203
+ }
1204
+
1205
+ sup.cite a:hover { background: var(--color-accent); color: var(--color-background); }
1206
+
1207
+ ol.refs {
1208
+ list-style: none;
1209
+ counter-reset: r;
1210
+ padding: 0;
1211
+ margin: 0.9rem 0 1.1rem;
1212
+ font-size: 0.8125rem;
1213
+ line-height: 1.55;
1214
+ }
1215
+
1216
+ ol.refs li {
1217
+ counter-increment: r;
1218
+ position: relative;
1219
+ padding-left: 2.1rem;
1220
+ margin-bottom: 0.5rem;
1221
+ }
1222
+
1223
+ ol.refs li::before {
1224
+ content: "[" counter(r) "]";
1225
+ position: absolute;
1226
+ left: 0;
1227
+ font-family: var(--teach-mono);
1228
+ color: var(--color-accent);
1229
+ }
1230
+
1231
+ ol.refs cite { font-style: italic; color: var(--color-text-secondary); }
1232
+ ol.refs a { word-break: break-word; }
1233
+
1234
+ footer .own {
1235
+ margin: 0;
1236
+ padding-top: 0.7rem;
1237
+ border-top: 1px solid var(--color-border);
1238
+ color: var(--color-muted);
1239
+ }
1240
+
1241
+ main.ref { padding-bottom: 4rem; }
1242
+ main.ref h1 { margin-top: 1.5rem; }
1243
+ main.ref h2 { margin-top: 2.5rem; }
1244
+ main.ref ol, main.ref ul { padding-left: 1.4rem; }
1245
+ main.ref li { margin-bottom: 0.5rem; }
1246
+ main.ref blockquote {
1247
+ margin: 1.5rem 0;
1248
+ padding: 0.1rem 0 0.1rem 1.1rem;
1249
+ border-left: 3px solid var(--color-border);
1250
+ color: var(--color-text-secondary);
1251
+ }`,
1252
+ }
1253
+
1254
+ export const TEACH_COMPONENTS: readonly Component[] = [
1255
+ TEACH_CHROME,
1256
+ TEACH_ARTICLE,
1257
+ TEACH_QUIZ,
1258
+ TEACH_GLOSSARY,
1259
+ TEACH_OUTLINE,
1260
+ TEACH_FIGURES,
1261
+ TEACH_REFERENCES,
1262
+ ]
1263
+
1264
+ /**
1265
+ * The generic components every design consumer gets, plus the teach chrome,
1266
+ * for the one caller that wants both: a teach workspace stylesheet.
1267
+ */
1268
+ export const TEACH_STYLESHEET_COMPONENTS: readonly Component[] = [
1269
+ ...COMPONENTS,
1270
+ ...TEACH_COMPONENTS,
1271
+ ]