@erclx/canon 5.51.0 → 5.53.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 (142) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -4
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +0 -3
  5. package/claude/skills/context-fold/REQUIREMENT.md +0 -1
  6. package/claude/skills/design-taste/references/kinds.md +0 -1
  7. package/claude/skills/draw-figure/REQUIREMENT.md +2 -3
  8. package/claude/skills/draw-figure/SKILL.md +4 -5
  9. package/claude/skills/draw-figure/references/figures.md +0 -1
  10. package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
  11. package/claude/skills/git-pr/references/pr.md +1 -1
  12. package/claude/skills/pick-by-eye/REQUIREMENT.md +0 -1
  13. package/claude/skills/pick-by-eye/SKILL.md +1 -1
  14. package/claude/skills/review-address/SKILL.md +1 -1
  15. package/claude/skills/review-address/references/post-review-findings.md +1 -1
  16. package/claude/skills/review-pr/references/post.md +2 -2
  17. package/claude/skills/target-check/SKILL.md +2 -0
  18. package/claude/skills/target-setup/SKILL.md +1 -1
  19. package/claude/skills/target-update/REQUIREMENT.md +33 -0
  20. package/claude/skills/target-update/SKILL.md +136 -0
  21. package/claude/skills/teach-craft/REQUIREMENT.md +30 -0
  22. package/claude/skills/teach-craft/SKILL.md +39 -0
  23. package/claude/skills/teach-craft/references/lesson-craft.md +52 -0
  24. package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +12 -4
  25. package/docs/agents/audits.md +0 -1
  26. package/docs/agents/migrate.md +2 -1
  27. package/docs/target/migrations.md +12 -1
  28. package/docs/target/projects.md +1 -1
  29. package/docs/target/sync.md +1 -1
  30. package/docs/workflow/parallel-features.md +1 -1
  31. package/docs/workflow/skill-map-on-request.md +10 -22
  32. package/docs/workflow/skill-map.md +13 -14
  33. package/package.json +1 -4
  34. package/scripts/core/regen-web-previews.ts +2 -67
  35. package/src/audits/catalog.ts +0 -1
  36. package/src/browser/engine.ts +3 -6
  37. package/src/capture/render.ts +1 -1
  38. package/src/claude/cases/authoring.ts +0 -12
  39. package/src/claude/cases/misc.ts +0 -17
  40. package/src/claude/cases/setup.ts +7 -0
  41. package/src/claude/cases/workflow.ts +2 -38
  42. package/src/claude/skills-audit.ts +0 -1
  43. package/src/cli/help.ts +0 -7
  44. package/src/cli.ts +0 -6
  45. package/src/commands/labels.ts +9 -7
  46. package/src/commands/records.ts +0 -1
  47. package/src/commands/serve.ts +2 -2
  48. package/src/design/components.ts +8 -1514
  49. package/src/design/css.ts +5 -6
  50. package/src/design/fonts.ts +3 -5
  51. package/src/driver/drive.ts +3 -4
  52. package/src/driver/steps.ts +2 -5
  53. package/src/inventory/walk.ts +1 -1
  54. package/src/markdown/scope.ts +4 -31
  55. package/src/migrate/status.ts +5 -1
  56. package/src/records/validate.ts +4 -176
  57. package/src/sandbox/equivalence.ts +0 -5
  58. package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +1 -1
  59. package/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
  60. package/claude/skills/draft-and-pick/SKILL.md +0 -11
  61. package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
  62. package/claude/skills/draft-doc/SKILL.md +0 -11
  63. package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
  64. package/claude/skills/draft-figure/SKILL.md +0 -11
  65. package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
  66. package/claude/skills/draft-identity/SKILL.md +0 -11
  67. package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
  68. package/claude/skills/draft-ready/SKILL.md +0 -11
  69. package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
  70. package/claude/skills/draft-screencast/SKILL.md +0 -11
  71. package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
  72. package/claude/skills/draft-slides/SKILL.md +0 -11
  73. package/claude/skills/make-deck/REQUIREMENT.md +0 -36
  74. package/claude/skills/make-deck/SKILL.md +0 -80
  75. package/claude/skills/make-deck/references/deck.md +0 -183
  76. package/claude/skills/read-frames/REQUIREMENT.md +0 -30
  77. package/claude/skills/read-frames/SKILL.md +0 -55
  78. package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
  79. package/claude/skills/record-screencast/SKILL.md +0 -99
  80. package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
  81. package/claude/skills/script-screencast/SKILL.md +0 -70
  82. package/claude/skills/script-screencast/references/beats-template.md +0 -119
  83. package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
  84. package/claude/skills/teach-workspace/SKILL.md +0 -196
  85. package/claude/skills/teach-workspace/references/glossary.md +0 -90
  86. package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
  87. package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
  88. package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
  89. package/claude/skills/teach-workspace/references/promotion.md +0 -116
  90. package/claude/skills/teach-workspace/references/revisit.md +0 -28
  91. package/claude/skills/teach-workspace/references/teach.md +0 -211
  92. package/claude/skills/video-craft/REQUIREMENT.md +0 -35
  93. package/claude/skills/video-craft/SKILL.md +0 -96
  94. package/claude/skills/video-craft/references/adopted.md +0 -110
  95. package/src/commands/demo.ts +0 -542
  96. package/src/commands/slides.ts +0 -184
  97. package/src/commands/teach.ts +0 -1401
  98. package/src/demo/beats.ts +0 -135
  99. package/src/demo/compile.ts +0 -350
  100. package/src/demo/container.ts +0 -219
  101. package/src/demo/cursors.ts +0 -55
  102. package/src/demo/drive.ts +0 -442
  103. package/src/demo/pointer.ts +0 -178
  104. package/src/demo/theme.ts +0 -112
  105. package/src/demo/timeline.ts +0 -64
  106. package/src/slides/convert/chart.ts +0 -165
  107. package/src/slides/convert/deck.ts +0 -244
  108. package/src/slides/convert/export.ts +0 -567
  109. package/src/slides/convert/master.ts +0 -198
  110. package/src/slides/convert/shapes.ts +0 -804
  111. package/src/slides/convert/svg.ts +0 -38
  112. package/src/slides/convert/walk.ts +0 -671
  113. package/src/slides/decks.ts +0 -110
  114. package/src/slides/open.ts +0 -18
  115. package/src/slides/package/fonts.ts +0 -301
  116. package/src/slides/package/motion.ts +0 -316
  117. package/src/slides/package/test-face.ts +0 -113
  118. package/src/teach/browser/compile.ts +0 -44
  119. package/src/teach/browser/dismiss.ts +0 -76
  120. package/src/teach/browser/focus-line.ts +0 -19
  121. package/src/teach/browser/glossary-filter.ts +0 -44
  122. package/src/teach/browser/head.ts +0 -30
  123. package/src/teach/browser/quiz.ts +0 -19
  124. package/src/teach/browser/reload.ts +0 -9
  125. package/src/teach/browser/sidebar.ts +0 -233
  126. package/src/teach/browser/theme.ts +0 -18
  127. package/src/teach/components/code.tsx +0 -24
  128. package/src/teach/components/heading.tsx +0 -11
  129. package/src/teach/components/list.tsx +0 -17
  130. package/src/teach/components/paragraph.tsx +0 -29
  131. package/src/teach/components/refs.tsx +0 -28
  132. package/src/teach/cross-references.ts +0 -78
  133. package/src/teach/fonts.ts +0 -16
  134. package/src/teach/highlight.ts +0 -71
  135. package/src/teach/html/jsx-dev-runtime.ts +0 -13
  136. package/src/teach/html/jsx-runtime.ts +0 -113
  137. package/src/teach/html/raw.ts +0 -28
  138. package/src/teach/lesson.ts +0 -178
  139. package/src/teach/nav.ts +0 -1337
  140. package/src/teach/render.ts +0 -240
  141. package/src/teach/workspace.ts +0 -1164
  142. package/src/teach/write-if-changed.ts +0 -17
@@ -6,14 +6,10 @@
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 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.
9
+ * `COMPONENTS` is the generic default every project installing the `design`
10
+ * domain receives. A caller wanting a different set passes an explicit list to
11
+ * `buildDesignCss`'s `components` option, which takes
12
+ * `boolean | readonly Component[]` for that reason.
17
13
  *
18
14
  * It does not live in `canon/DESIGN.md`. `claude/skills/design-record/references/design.md` keeps CSS class
19
15
  * names out of that record and says they live in code, which is here.
@@ -122,14 +118,10 @@ const SCROLLBAR: Component = {
122
118
  /**
123
119
  * The hand-drawn SVG diagram figure and its caption, sized wider than the
124
120
  * reading measure and cleared past an outline rail on a wide viewport.
125
- * Promoted out of `TEACH_COMPONENTS` per
126
- * `.canon/groundwork/86-portable-hand-drawn-figures/06-decision.md` item 4:
127
- * any project can draw one hand-drawn-styled figure without teach's own quiz
128
- * and schedule machinery. It could not move as `TEACH_FIGURES` as-is, since
129
- * that component's `reads` list named `--teach-hand` and `--teach-wide`
130
- * while only `TEACH_CHROME` declared them, so this component declares its
131
- * own `--figure-hand` and `--figure-wide` defaults rather than borrowing
132
- * undeclared custom properties from a teach-only sibling.
121
+ * Per `.canon/groundwork/86-portable-hand-drawn-figures/06-decision.md` item 4,
122
+ * any project can draw one hand-drawn-styled figure. It declares its own
123
+ * `--figure-hand` and `--figure-wide` defaults rather than reading custom
124
+ * properties another component would have to declare.
133
125
  */
134
126
  const HAND_DRAWN_FIGURE: Component = {
135
127
  name: 'hand-drawn-figure',
@@ -199,1501 +191,3 @@ export const COMPONENTS: readonly Component[] = [
199
191
  SCROLLBAR,
200
192
  HAND_DRAWN_FIGURE,
201
193
  ]
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
-
213
- /**
214
- * The colors highlighted code renders in, one per token class the teach
215
- * article maps highlight.js spans onto. Hex rather than `color-mix` so the
216
- * contrast test can measure each against the code surface, and dark by default
217
- * with a light override, following the same `data-theme` switch as the ground
218
- * they sit on. Teach-local until a second surface renders highlighted code.
219
- * `keyword`, `comment` and `meta` repeat the accent, muted and secondary
220
- * values, so code reads in the page's own palette.
221
- */
222
- export const TEACH_SYNTAX: readonly {
223
- readonly name: string
224
- readonly dark: string
225
- readonly light: string
226
- }[] = [
227
- { name: 'keyword', dark: '#c76b5f', light: '#ad4a4b' },
228
- { name: 'string', dark: '#8dbb6c', light: '#2d6b22' },
229
- { name: 'number', dark: '#d4a15a', light: '#8a5300' },
230
- { name: 'title', dark: '#b99ad8', light: '#72489a' },
231
- { name: 'attr', dark: '#6fb3c4', light: '#1e6478' },
232
- { name: 'comment', dark: '#7f7f7c', light: '#6e6d6c' },
233
- { name: 'meta', dark: '#8c8b86', light: '#666561' },
234
- ]
235
-
236
- const syntaxProperty = (name: string): string => `--teach-syntax-${name}`
237
-
238
- const TEACH_SYNTAX_DARK = TEACH_SYNTAX.map(
239
- ({ name, dark }) => ` ${syntaxProperty(name)}: ${dark};`,
240
- ).join('\n')
241
-
242
- const TEACH_SYNTAX_LIGHT = TEACH_SYNTAX.map(
243
- ({ name, light }) => ` ${syntaxProperty(name)}: ${light};`,
244
- ).join('\n')
245
-
246
- const TEACH_CHROME: Component = {
247
- name: 'teach-chrome',
248
- note: [
249
- 'The masthead, breadcrumb, jump menu, theme toggle, course sidebar, and footer',
250
- 'navigation shared by every teach page. Recovered from two gitignored',
251
- 'course.css files that predate a regression that dropped this layer from the',
252
- "generator, and rewritten onto this module's tokens rather than the",
253
- 'incompatible palette they carried. See canon/context/features/teach.md',
254
- 'for the shape.',
255
- ].join('\n '),
256
- reads: [
257
- '--color-background',
258
- '--color-surface',
259
- '--color-chrome',
260
- '--color-border',
261
- '--color-text',
262
- '--color-text-body',
263
- '--color-text-secondary',
264
- '--color-muted',
265
- '--color-accent',
266
- '--teach-sans',
267
- '--teach-mono',
268
- '--teach-measure',
269
- '--teach-shadow',
270
- '--color-teach-accent-bg',
271
- '--type-body-family',
272
- '--type-code-family',
273
- '--t3',
274
- '--t4',
275
- '--t5',
276
- '--t6',
277
- ],
278
- rules: `:root {
279
- --teach-sans: var(--type-body-family);
280
- --teach-hand: 'Virgil', 'Excalifont', cursive;
281
- --teach-mono: var(--type-code-family);
282
- --teach-measure: 52rem;
283
- --teach-mast-h: 3.5rem;
284
- --teach-shadow: 0 1px 2px rgba(20, 20, 20, 0.04);
285
- --color-teach-accent-bg: color-mix(in srgb, var(--color-accent) 14%, var(--color-background));
286
-
287
- /* Aliases for the retired teach palette. Every already-written lesson draws
288
- its hand-authored diagrams with fill and stroke values pinned to these
289
- names directly in the SVG markup, which this rewrite cannot reach without
290
- editing lesson content the same pass that recovered the chrome was asked
291
- not to touch. */
292
- --panel: var(--color-surface);
293
- --rule: var(--color-border);
294
- --ink: var(--color-text-body);
295
- --ink-soft: var(--color-text-secondary);
296
- --ink-faint: var(--color-muted);
297
- --accent: var(--color-accent);
298
- --accent-bg: var(--color-teach-accent-bg);
299
-
300
- ${TEACH_SYNTAX_DARK}
301
- }
302
-
303
- [data-theme='light'] {
304
- ${TEACH_SYNTAX_LIGHT}
305
- }
306
-
307
- @media (prefers-color-scheme: dark) {
308
- :root:not([data-theme='light']) {
309
- --teach-shadow: none;
310
- }
311
- }
312
-
313
- :root[data-theme='dark'] {
314
- --teach-shadow: none;
315
- }
316
-
317
- html { scroll-padding-top: calc(var(--teach-mast-h) + 1rem); }
318
-
319
- /* No padding at all on the body, so the sticky bar reaches both window edges
320
- and the sidebar can travel the full height. The gutter moves onto \`main\` and
321
- the bottom room onto \`.pane\`: a flex item's sticky containing block is the
322
- flex container's content box, so padding here is space the sidebar could
323
- never reach and it would ride up by exactly that much near the foot. */
324
- body {
325
- margin: 0;
326
- padding: 0;
327
- display: flex;
328
- align-items: flex-start;
329
- background: var(--color-background);
330
- color: var(--color-text-body);
331
- font-family: var(--teach-sans);
332
- font-size: var(--t3);
333
- line-height: 1.55;
334
- -webkit-font-smoothing: antialiased;
335
- }
336
-
337
- /* The bar row and the text column resolve to the same 52rem content box, so a
338
- listing page and a lesson start at the same left edge as the chrome above
339
- them. A narrower \`.wide-body\` was what put the navigation and the content on
340
- two different measures. */
341
- main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); margin: 0 auto; padding: 2.25rem 1.5rem 0; }
342
- .wide-body { max-width: calc(var(--teach-measure) + 3rem); }
343
-
344
- /* Everything but the sidebar. \`flow-root\` rather than a bare block, because a
345
- bottom margin collapsing out of here makes the document taller than the flex
346
- container and a sticky sidebar can only travel inside its containing block,
347
- so it would ride up by exactly the escaped margin on a tall window. The
348
- border box is what keeps \`min-height: 100vh\` plus the bottom room from
349
- scrolling every short page by that padding. */
350
- .pane {
351
- flex: 1 1 auto;
352
- min-width: 0;
353
- display: flow-root;
354
- box-sizing: border-box;
355
- min-height: 100vh;
356
- padding-bottom: 7rem;
357
- border-left: 1px solid var(--color-border);
358
- }
359
-
360
- html.sb-shut .pane { border-left: 0; }
361
-
362
- .bar {
363
- position: sticky;
364
- top: 0;
365
- z-index: 30;
366
- background: var(--color-background);
367
- border-bottom: 1px solid var(--color-border);
368
- padding: 0 1.5rem;
369
- cursor: default;
370
- }
371
-
372
- /* The reading bar rides the masthead's own bottom edge rather than arriving as
373
- a second element. It reports position inside the lesson, which nothing
374
- reported, where the retired track reported position in the course, which the
375
- sidebar now reports. */
376
- .bar::after {
377
- content: "";
378
- position: absolute;
379
- left: 0;
380
- bottom: -1px;
381
- height: 2px;
382
- width: var(--read, 0%);
383
- background: var(--color-accent);
384
- }
385
-
386
- /* The height is the token rather than the sum of two paddings, so the sidebar's
387
- own top row lands on the same seam the bar's bottom border draws. The row
388
- spans the window rather than the reading measure: capped, the toggle and the
389
- theme control floated in from the edges by an amount that changed per page. */
390
- .mast {
391
- max-width: none;
392
- margin: 0;
393
- min-height: var(--teach-mast-h);
394
- padding: 0 0.9rem;
395
- display: flex;
396
- justify-content: space-between;
397
- align-items: center;
398
- gap: 0.55rem;
399
- font-size: var(--t5);
400
- color: var(--color-muted);
401
- cursor: default;
402
- }
403
-
404
- .mast a { color: var(--color-accent); text-decoration: none; font-weight: 600; }
405
- .mast a:hover { text-decoration: underline; }
406
-
407
- .nav {
408
- width: 100%;
409
- margin: 0;
410
- display: flex;
411
- gap: 0.6rem;
412
- }
413
-
414
- .nav > a, .nav > span {
415
- flex: 1;
416
- padding: 0.65rem 0.95rem;
417
- border: 1px solid var(--color-border);
418
- border-radius: 8px;
419
- text-decoration: none;
420
- color: var(--color-text-body);
421
- font-size: var(--t4);
422
- font-weight: 600;
423
- line-height: 1.3;
424
- box-shadow: var(--teach-shadow);
425
- transition: border-color 0.12s, background 0.12s;
426
- }
427
-
428
- .nav > a:hover { border-color: var(--color-accent); background: var(--color-teach-accent-bg); }
429
- .nav > .end { color: var(--color-muted); border-style: dashed; font-weight: 400; box-shadow: none; }
430
- .nav > :only-child { flex: 0 1 calc(50% - 0.3rem); }
431
- .nav > .to-next:only-child { margin-left: auto; }
432
-
433
- .nav .lbl {
434
- display: block;
435
- font-size: var(--t6);
436
- font-weight: 700;
437
- letter-spacing: 0;
438
- color: var(--color-muted);
439
- margin-bottom: 0.15rem;
440
- }
441
-
442
- .nav > a:hover .lbl { color: var(--color-accent); }
443
- .nav .to-next { text-align: right; }
444
- .nav .to-index { flex: 0 0 auto; text-align: center; }
445
-
446
- /* ---- Masthead contents ---- */
447
-
448
- /* Sticky position and stacking both live on \`.bar\` now, so nothing here
449
- restates either. */
450
- /* A breadcrumb rather than a row of links. The gap tightens because the
451
- separators now carry the spacing the gap used to. */
452
- .mast-left { display: flex; align-items: center; gap: 0.3rem; min-width: 0; flex-wrap: wrap; cursor: default; }
453
-
454
- /* \`--rule\` measured 1.27:1 here. It is a border token, and a border is a
455
- shape a reader infers rather than a glyph they resolve, so it is the wrong
456
- value for a character even a decorative one. */
457
- .crumb-sep {
458
- color: var(--color-muted);
459
- font-size: var(--t4);
460
- margin: 0 0.1rem;
461
- user-select: none;
462
- }
463
-
464
- /* One arrow, on the first segment, since that is the only one that goes up. */
465
- .crumb-back {
466
- margin-right: 0.3rem;
467
- font-size: var(--t4);
468
- line-height: 1;
469
- }
470
-
471
- .crumb { display: inline-flex; align-items: center; }
472
-
473
- /* The label and its caret are one component with two zones, like a split
474
- button: the label navigates, the caret opens the menu, and one surface fills
475
- for both so the pair reads as a single chip rather than two loose pieces.
476
- The pointer covers the gap between them, which is interactive on both sides. */
477
- .crumb-item {
478
- display: inline-flex;
479
- align-items: center;
480
- gap: 0;
481
- border-radius: 7px;
482
- padding: 0.1rem 0.15rem 0.1rem 0.35rem;
483
- cursor: pointer;
484
- }
485
-
486
- .crumb-item:has(details.jump):hover,
487
- .crumb-item:has(details.jump[open]) { background: var(--color-chrome); }
488
-
489
- /* Accent reports where you are. A crumb is navigation you can take, so it is
490
- neutral, and \`.mast a\` keeps the accent for any other bar link. */
491
- .mast a.crumb { color: var(--color-text-secondary); padding: 0.1rem 0.15rem 0.1rem 0; }
492
- .mast a.crumb:hover { text-decoration: none; }
493
- .crumb-item:hover a.crumb { color: var(--color-text); }
494
-
495
- .crumb-here { color: var(--color-text); font-weight: 650; }
496
-
497
- /* \`align-items: center\` centres boxes, and a label's line box carries
498
- half-leading above the cap and descender room below, so its capitals sit
499
- above its own centre by an amount that scales with the font size. Trimming
500
- the label to its cap band fixes the box rather than nudging the mark. It
501
- lands on a span holding the text alone, since the link's own \`inline-flex\`
502
- ignores it. Firefox ships no \`text-box\` and keeps the caret a pixel low. */
503
- .crumb-t, .crumb-here, .sb-ws .ws-name {
504
- display: block;
505
- text-box: trim-both cap alphabetic;
506
- }
507
-
508
- /* The caret is the whole control, so it carries the 24 pixel minimum on its
509
- own rather than inheriting a label's width. It fills nothing itself, since
510
- the chip around it does. */
511
- .crumb-item > .jump > summary {
512
- min-width: 1.5rem;
513
- min-height: 1.5rem;
514
- display: inline-flex;
515
- align-items: center;
516
- justify-content: center;
517
- padding: 0.15rem 0.3rem 0.15rem 0;
518
- border: 0;
519
- background: transparent;
520
- }
521
-
522
- /* A bar link is its own tap target rather than a line of text, so it carries
523
- the 24 pixel minimum the keyboard rule sets. */
524
- .mast a,
525
- .mast summary {
526
- min-height: 1.6rem;
527
- display: inline-flex;
528
- align-items: center;
529
- }
530
-
531
- .mast .pos {
532
- font-family: var(--teach-mono);
533
- font-size: 0.75rem;
534
- color: var(--color-muted);
535
- }
536
-
537
- .mast-right { display: flex; align-items: center; gap: 0.2rem; margin-left: auto; }
538
-
539
- /* No border, at the fold control's size and radius, so every control on the
540
- bar is drawn one way. */
541
- .theme {
542
- width: 1.75rem;
543
- height: 1.75rem;
544
- display: inline-flex;
545
- align-items: center;
546
- justify-content: center;
547
- padding: 0;
548
- border: 0;
549
- border-radius: 6px;
550
- background: transparent;
551
- color: var(--color-muted);
552
- cursor: pointer;
553
- }
554
-
555
- .theme:hover { color: var(--color-text); background: var(--color-chrome); }
556
- .theme svg { width: 0.95rem; height: 0.95rem; display: block; }
557
- .theme .moon { display: none; }
558
- :root[data-theme="dark"] .theme .sun { display: none; }
559
- :root[data-theme="dark"] .theme .moon { display: block; }
560
-
561
- @media (prefers-color-scheme: dark) {
562
- :root:not([data-theme="light"]) .theme .sun { display: none; }
563
- :root:not([data-theme="light"]) .theme .moon { display: block; }
564
- }
565
-
566
- /* ---- The course sidebar ---- */
567
-
568
- /* A segmented progress strip used to sit here, one \`flex: 1\` segment per
569
- lesson, and it stopped working at the scale a reader most needs it: fifty
570
- lessons rendered as a row of dots. The sidebar reports course position
571
- instead, at any length, and the bar reports position inside the lesson. */
572
-
573
- /* The toggle in the masthead, left of the breadcrumb. */
574
- .sb-fold {
575
- flex: none;
576
- width: 1.75rem;
577
- height: 1.75rem;
578
- margin-right: 0.35rem;
579
- font: inherit;
580
- font-size: var(--t5);
581
- line-height: 1;
582
- color: var(--color-muted);
583
- background: transparent;
584
- border: 0;
585
- border-radius: 6px;
586
- cursor: pointer;
587
- }
588
-
589
- .sb-fold:hover { color: var(--color-text); background: var(--color-chrome); }
590
-
591
- .sb {
592
- flex: 0 0 var(--sb-w, 16rem);
593
- width: var(--sb-w, 16rem);
594
- position: sticky;
595
- top: 0;
596
- align-self: flex-start;
597
- height: 100vh;
598
- display: flex;
599
- flex-direction: column;
600
- overflow: visible;
601
- background: transparent;
602
- font-family: var(--teach-sans);
603
- transition: flex-basis 0.14s, width 0.14s;
604
- }
605
-
606
- html.sb-shut .sb { flex-basis: 0; width: 0; overflow: hidden; }
607
-
608
- .sb-top {
609
- flex: none;
610
- display: flex;
611
- align-items: center;
612
- height: var(--teach-mast-h);
613
- padding: 0 0.95rem;
614
- border-bottom: 1px solid var(--color-border);
615
- }
616
-
617
- .sb-ws { position: relative; }
618
-
619
- .sb-ws > summary {
620
- list-style: none;
621
- cursor: pointer;
622
- display: inline-flex;
623
- align-items: center;
624
- gap: 0.35rem;
625
- padding: 0.25rem 0.3rem;
626
- border-radius: 6px;
627
- font-size: var(--t4);
628
- font-weight: 650;
629
- color: var(--color-text);
630
- }
631
-
632
- .sb-ws > summary::-webkit-details-marker { display: none; }
633
- .sb-ws > summary:hover, .sb-ws[open] > summary { background: var(--color-chrome); color: var(--color-text); }
634
- .sb-ws .car { color: var(--color-muted); display: inline-flex; align-items: center; }
635
-
636
- /* The top band is as tall as the masthead and a mission title can run to a
637
- sentence, so the name stops at two lines rather than overflowing the rule.
638
- Nav carries the whole title in the element's \`title\` attribute. The cap
639
- trim the crumb labels share is dropped here, since it ends the box at the
640
- last baseline and the clip would cut every descender on the second line. */
641
- .sb-ws { min-width: 0; }
642
- .sb-ws .ws-name {
643
- display: -webkit-box;
644
- -webkit-box-orient: vertical;
645
- -webkit-line-clamp: 2;
646
- line-clamp: 2;
647
- overflow: hidden;
648
- min-width: 0;
649
- line-height: 1.2;
650
- text-box: normal;
651
- }
652
-
653
- .sb-wl {
654
- position: absolute;
655
- top: calc(100% + 0.35rem);
656
- left: 0;
657
- z-index: 40;
658
- min-width: 13rem;
659
- background: var(--color-background);
660
- border: 1px solid var(--color-border);
661
- border-radius: 9px;
662
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
663
- padding: 0.3rem;
664
- }
665
-
666
- :root[data-theme="dark"] .sb-wl { box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); }
667
-
668
- /* The popover chrome comes from \`.sb-wl\`, so the list nested inside it drops
669
- its own rather than drawing a second border inside the first. */
670
- .sb-wl .jump-list {
671
- position: static;
672
- min-width: 0;
673
- max-width: none;
674
- border: 0;
675
- box-shadow: none;
676
- padding: 0;
677
- background: transparent;
678
- }
679
-
680
- .sb-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-bottom: 1rem; }
681
-
682
- .sb-meta {
683
- padding: 1rem 1.25rem 0.55rem;
684
- color: var(--color-muted);
685
- font-size: var(--t6);
686
- font-variant-numeric: tabular-nums;
687
- }
688
-
689
- .sb-filter { padding: 0 0.95rem 0.6rem; }
690
-
691
- .sb-filter input {
692
- width: 100%;
693
- box-sizing: border-box;
694
- font: inherit;
695
- font-size: var(--t6);
696
- color: var(--color-text);
697
- background: transparent;
698
- border: 1px solid var(--color-border);
699
- border-radius: 7px;
700
- padding: 0.3rem 0.55rem;
701
- }
702
-
703
- .sb-filter input:focus { outline: none; border-color: var(--color-accent); }
704
-
705
- .sb-list { list-style: none; margin: 0; padding: 0 0.7rem; }
706
- .sb-list li { margin: 0; }
707
- .sb-list li.hide { display: none; }
708
-
709
- .sb-l {
710
- position: relative;
711
- display: flex;
712
- gap: 0.55rem;
713
- align-items: baseline;
714
- padding: 0.38rem 0.55rem;
715
- border-radius: 6px;
716
- text-decoration: none;
717
- color: var(--color-text-body);
718
- font-size: var(--t5);
719
- line-height: 1.45;
720
- }
721
-
722
- .sb-l:hover { background: var(--color-chrome); color: var(--color-text); }
723
-
724
- .sb-n {
725
- flex: none;
726
- font-family: var(--teach-mono);
727
- font-size: var(--t6);
728
- color: var(--color-muted);
729
- font-variant-numeric: tabular-nums;
730
- }
731
-
732
- .sb-on { color: var(--color-text); font-weight: 650; }
733
-
734
- .sb-on::before {
735
- content: "";
736
- position: absolute;
737
- left: -0.6rem;
738
- top: 0.28rem;
739
- bottom: 0.28rem;
740
- width: 2px;
741
- border-radius: 2px;
742
- background: var(--color-accent);
743
- }
744
-
745
- .sb-on .sb-n { color: var(--color-accent); }
746
-
747
- .sb-empty { padding: 0.3rem 1.2rem; color: var(--color-muted); font-size: var(--t6); }
748
-
749
- .sb-foot {
750
- flex: none;
751
- padding: 0.7rem 1.25rem;
752
- border-top: 1px solid var(--color-border);
753
- color: var(--color-muted);
754
- font-size: var(--t6);
755
- font-variant-numeric: tabular-nums;
756
- }
757
-
758
- /* The panel is resizable between a width that still fits a lesson title and one
759
- that would start eating the reading measure. */
760
- .sb-grip {
761
- position: absolute;
762
- top: 0;
763
- bottom: 0;
764
- right: -3px;
765
- width: 7px;
766
- z-index: 25;
767
- padding: 0;
768
- border: 0;
769
- background: transparent;
770
- cursor: col-resize;
771
- touch-action: none;
772
- }
773
-
774
- .sb-grip::before {
775
- content: "";
776
- position: absolute;
777
- left: 3px;
778
- top: 50%;
779
- transform: translateY(-50%);
780
- width: 1px;
781
- height: 2.2rem;
782
- border-radius: 1px;
783
- background: var(--color-border);
784
- opacity: 0;
785
- transition: opacity 0.12s;
786
- }
787
-
788
- .sb-grip:hover::before, .sb-grip:focus-visible::before, html.sb-drag .sb-grip::before {
789
- opacity: 1;
790
- background: var(--color-accent);
791
- }
792
-
793
- .sb-grip:focus-visible { outline: none; }
794
- html.sb-drag { cursor: col-resize; user-select: none; }
795
- html.sb-shut .sb-grip { display: none; }
796
-
797
- /* Both belong to the overlay and the script appends both on every page, so
798
- each is hidden until the breakpoint hands it a job. Without this the close
799
- control renders as an unstyled button stretched across the column, and the
800
- scrim becomes a flex item of the body. */
801
- .sb-close, .sb-scrim { display: none; }
802
-
803
- /* ---- Footer navigation, the only place it appears ---- */
804
-
805
- .nav {
806
- max-width: calc(var(--teach-measure) + 3rem);
807
- margin: 4rem auto 0;
808
- padding: 0 1.5rem;
809
- box-sizing: border-box;
810
- align-items: stretch;
811
- }
812
-
813
- .nav > a, .nav > span { display: flex; flex-direction: column; justify-content: center; }
814
-
815
- .nav .arrow {
816
- font-family: var(--teach-mono);
817
- color: var(--color-muted);
818
- padding-right: 0.35rem;
819
- }
820
-
821
- .nav > a:hover .arrow { color: var(--color-accent); }
822
-
823
- /* ---- Jump menu: random access, where the foot nav is sequential ---- */
824
-
825
- .jump { position: relative; }
826
-
827
- .jump summary {
828
- list-style: none;
829
- cursor: pointer;
830
- font-family: var(--teach-mono);
831
- font-size: 0.75rem;
832
- color: var(--color-muted);
833
- padding: 0.25rem 0.55rem;
834
- border: 1px solid transparent;
835
- border-radius: 6px;
836
- display: inline-flex;
837
- align-items: center;
838
- gap: 0.4rem;
839
- }
840
-
841
- .jump summary::-webkit-details-marker { display: none; }
842
- /* The caret says "this is a menu" and nothing else, so it is drawn at full
843
- strength in every state and stays muted. A reveal on hover hid the only
844
- route into the menu from a reader who cannot hover, and an affordance is not
845
- a state, so it takes no accent on hover or while open. */
846
- .jump summary .caret { color: var(--color-muted); }
847
-
848
- .jump-list {
849
- position: absolute;
850
- top: calc(100% + 0.4rem);
851
- left: 0;
852
- z-index: 20;
853
- background: var(--color-background);
854
- border: 1px solid var(--color-border);
855
- border-radius: 9px;
856
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
857
- padding: 0.25rem;
858
- list-style: none;
859
- margin: 0;
860
- }
861
-
862
- :root[data-theme="dark"] .jump-list { box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); }
863
-
864
- /* One row recipe wherever the menu is mounted, on the steps the lesson list
865
- beside it already reads: the label at \`--t5\` and the numeral at \`--t6\` mono.
866
- The weight is restated because \`.mast a\` sets 600 for the bar's own links. */
867
- .jump-list a {
868
- display: grid;
869
- grid-template-columns: 1.65rem 1fr auto;
870
- gap: 0.55rem;
871
- align-items: center;
872
- padding: 0.38rem 0.55rem;
873
- border-radius: 6px;
874
- text-decoration: none;
875
- color: var(--color-text-body);
876
- font-size: var(--t5);
877
- font-weight: 400;
878
- line-height: 1.45;
879
- }
880
-
881
- .jump-list a:hover { background: var(--color-chrome); }
882
- .jump-list .n { font-family: var(--teach-mono); font-size: var(--t6); color: var(--color-muted); }
883
- /* The third column used to hold a status dot that read the same on every row,
884
- so it reported nothing. It is a per-entry trailing slot now: the workspace
885
- menu puts a lesson count there and the lesson menu leaves it empty. */
886
- .jump-list .ct {
887
- font-family: var(--teach-mono);
888
- font-size: var(--t6);
889
- color: var(--color-muted);
890
- font-variant-numeric: tabular-nums;
891
- text-align: right;
892
- }
893
-
894
- .jump-list li.at a { background: var(--color-teach-accent-bg); }
895
- .jump-list li.at .n { color: var(--color-accent); }
896
- .jump-list li.soon a { color: var(--color-muted); }
897
-
898
- .jump.ws summary { color: var(--color-text-secondary); }
899
-
900
- .jump-list li.all { border-top: 1px solid var(--color-border); margin-top: 0.25rem; padding-top: 0.25rem; }
901
- .jump-list li.all a { color: var(--color-accent); }
902
- .jump-list li.all .n { color: var(--color-accent); }
903
-
904
- /* The menu fits its content above an 11rem floor rather than padding three
905
- short titles out to a wide panel. It is anchored to its summary, so the
906
- clamp keeps a long row wrapping on screen without a breakpoint. */
907
- .jump-list {
908
- min-width: min(11rem, calc(100vw - 2rem));
909
- max-width: calc(100vw - 2rem);
910
- }
911
-
912
- /* The route back to the full listing sits above the workspaces it lists. */
913
- .jump-list li.head a { color: var(--color-text-secondary); font-weight: 600; }
914
- .jump-list li.head { border-bottom: 1px solid var(--color-border); margin-bottom: 0.3rem; padding-bottom: 0.3rem; }
915
-
916
- /* An unwritten lesson and a stub workspace are inert, so they render inert.
917
- Reduced opacity alone reads as a rendering fault, so the cursor says it too. */
918
- .jump-list li.soon a,
919
- .toc li.soon a,
920
- [aria-disabled="true"] {
921
- opacity: 0.55;
922
- cursor: not-allowed;
923
- pointer-events: none;
924
- }
925
-
926
- @media (max-width: 640px) {
927
- .bar { padding: 0 1rem; }
928
- main { padding-left: 1rem; padding-right: 1rem; }
929
- .mast { font-size: 0.75rem; }
930
- }
931
-
932
- @media (max-width: 640px) {
933
- .jump-list {
934
- position: fixed;
935
- left: 1rem;
936
- right: 1rem;
937
- top: auto;
938
- min-width: 0;
939
- max-width: none;
940
- }
941
- }
942
-
943
- /* ---- What gives way below 1100px ---- */
944
-
945
- /* Hiding the sidebar outright leaves the toggle painted exactly where it cannot
946
- act, and it deletes the filter and the per-lesson outline, which the
947
- breadcrumb caret does not carry. The panel becomes an overlay instead, and it
948
- states its own ground because \`.sb\` is transparent by design for a column
949
- sitting on the page rather than floating over the lesson. */
950
- @media (max-width: ${TEACH_SIDEBAR_BREAKPOINT}px) {
951
- .pane { border-left: 0; }
952
-
953
- .sb {
954
- position: fixed;
955
- top: 0;
956
- left: 0;
957
- bottom: 0;
958
- z-index: 60;
959
- width: min(21rem, 86vw);
960
- height: 100dvh;
961
- overflow-y: auto;
962
- background: var(--color-background);
963
- border-right: 1px solid var(--color-border);
964
- box-shadow: 0 0 40px rgb(0 0 0 / 35%);
965
- transform: translateX(-100%);
966
- transition: transform 0.18s;
967
- }
968
-
969
- html:not(.sb-shut) .sb { transform: none; }
970
-
971
- /* The wide rule collapses a shut panel to \`width: 0\` and outranks a bare
972
- \`.sb\` in here, which produced a one-pixel panel whose contents spilled out
973
- and intercepted every click on the toggle. The shut state restates the
974
- width so the transform is what moves the panel off screen. */
975
- html.sb-shut .sb { width: min(21rem, 86vw); flex-basis: auto; overflow-y: auto; }
976
-
977
- /* A transform moves the panel out of sight and leaves every control inside it
978
- in the tab order, so the first Tab from a shut page landed on the workspace
979
- switcher inside an invisible panel. Visibility takes it out of the
980
- sequence, delayed on the way out so the slide still shows. */
981
- html.sb-shut .sb { visibility: hidden; transition: transform 0.18s, visibility 0s linear 0.18s; }
982
- html:not(.sb-shut) .sb { visibility: visible; transition: transform 0.18s, visibility 0s; }
983
-
984
- .sb-grip { display: none; }
985
-
986
- /* The panel covers the masthead and the toggle that opened it, so it carries
987
- its own way out rather than leaving the scrim as the only route on a phone.
988
- It carries no border, at 1.75rem to match \`.sb-fold\` and \`.theme\`, which also clears
989
- the 24 pixel pointer target floor. */
990
- .sb-close {
991
- display: inline-flex;
992
- align-items: center;
993
- justify-content: center;
994
- position: absolute;
995
- top: 0.55rem;
996
- right: 0.6rem;
997
- z-index: 61;
998
- appearance: none;
999
- width: 1.75rem;
1000
- height: 1.75rem;
1001
- font: inherit;
1002
- font-size: var(--t5);
1003
- line-height: 1;
1004
- color: var(--color-muted);
1005
- background: transparent;
1006
- border: 0;
1007
- border-radius: 6px;
1008
- cursor: pointer;
1009
- }
1010
-
1011
- .sb-close:hover { color: var(--color-text); background: var(--color-chrome); }
1012
-
1013
- .sb-scrim {
1014
- display: block;
1015
- position: fixed;
1016
- inset: 0;
1017
- z-index: 55;
1018
- background: rgb(0 0 0 / 45%);
1019
- opacity: 0;
1020
- pointer-events: none;
1021
- transition: opacity 0.18s;
1022
- }
1023
-
1024
- html:not(.sb-shut) .sb-scrim { opacity: 1; pointer-events: auto; }
1025
- }
1026
-
1027
- @media (prefers-reduced-motion: reduce) {
1028
- .sb, .sb-scrim, .sb-grip::before { transition: none; }
1029
- }`,
1030
- }
1031
-
1032
- const TEACH_ARTICLE: Component = {
1033
- name: 'teach-article',
1034
- note: [
1035
- 'The base reading-column styles a teach page renders prose in: type, panels,',
1036
- 'code, callouts, tables, the workspace contents listing, and the focus ring.',
1037
- 'Recovered alongside teach-chrome from the same two files.',
1038
- ].join('\n '),
1039
- reads: [
1040
- '--color-border',
1041
- '--color-surface',
1042
- '--color-text',
1043
- '--color-text-body',
1044
- '--color-text-secondary',
1045
- '--color-muted',
1046
- '--color-accent',
1047
- '--teach-hand',
1048
- '--teach-mono',
1049
- ...TEACH_SYNTAX.map(({ name }) => syntaxProperty(name)),
1050
- '--t1',
1051
- '--t2',
1052
- '--t3',
1053
- '--t4',
1054
- '--t5',
1055
- '--t6',
1056
- ],
1057
- rules: `/* ---- Type ---- */
1058
-
1059
- h1 {
1060
- font-size: var(--t1);
1061
- line-height: 1.1;
1062
- font-weight: 800;
1063
- letter-spacing: -0.022em;
1064
- margin: 0 0 0.55rem;
1065
- text-wrap: balance;
1066
- }
1067
-
1068
- h2 {
1069
- font-size: var(--t2);
1070
- line-height: 1.3;
1071
- font-weight: 700;
1072
- letter-spacing: -0.012em;
1073
- margin: 3rem 0 0.85rem;
1074
- }
1075
-
1076
- h3 { font-size: var(--t3); line-height: 1.55; font-weight: 700; margin: 1.9rem 0 0.4rem; }
1077
-
1078
- p { margin: 0 0 1.15rem; }
1079
-
1080
- .lede {
1081
- font-size: var(--t3);
1082
- line-height: 1.55;
1083
- color: var(--color-text-secondary);
1084
- margin-bottom: 2rem;
1085
- }
1086
-
1087
- a { color: var(--color-accent); }
1088
-
1089
- em { font-style: italic; }
1090
-
1091
- /* ---- Panels ---- */
1092
-
1093
- .assumes, .progress {
1094
- font-size: var(--t5);
1095
- line-height: 1.6;
1096
- color: var(--color-text-secondary);
1097
- background: var(--color-surface);
1098
- border-radius: 7px;
1099
- padding: 0.9rem 1.15rem;
1100
- margin: 0 0 2.5rem;
1101
- }
1102
-
1103
- .assumes b, .progress b {
1104
- display: block;
1105
- color: var(--color-muted);
1106
- font-weight: 700;
1107
- letter-spacing: 0;
1108
- font-size: var(--t6);
1109
- margin-bottom: 0.2rem;
1110
- }
1111
-
1112
- /* ---- Code ---- */
1113
-
1114
- code {
1115
- font-family: var(--teach-mono);
1116
- font-size: 0.85em;
1117
- background: var(--color-surface);
1118
- padding: 0.13em 0.38em;
1119
- border-radius: 4px;
1120
- }
1121
-
1122
- pre {
1123
- font-family: var(--teach-mono);
1124
- font-size: var(--t5);
1125
- line-height: 1.6;
1126
- background: var(--color-surface);
1127
- padding: 1.15rem 1.3rem;
1128
- border-radius: 7px;
1129
- overflow-x: auto;
1130
- margin: 0 0 1.5rem;
1131
- }
1132
-
1133
- pre code { background: none; padding: 0; font-size: 1em; }
1134
-
1135
- /* Build-time highlight.js spans. Every class reads a --teach-syntax-* token,
1136
- so the dark and light values switch with the ground rather than per rule. */
1137
- .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-built_in, .hljs-doctag { color: var(--teach-syntax-keyword); }
1138
- .hljs-string, .hljs-regexp, .hljs-addition, .hljs-template-tag, .hljs-symbol { color: var(--teach-syntax-string); }
1139
- .hljs-number, .hljs-type, .hljs-selector-class, .hljs-selector-id { color: var(--teach-syntax-number); }
1140
- .hljs-title, .hljs-section, .hljs-name, .hljs-bullet { color: var(--teach-syntax-title); }
1141
- .hljs-attr, .hljs-attribute, .hljs-variable, .hljs-template-variable, .hljs-property { color: var(--teach-syntax-attr); }
1142
- .hljs-comment, .hljs-quote { color: var(--teach-syntax-comment); font-style: italic; }
1143
- .hljs-meta, .hljs-deletion, .hljs-subst { color: var(--teach-syntax-meta); }
1144
- .hljs-emphasis { font-style: italic; }
1145
- .hljs-strong { font-weight: 700; }
1146
-
1147
- /* ---- Callout ---- */
1148
-
1149
- .hard {
1150
- background: var(--color-teach-accent-bg);
1151
- border-radius: 7px;
1152
- padding: 1.1rem 1.3rem;
1153
- margin: 2rem 0;
1154
- }
1155
-
1156
- .hard-label {
1157
- font-family: var(--teach-hand);
1158
- font-size: var(--t3);
1159
- color: var(--color-accent);
1160
- display: block;
1161
- margin-bottom: 0.35rem;
1162
- }
1163
-
1164
- .hard p:last-child { margin-bottom: 0; }
1165
-
1166
- /* ---- Tables ---- */
1167
-
1168
- table { width: 100%; border-collapse: collapse; font-size: var(--t4); margin: 0 0 1.5rem; }
1169
- th, td { text-align: right; padding: 0.55rem 0.7rem; border-bottom: 1px solid var(--color-border); }
1170
- th:first-child, td:first-child { text-align: left; }
1171
-
1172
- th {
1173
- font-weight: 700;
1174
- font-size: var(--t5);
1175
- letter-spacing: 0;
1176
- color: var(--color-muted);
1177
- border-bottom-color: var(--color-border);
1178
- }
1179
-
1180
- tbody tr.mark td { background: var(--color-teach-accent-bg); }
1181
- .scroll { overflow-x: auto; }
1182
-
1183
- /* ---- Contents ---- */
1184
-
1185
- .toc { list-style: none; padding: 0; margin: 0 0 2.5rem; }
1186
- .toc li + li { border-top: 1px solid var(--color-border); }
1187
-
1188
- .toc a {
1189
- display: grid;
1190
- grid-template-columns: 2.25rem 1fr auto;
1191
- gap: 0 1.1rem;
1192
- align-items: baseline;
1193
- padding: 1rem 0.7rem;
1194
- text-decoration: none;
1195
- color: var(--color-text-body);
1196
- border-radius: 7px;
1197
- }
1198
-
1199
- .toc a:hover { background: var(--color-surface); }
1200
- .toc .num { font-family: var(--teach-mono); font-size: var(--t5); color: var(--color-muted); }
1201
- .toc b { font-size: var(--t3); font-weight: 700; letter-spacing: -0.012em; }
1202
-
1203
- .toc .blurb {
1204
- grid-column: 2;
1205
- font-size: var(--t4);
1206
- line-height: 1.55;
1207
- color: var(--color-text-secondary);
1208
- margin-top: 0.1rem;
1209
- }
1210
-
1211
- /* A word, not a pill and not a dot. A mark reading the same on every live row
1212
- reports nothing, so the dot retired here as it did from the jump menus.
1213
- Sentence case and no tracking, per the UI copy rule. */
1214
- .toc .state {
1215
- font-size: var(--t5);
1216
- font-weight: 500;
1217
- letter-spacing: 0;
1218
- text-transform: none;
1219
- color: var(--color-muted);
1220
- white-space: nowrap;
1221
- }
1222
-
1223
- /* A link leaving the page says so with a mark beside it rather than with a
1224
- badge spelling out the sentence. */
1225
- .toc .ext {
1226
- font-size: var(--t4);
1227
- color: var(--color-muted);
1228
- margin-left: 0.35rem;
1229
- }
1230
-
1231
- .toc a:hover .ext { color: var(--color-accent); }
1232
-
1233
- ol.succ {
1234
- margin: 0 0 2.5rem;
1235
- padding-left: 1.35rem;
1236
- font-size: var(--t4);
1237
- line-height: 1.55;
1238
- color: var(--color-text-secondary);
1239
- }
1240
-
1241
- ol.succ li { margin-bottom: 0.4rem; }
1242
- .toc li.soon b { color: var(--color-muted); font-weight: 400; }
1243
-
1244
- /* ---- Focus is one token, and it is the accent ---- */
1245
-
1246
- :where(a, button, summary, input, .opt):focus-visible {
1247
- outline: 2px solid var(--color-accent);
1248
- outline-offset: 2px;
1249
- border-radius: 5px;
1250
- }
1251
-
1252
- /* A summary carries its own border, so the ring sits outside it rather than
1253
- doubling the box. */
1254
- .jump summary:focus-visible { outline-offset: 1px; }
1255
-
1256
- /* The browser default ring is white on a dark ground, which reads as a second
1257
- accent. Suppressing it only where the token above replaces it. */
1258
- :where(a, button, summary, input, .opt):focus:not(:focus-visible) { outline: none; }
1259
-
1260
- /* The week plan. Its markup shipped with no rule matching any of its five
1261
- classes, so it rendered as a run of inline spans with the ordinal, the
1262
- title, the description, and the duration all touching. */
1263
-
1264
- .road { margin: 1.75rem 0 2.5rem; }
1265
-
1266
- .road-row {
1267
- display: grid;
1268
- grid-template-columns: 1.75rem 1fr auto;
1269
- gap: 0.9rem;
1270
- align-items: baseline;
1271
- padding: 0.85rem 0;
1272
- border-bottom: 1px solid var(--color-border);
1273
- }
1274
-
1275
- .road-row:last-child { border-bottom: 0; }
1276
-
1277
- .road-n {
1278
- font-family: var(--teach-mono);
1279
- font-size: var(--t5);
1280
- color: var(--color-accent);
1281
- }
1282
-
1283
- .road-b { display: block; }
1284
- .road-b b { display: block; margin-bottom: 0.2rem; }
1285
- .road-b span { color: var(--color-text-secondary); font-size: var(--t4); line-height: 1.55; }
1286
-
1287
- .road-t {
1288
- font-family: var(--teach-mono);
1289
- font-size: var(--t5);
1290
- color: var(--color-muted);
1291
- white-space: nowrap;
1292
- }
1293
-
1294
- /* The closing line is a paragraph rather than a row, so it keeps the column
1295
- the rows set rather than sitting flush against the measure. */
1296
- .road + p > b:first-child { color: var(--color-text); }
1297
-
1298
- @media (max-width: 640px) {
1299
- .road-row { grid-template-columns: 1.5rem 1fr; }
1300
- .road-t { grid-column: 2; }
1301
- }`,
1302
- }
1303
-
1304
- const TEACH_QUIZ: Component = {
1305
- name: 'teach-quiz',
1306
- note: [
1307
- 'The recognition quiz a lesson embeds: the option list, its right/wrong/chosen',
1308
- 'states, and the feedback block. `canon teach nav` layers a small stepper block',
1309
- 'of its own on top of this for the non-legacy option shape.',
1310
- ].join('\n '),
1311
- reads: [
1312
- '--color-border',
1313
- '--color-surface',
1314
- '--color-text',
1315
- '--color-text-secondary',
1316
- '--color-muted',
1317
- '--color-accent',
1318
- '--teach-sans',
1319
- '--teach-mono',
1320
- '--teach-shadow',
1321
- '--color-teach-accent-bg',
1322
- '--t4',
1323
- '--t5',
1324
- '--t6',
1325
- ],
1326
- rules: `/* ---- Quiz ---- */
1327
-
1328
- .quiz { margin-top: 3rem; border-top: 1px solid var(--color-border); padding-top: 1.75rem; }
1329
- .quiz > h2:first-child { margin-top: 0; }
1330
- .q { margin: 0 0 2.5rem; }
1331
- .q-stem { font-weight: 700; margin-bottom: 0.85rem; }
1332
-
1333
- .opt {
1334
- display: flex;
1335
- gap: 0.75rem;
1336
- align-items: baseline;
1337
- border: 1px solid var(--color-border);
1338
- border-radius: 8px;
1339
- padding: 0.7rem 0.95rem;
1340
- margin-bottom: 0.45rem;
1341
- cursor: pointer;
1342
- font-family: var(--teach-sans);
1343
- font-size: var(--t4);
1344
- background: transparent;
1345
- box-sizing: border-box;
1346
- width: 100%;
1347
- text-align: left;
1348
- color: inherit;
1349
- line-height: 1.45;
1350
- box-shadow: var(--teach-shadow);
1351
- }
1352
-
1353
- .opt::before {
1354
- content: attr(data-k);
1355
- font-family: var(--teach-mono);
1356
- font-size: var(--t6);
1357
- color: var(--color-muted);
1358
- border: 1px solid var(--color-border);
1359
- border-radius: 5px;
1360
- padding: 0.05rem 0.38rem;
1361
- flex: 0 0 auto;
1362
- }
1363
-
1364
- .opt:hover { border-color: var(--color-accent); background: var(--color-teach-accent-bg); }
1365
- .opt[data-state="right"] { border-color: var(--color-accent); background: var(--color-teach-accent-bg); }
1366
- .opt[data-state="right"]::before { color: var(--color-accent); border-color: var(--color-accent); }
1367
- .opt[data-state="wrong"] { opacity: 0.38; }
1368
-
1369
- /* The option the learner actually picked stays legible and says so.
1370
- A wrong pick that fades like an option nobody considered destroys the
1371
- contrast between what they thought and what is true, which is the
1372
- moment the correction lands. */
1373
-
1374
- .opt[data-state="chosen"] {
1375
- opacity: 1;
1376
- border-color: var(--color-muted);
1377
- border-style: dashed;
1378
- }
1379
-
1380
- .opt[data-state="chosen"]::after {
1381
- content: "Your answer";
1382
- margin-left: auto;
1383
- font-size: var(--t6);
1384
- font-weight: 700;
1385
- letter-spacing: 0;
1386
- color: var(--color-muted);
1387
- align-self: center;
1388
- }
1389
-
1390
- .opt[data-state="right"]::after {
1391
- content: "Correct";
1392
- margin-left: auto;
1393
- font-size: var(--t6);
1394
- font-weight: 700;
1395
- letter-spacing: 0;
1396
- color: var(--color-accent);
1397
- align-self: center;
1398
- }
1399
-
1400
- .fb {
1401
- display: none;
1402
- font-size: var(--t4);
1403
- line-height: 1.55;
1404
- color: var(--color-text-secondary);
1405
- background: var(--color-surface);
1406
- border-radius: 7px;
1407
- padding: 0.85rem 1.05rem;
1408
- margin-top: 0.55rem;
1409
- }
1410
-
1411
- .fb.show { display: block; }
1412
- .fb b { color: var(--color-text); font-weight: 700; }
1413
-
1414
- footer {
1415
- margin-top: 4rem;
1416
- padding-top: 1.4rem;
1417
- border-top: 1px solid var(--color-border);
1418
- font-size: var(--t5);
1419
- line-height: 1.6;
1420
- color: var(--color-muted);
1421
- }`,
1422
- }
1423
-
1424
- const TEACH_GLOSSARY: Component = {
1425
- name: 'teach-glossary',
1426
- note: [
1427
- 'The filterable glossary view a workspace root carries: the term count, the',
1428
- 'filter input, and the scrolling term list.',
1429
- ].join('\n '),
1430
- reads: [
1431
- '--color-background',
1432
- '--color-border',
1433
- '--color-text-body',
1434
- '--color-text-secondary',
1435
- '--color-muted',
1436
- '--color-accent',
1437
- '--teach-sans',
1438
- '--teach-mono',
1439
- '--t4',
1440
- '--t5',
1441
- ],
1442
- rules: `/* ---- Glossary view, generated from the markdown source ---- */
1443
-
1444
- /* No border, sitting on the page ground, with the count beside it. The border and the
1445
- reading surface are what made the control look like something to fill in,
1446
- and the count is what says it filters the list under it. */
1447
- .filter-row {
1448
- display: flex;
1449
- align-items: baseline;
1450
- gap: 1rem;
1451
- margin: 0 0 1rem;
1452
- }
1453
-
1454
- .filter {
1455
- flex: 1;
1456
- min-width: 0;
1457
- font-family: var(--teach-sans);
1458
- font-size: var(--t4);
1459
- color: var(--color-text-body);
1460
- background: transparent;
1461
- border: 0;
1462
- padding: 0.3rem 0;
1463
- }
1464
-
1465
- .filter-count {
1466
- font-family: var(--teach-mono);
1467
- font-size: var(--t5);
1468
- color: var(--color-muted);
1469
- white-space: nowrap;
1470
- }
1471
-
1472
- /* The \`:where(...)\` focus token has zero specificity, so a \`.filter:focus\`
1473
- rule outranks it and leaves a keyboard reaching the control with no ring.
1474
- The ring stays the token's and only the pointer ring is dropped. */
1475
- .filter:focus:not(:focus-visible) { outline: none; }
1476
- .filter::placeholder { color: var(--color-muted); }
1477
-
1478
- /* A fixed scroll region rather than a collapsing list. Filtering a list that
1479
- sets page height makes the page jump under the reader, and the same fixed
1480
- region is what lets the glossary grow past what one screen shows without
1481
- the page growing with it. */
1482
-
1483
- /* A scroll region is a tab stop in its own right, so it takes the same ring
1484
- as everything else rather than the browser's white default. */
1485
- .gloss:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; border-radius: 5px; }
1486
-
1487
- .gloss {
1488
- height: clamp(14rem, 46vh, 32rem);
1489
- overflow-y: auto;
1490
- margin-bottom: 2.5rem;
1491
- border-bottom: 1px solid var(--color-border);
1492
- scrollbar-width: thin;
1493
- }
1494
-
1495
- /* Scrollbar styling is global rather than per component. It was scoped to the
1496
- glossary, so the outline rail, the code blocks, and the scrolling tables all
1497
- fell back to the browser default, which paints a light track on a dark page
1498
- and is the one element on screen taking no color from these tokens. */
1499
-
1500
- .gloss .empty {
1501
- display: none;
1502
- padding: 1.2rem 0;
1503
- color: var(--color-muted);
1504
- font-size: var(--t4);
1505
- }
1506
-
1507
- .gloss.none .empty { display: block; }
1508
-
1509
- .gterm {
1510
- padding: 0.7rem 0;
1511
- border-bottom: 1px solid var(--color-border);
1512
- font-size: var(--t4);
1513
- line-height: 1.55;
1514
- }
1515
-
1516
- .gloss-group {
1517
- margin: 1.1rem 0 0.4rem;
1518
- font-family: var(--teach-sans);
1519
- font-size: var(--t5);
1520
- font-weight: 600;
1521
- color: var(--color-muted);
1522
- letter-spacing: 0;
1523
- }
1524
-
1525
- .gloss-group:first-of-type { margin-top: 0; }
1526
-
1527
- .gterm b { font-weight: 700; }
1528
- /* The markdown source separates a term from its definition with a colon,
1529
- and the house standard bans an em dash outright, so the view mirrors
1530
- the source rather than inventing punctuation for it. */
1531
- /* The space is part of the generated content, since the term and its
1532
- definition are adjacent elements with no whitespace between them in the
1533
- markup and the colon rendered flush against the first word. */
1534
- .gterm b::after { content: ": "; color: var(--color-muted); font-weight: 400; white-space: pre; }
1535
- .gterm span { color: var(--color-text-secondary); }
1536
-
1537
- ol.succ {
1538
- margin: 0 0 2.5rem;
1539
- padding-left: 1.35rem;
1540
- font-size: var(--t4);
1541
- line-height: 1.55;
1542
- color: var(--color-text-secondary);
1543
- }
1544
-
1545
- ol.succ li { margin-bottom: 0.4rem; }
1546
- .toc li.soon b { color: var(--color-muted); font-weight: 400; }
1547
-
1548
- /* A filter that matches nothing needs a way back, not just a sentence. */
1549
- .gloss .empty .clear {
1550
- font-family: var(--teach-sans);
1551
- font-size: var(--t4);
1552
- color: var(--color-accent);
1553
- background: none;
1554
- border: 0;
1555
- border-bottom: 1px solid currentColor;
1556
- padding: 0;
1557
- margin-left: 0.35rem;
1558
- cursor: pointer;
1559
- }`,
1560
- }
1561
-
1562
- const TEACH_OUTLINE: Component = {
1563
- name: 'teach-outline',
1564
- note: [
1565
- 'The in-lesson outline, folded under the lesson being read in the course',
1566
- 'sidebar. It used to be a fixed rail in the gutter the reading measure left',
1567
- 'over, which reported the same thing twice once the sidebar carried the',
1568
- 'course and disappeared entirely below 1420px.',
1569
- ].join('\n '),
1570
- reads: ['--color-text', '--color-text-secondary', '--color-accent', '--t6'],
1571
- rules: `/* ---- In-lesson outline, folded under the lesson being read ---- */
1572
-
1573
- /* The rail this replaces was \`position: fixed\` in the right-hand gutter, which
1574
- only existed above 1420px and reported nothing at all below it. Folding the
1575
- headings under the current lesson puts the course and the lesson in one
1576
- column, so a reader tracks both in one place at any window width. */
1577
- .sb-out { list-style: none; margin: 0.1rem 0 0.55rem; padding: 0 0 0 1.45rem; }
1578
-
1579
- .sb-out a {
1580
- display: block;
1581
- padding: 0.18rem 0.5rem;
1582
- text-decoration: none;
1583
- color: var(--color-text-secondary);
1584
- font-size: var(--t6);
1585
- }
1586
-
1587
- .sb-out a:hover { color: var(--color-text); }
1588
- .sb-out a.on { color: var(--color-accent); }`,
1589
- }
1590
-
1591
- const TEACH_REFERENCES: Component = {
1592
- name: 'teach-references',
1593
- note: [
1594
- 'The numbered citation marks and reference list a lesson closes with, and the',
1595
- 'rendered view of a promoted reference page.',
1596
- ].join('\n '),
1597
- reads: [
1598
- '--color-background',
1599
- '--color-border',
1600
- '--color-text-secondary',
1601
- '--color-muted',
1602
- '--color-accent',
1603
- '--teach-mono',
1604
- '--color-teach-accent-bg',
1605
- '--t5',
1606
- '--t6',
1607
- ],
1608
- rules: `footer {
1609
- margin-top: 4rem;
1610
- padding-top: 1.4rem;
1611
- border-top: 1px solid var(--color-border);
1612
- font-size: var(--t5);
1613
- line-height: 1.6;
1614
- color: var(--color-muted);
1615
- }
1616
-
1617
- /* ---- References, numbered rather than narrated ---- */
1618
-
1619
- sup.cite {
1620
- font-family: var(--teach-mono);
1621
- font-size: var(--t6);
1622
- line-height: 0;
1623
- margin-left: 0.1em;
1624
- }
1625
-
1626
- sup.cite a {
1627
- text-decoration: none;
1628
- padding: 0.05em 0.25em;
1629
- border-radius: 3px;
1630
- background: var(--color-teach-accent-bg);
1631
- color: var(--color-accent);
1632
- }
1633
-
1634
- sup.cite a:hover { background: var(--color-accent); color: var(--color-background); }
1635
-
1636
- ol.refs {
1637
- list-style: none;
1638
- counter-reset: r;
1639
- padding: 0;
1640
- margin: 0.9rem 0 1.1rem;
1641
- font-size: var(--t5);
1642
- line-height: 1.6;
1643
- }
1644
-
1645
- ol.refs li {
1646
- counter-increment: r;
1647
- position: relative;
1648
- padding-left: 2.1rem;
1649
- margin-bottom: 0.5rem;
1650
- }
1651
-
1652
- ol.refs li::before {
1653
- content: "[" counter(r) "]";
1654
- position: absolute;
1655
- left: 0;
1656
- font-family: var(--teach-mono);
1657
- color: var(--color-accent);
1658
- }
1659
-
1660
- ol.refs cite { font-style: italic; color: var(--color-text-secondary); }
1661
- ol.refs a { word-break: break-word; }
1662
-
1663
- footer .own {
1664
- margin: 0;
1665
- padding-top: 0.7rem;
1666
- border-top: 1px solid var(--color-border);
1667
- color: var(--color-muted);
1668
- }
1669
-
1670
- main.ref { padding-bottom: 4rem; }
1671
- main.ref h1 { margin-top: 1.5rem; }
1672
- main.ref h2 { margin-top: 2.5rem; }
1673
- main.ref ol, main.ref ul { padding-left: 1.4rem; }
1674
- main.ref li { margin-bottom: 0.5rem; }
1675
- main.ref blockquote {
1676
- margin: 1.5rem 0;
1677
- padding: 0.1rem 0 0.1rem 1.1rem;
1678
- border-left: 3px solid var(--color-border);
1679
- color: var(--color-text-secondary);
1680
- }`,
1681
- }
1682
-
1683
- export const TEACH_COMPONENTS: readonly Component[] = [
1684
- TEACH_CHROME,
1685
- TEACH_ARTICLE,
1686
- TEACH_QUIZ,
1687
- TEACH_GLOSSARY,
1688
- TEACH_OUTLINE,
1689
- TEACH_REFERENCES,
1690
- ]
1691
-
1692
- /**
1693
- * The generic components every design consumer gets, plus the teach chrome,
1694
- * for the one caller that wants both: a teach workspace stylesheet.
1695
- */
1696
- export const TEACH_STYLESHEET_COMPONENTS: readonly Component[] = [
1697
- ...COMPONENTS,
1698
- ...TEACH_COMPONENTS,
1699
- ]