@brandon_m_behring/book-scaffold-astro 3.0.0-alpha.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 (84) hide show
  1. package/CLAUDE.md +179 -0
  2. package/bin/book-scaffold.mjs +61 -0
  3. package/components/CaseStudy.astro +36 -0
  4. package/components/ChapterHeader.astro +61 -0
  5. package/components/ChapterNav.astro +29 -0
  6. package/components/ChapterTOC.astro +33 -0
  7. package/components/Citation.astro +94 -0
  8. package/components/Cite.astro +71 -0
  9. package/components/CodeBlock.astro +115 -0
  10. package/components/CodeRef.astro +49 -0
  11. package/components/ConceptBox.astro +26 -0
  12. package/components/Convergence.astro +41 -0
  13. package/components/CounterBox.astro +15 -0
  14. package/components/Divergence.astro +32 -0
  15. package/components/DynConnect.astro +15 -0
  16. package/components/ExampleBox.astro +15 -0
  17. package/components/Figure.astro +35 -0
  18. package/components/InsightBox.astro +15 -0
  19. package/components/KeyIdea.astro +21 -0
  20. package/components/MarginNote.astro +37 -0
  21. package/components/NoteBox.astro +15 -0
  22. package/components/OpenQuestion.astro +15 -0
  23. package/components/PaperBox.astro +15 -0
  24. package/components/PatternTimeline.astro +133 -0
  25. package/components/Recovery.astro +34 -0
  26. package/components/ResultBox.astro +15 -0
  27. package/components/Sidebar.astro +268 -0
  28. package/components/Sidenote.astro +26 -0
  29. package/components/SkillBox.astro +24 -0
  30. package/components/SourceArchive.astro +285 -0
  31. package/components/StatusBadge.astro +51 -0
  32. package/components/Tag.astro +60 -0
  33. package/components/Theorem.astro +65 -0
  34. package/components/TipBox.astro +15 -0
  35. package/components/ToolFilter.tsx +160 -0
  36. package/components/TryThis.astro +23 -0
  37. package/components/VersionSelector.tsx +85 -0
  38. package/components/WarnBox.astro +15 -0
  39. package/components/WeekRef.astro +51 -0
  40. package/components/XRef.astro +40 -0
  41. package/dist/index.d.ts +135 -0
  42. package/dist/index.mjs +369 -0
  43. package/dist/lib/katex-macros.d.ts +26 -0
  44. package/dist/lib/katex-macros.mjs +98 -0
  45. package/dist/schemas.d.ts +17 -0
  46. package/dist/schemas.mjs +160 -0
  47. package/dist/types-Cz-pwE1N.d.ts +61 -0
  48. package/examples/chapter-template-academic.mdx +100 -0
  49. package/examples/chapter-template-tools.mdx +90 -0
  50. package/layouts/Base.astro +250 -0
  51. package/layouts/Chapter.astro +37 -0
  52. package/package.json +137 -0
  53. package/pages/chapters.astro +371 -0
  54. package/pages/convergence.astro +96 -0
  55. package/pages/print.astro +39 -0
  56. package/pages/references.astro +160 -0
  57. package/pages/search.astro +87 -0
  58. package/pedagogy/kf-chapter-shape.md +96 -0
  59. package/pedagogy/source-tiers.md +121 -0
  60. package/pedagogy/volatility-classes.md +110 -0
  61. package/recipes/00-getting-started.md +77 -0
  62. package/recipes/01-add-math.md +71 -0
  63. package/recipes/02-bibliography-pipeline.md +82 -0
  64. package/recipes/03-asset-pipelines.md +84 -0
  65. package/recipes/04-component-library.md +118 -0
  66. package/recipes/05-deploy-cloudflare.md +74 -0
  67. package/recipes/06-mobile-first-layout.md +73 -0
  68. package/recipes/07-chapter-shapes.md +84 -0
  69. package/recipes/08-decisions-ledger.md +110 -0
  70. package/recipes/09-validation.md +106 -0
  71. package/recipes/10-custom-domain.md +72 -0
  72. package/recipes/README.md +43 -0
  73. package/scripts/build-bib.mjs +99 -0
  74. package/scripts/build-figures.mjs +179 -0
  75. package/scripts/render-notebooks.mjs +223 -0
  76. package/scripts/validate.mjs +179 -0
  77. package/styles/callouts.css +303 -0
  78. package/styles/chapter.css +209 -0
  79. package/styles/convergence.css +349 -0
  80. package/styles/layout.css +156 -0
  81. package/styles/print.css +203 -0
  82. package/styles/tokens.css +194 -0
  83. package/styles/tool-filter.css +135 -0
  84. package/styles/typography.css +147 -0
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * validate.mjs — pre-flight check for book content.
4
+ *
5
+ * Catches authoring errors that astro build either misses or surfaces
6
+ * with insufficient context. Designed to run in <2 s on a medium-sized
7
+ * book so it's pre-commit-hook friendly.
8
+ *
9
+ * Checks performed (per Q14 in the v2.0 plan):
10
+ *
11
+ * 1. <Cite key="..." /> — key exists in src/data/references.json.
12
+ * (Cite.astro already throws on unknown keys at build time; we
13
+ * surface ALL bad keys at once instead of failing on the first.)
14
+ *
15
+ * 2. <XRef id="..." /> — id exists in src/data/labels.json. XRef
16
+ * doesn't fail the build for unknown ids; without this check,
17
+ * typos ship to readers as "[?label]" placeholders.
18
+ *
19
+ * 3. <Figure src="/path/..." /> — referenced file exists under
20
+ * public/. Figure.astro renders a broken-image icon otherwise.
21
+ *
22
+ * 4. Internal markdown links [text](/foo) — target resolves to a
23
+ * known chapter slug or a known top-level route. External (http*)
24
+ * links are not checked (would need network IO).
25
+ *
26
+ * 5. <CodeRef path="..." line={N} /> — when run inside a repo
27
+ * whose root is BOOK_REPO_ROOT, the path exists and the line
28
+ * number is within file bounds. Skipped when BOOK_REPO_ROOT
29
+ * isn't set (the scaffold default; only meaningful for academic
30
+ * books that paired with an experiments/ subtree).
31
+ *
32
+ * What this DOESN'T do (and why):
33
+ * - frontmatter Zod validation — already done by astro build's
34
+ * content-collection sync.
35
+ * - MDX renders — same; astro build will fail.
36
+ * - KaTeX strict-mode — covered by rehype-katex when academic
37
+ * profile is active; undefined macros become build errors.
38
+ *
39
+ * Usage:
40
+ * node scripts/validate.mjs
41
+ * BOOK_REPO_ROOT=/abs/path/to/code/repo node scripts/validate.mjs
42
+ *
43
+ * Exit code = total failure count (0 = pass, ≥1 = errors).
44
+ *
45
+ * Wire into:
46
+ * - package.json scripts: "validate": "node scripts/validate.mjs"
47
+ * - pre-commit hook: .pre-commit-config.yaml
48
+ * - CI build pipeline: run before `astro build`
49
+ */
50
+ import { readFile, access } from 'node:fs/promises';
51
+ import { glob } from 'node:fs/promises';
52
+ import { resolve, dirname, join } from 'node:path';
53
+ import { fileURLToPath } from 'node:url';
54
+
55
+ const ROOT = resolve(fileURLToPath(import.meta.url), '../..');
56
+ const CHAPTERS_DIR = resolve(ROOT, 'src/content/chapters');
57
+ const PUBLIC_DIR = resolve(ROOT, 'public');
58
+ const DATA_DIR = resolve(ROOT, 'src/data');
59
+ const PROFILE = process.env.BOOK_PROFILE ?? 'minimal';
60
+ const REPO_ROOT = process.env.BOOK_REPO_ROOT ?? null;
61
+
62
+ const errors = [];
63
+ const warnings = [];
64
+ const fail = (file, line, msg) => errors.push({ file, line, msg });
65
+ const warn = (file, line, msg) => warnings.push({ file, line, msg });
66
+
67
+ // ===== Load reference data (graceful when missing) =====
68
+ async function loadJson(path) {
69
+ try {
70
+ return JSON.parse(await readFile(path, 'utf8'));
71
+ } catch {
72
+ return {};
73
+ }
74
+ }
75
+ const refs = await loadJson(join(DATA_DIR, 'references.json'));
76
+ const labels = await loadJson(join(DATA_DIR, 'labels.json'));
77
+
78
+ // ===== Collect chapter files =====
79
+ const chapterFiles = [];
80
+ for await (const f of glob('**/*.{md,mdx}', { cwd: CHAPTERS_DIR })) {
81
+ if (!f.split('/').pop().startsWith('_')) chapterFiles.push(f);
82
+ }
83
+
84
+ // ===== Build slug set from chapter filenames (for internal-link check) =====
85
+ const validSlugs = new Set(chapterFiles.map((f) => f.replace(/\.(md|mdx)$/, '')));
86
+ const validTopLevelRoutes = new Set([
87
+ '/', '/chapters/', '/references/', '/search/', '/print/', '/convergence/',
88
+ ]);
89
+
90
+ // ===== Pattern helpers (regex-based; cheap, good enough for MDX) =====
91
+ const RE_CITE = /<Cite[^>]+key=["']([^"']+)["']/g;
92
+ const RE_XREF = /<XRef[^>]+id=["']([^"']+)["']/g;
93
+ const RE_FIGURE = /<Figure[^>]+src=["']([^"']+)["']/g;
94
+ const RE_CODEREF = /<CodeRef[^>]+path=["']([^"']+)["'](?:[^>]*line=\{(\d+)\})?(?:[^>]*lineEnd=\{(\d+)\})?/g;
95
+ const RE_MD_LINK = /\[(?:[^\]]*)\]\((\/[^)\s#]+)(?:#[^)]*)?\)/g;
96
+
97
+ async function fileExists(p) {
98
+ try {
99
+ await access(p);
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ function lineOf(content, idx) {
107
+ return content.slice(0, idx).split('\n').length;
108
+ }
109
+
110
+ // ===== Run all checks on each chapter =====
111
+ for (const rel of chapterFiles) {
112
+ const abs = join(CHAPTERS_DIR, rel);
113
+ const content = await readFile(abs, 'utf8');
114
+
115
+ // 1. Cite keys (academic profile only — tools profile uses YAML manifest)
116
+ if (PROFILE === 'academic') {
117
+ for (const m of content.matchAll(RE_CITE)) {
118
+ if (!refs[m[1]]) fail(rel, lineOf(content, m.index), `Unknown bibkey "${m[1]}" — not in references.json`);
119
+ }
120
+ }
121
+
122
+ // 2. XRef ids
123
+ for (const m of content.matchAll(RE_XREF)) {
124
+ if (!labels[m[1]]) fail(rel, lineOf(content, m.index), `Unknown XRef id "${m[1]}" — not in labels.json`);
125
+ }
126
+
127
+ // 3. Figure src exists in public/
128
+ for (const m of content.matchAll(RE_FIGURE)) {
129
+ const src = m[1];
130
+ if (src.startsWith('http')) continue; // external image
131
+ const path = src.startsWith('/') ? join(PUBLIC_DIR, src) : join(dirname(abs), src);
132
+ if (!(await fileExists(path))) {
133
+ fail(rel, lineOf(content, m.index), `Figure src "${src}" not found at ${path}`);
134
+ }
135
+ }
136
+
137
+ // 4. Internal markdown links resolve
138
+ for (const m of content.matchAll(RE_MD_LINK)) {
139
+ const target = m[1].replace(/\/$/, '');
140
+ if (validTopLevelRoutes.has(target + '/') || validTopLevelRoutes.has(target)) continue;
141
+ const chMatch = target.match(/^\/chapters\/(.+)$/);
142
+ if (chMatch && validSlugs.has(chMatch[1])) continue;
143
+ warn(rel, lineOf(content, m.index), `Internal link "${m[1]}" — target may not resolve (check spelling or route)`);
144
+ }
145
+
146
+ // 5. CodeRef path + line bounds (only when BOOK_REPO_ROOT set)
147
+ if (REPO_ROOT) {
148
+ for (const m of content.matchAll(RE_CODEREF)) {
149
+ const [, path, lineStart, lineEnd] = m;
150
+ const abs2 = resolve(REPO_ROOT, path);
151
+ if (!(await fileExists(abs2))) {
152
+ fail(rel, lineOf(content, m.index), `CodeRef path "${path}" not found at ${abs2}`);
153
+ continue;
154
+ }
155
+ if (lineStart) {
156
+ const fileLineCount = (await readFile(abs2, 'utf8')).split('\n').length;
157
+ const lo = Number(lineStart);
158
+ const hi = lineEnd ? Number(lineEnd) : lo;
159
+ if (lo > fileLineCount || hi > fileLineCount) {
160
+ fail(rel, lineOf(content, m.index), `CodeRef line ${lo}-${hi} exceeds file length (${fileLineCount}) in "${path}"`);
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ // ===== Report =====
168
+ const format = ({ file, line, msg }) => ` ${file}:${line} ${msg}`;
169
+ if (warnings.length > 0) {
170
+ console.warn(`validate: ${warnings.length} warning(s):`);
171
+ warnings.forEach((w) => console.warn(format(w)));
172
+ }
173
+ if (errors.length === 0) {
174
+ console.log(`validate: ✓ ${chapterFiles.length} chapter(s) checked (profile=${PROFILE}); no errors.`);
175
+ process.exit(0);
176
+ }
177
+ console.error(`validate: ✗ ${errors.length} error(s) in ${chapterFiles.length} chapter(s) (profile=${PROFILE}):`);
178
+ errors.forEach((e) => console.error(format(e)));
179
+ process.exit(errors.length);
@@ -0,0 +1,303 @@
1
+ /* callouts.css — Shared callout-component styling.
2
+ *
3
+ * All callout components render the same .callout wrapper; semantic
4
+ * variants (.callout-skill, .callout-case, etc.) override the bar color
5
+ * and background tint. Palette comes from tokens.css; dark mode needs
6
+ * no overrides because everything references the --warm-*-tint tokens
7
+ * which re-compute from --paper.
8
+ */
9
+
10
+ .callout {
11
+ padding: var(--space-3) var(--space-4);
12
+ margin: var(--space-4) 0;
13
+ border-left: var(--border-bar) solid var(--color-border);
14
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
15
+ background: var(--color-bg-subtle);
16
+ color: var(--color-text);
17
+ }
18
+
19
+ .callout-title {
20
+ display: block;
21
+ font-size: var(--text-base);
22
+ font-weight: 600;
23
+ font-style: normal;
24
+ color: inherit;
25
+ margin: 0 0 var(--space-2) 0;
26
+ font-family: var(--font-body);
27
+ letter-spacing: 0.02em;
28
+ }
29
+
30
+ /* Body wrapper: collapse vertical margin on first/last children so the
31
+ * callout has consistent internal padding regardless of content. */
32
+ .callout-body > *:first-child { margin-top: 0; }
33
+ .callout-body > *:last-child { margin-bottom: 0; }
34
+
35
+ /* ===== Variant palettes ===== */
36
+ .callout-skill { /* practical how-to (green / tip) */
37
+ border-left-color: var(--callout-tip);
38
+ background: var(--warm-green-tint);
39
+ }
40
+
41
+ .callout-case { /* concrete anecdote (blue / info) */
42
+ border-left-color: var(--callout-info);
43
+ background: var(--warm-blue-tint);
44
+ }
45
+
46
+ .callout-concept { /* durable vocabulary (plum / authority) */
47
+ border-left-color: var(--callout-official);
48
+ background: var(--warm-plum-tint);
49
+ }
50
+
51
+ .callout-key { /* memorize this (blue, thicker bar) */
52
+ border-left-color: var(--callout-info);
53
+ background: var(--warm-blue-tint);
54
+ border-left-width: calc(var(--border-bar) * 1.5);
55
+ }
56
+
57
+ .callout-try { /* practice exercise (plum + rose blend) */
58
+ border-left-color: color-mix(in srgb, var(--warm-plum) 60%, var(--warm-rose));
59
+ background: color-mix(in srgb, var(--warm-plum-tint) 50%, var(--warm-rose-tint));
60
+ }
61
+
62
+ .callout-recovery {/* escape route from anti-pattern (rose / warning) */
63
+ border-left-color: var(--callout-warn);
64
+ background: var(--warm-rose-tint);
65
+ }
66
+
67
+ /* Recovery has a structured layout: symptom row + escape body. */
68
+ .callout-recovery .recovery-symptom {
69
+ font-size: var(--text-sm);
70
+ color: var(--color-text-muted);
71
+ margin-bottom: var(--space-2);
72
+ }
73
+ .callout-recovery .recovery-symptom strong {
74
+ color: var(--color-text);
75
+ }
76
+
77
+ /* ===== Comparative callouts (Convergence / Divergence) =====
78
+ * Same gold palette (insight), different border style to signal
79
+ * "stable signal" (solid) vs "open design space" (dashed). Both surface
80
+ * in the Evolution cornerstone of every chapter, per the Koller-Friedman
81
+ * decomposition. */
82
+ .callout-convergence {
83
+ border-left: var(--border-bar) solid var(--callout-insight);
84
+ background: var(--warm-gold-tint);
85
+ }
86
+ .callout-divergence {
87
+ border-left: var(--border-bar) dashed var(--callout-insight);
88
+ background: var(--warm-gold-tint);
89
+ }
90
+
91
+ /* Tool badges rendered inline in the Convergence title */
92
+ .callout-convergence .tool-badges {
93
+ display: inline-flex;
94
+ flex-wrap: wrap;
95
+ gap: var(--space-2);
96
+ margin-left: var(--space-2);
97
+ }
98
+ .tool-badge {
99
+ display: inline-block;
100
+ padding: 0.05em 0.45em;
101
+ background: var(--color-bg);
102
+ border: 1px solid var(--color-border);
103
+ border-radius: var(--radius-sm);
104
+ font-size: var(--text-xs);
105
+ font-weight: 500;
106
+ color: var(--color-text-muted);
107
+ font-family: var(--font-code);
108
+ letter-spacing: 0;
109
+ }
110
+
111
+ /* Divergence axis label */
112
+ .callout-divergence .divergence-axis {
113
+ font-weight: 400;
114
+ color: var(--color-text-muted);
115
+ font-size: var(--text-sm);
116
+ }
117
+
118
+ /* ===== Academic-profile callout variants (Q3 — coexist with tools variants) =====
119
+ * Maps the 10 academic callouts (NoteBox/ExampleBox/DynConnect/InsightBox/
120
+ * WarnBox/CounterBox/TipBox/OpenQuestion/PaperBox/ResultBox) to Paul Tol
121
+ * semantic colors. Each has a default-title prop in the component; CSS
122
+ * just handles bar color + background tint.
123
+ *
124
+ * Blue family — neutral info, examples, dynamical-systems bridge */
125
+ .callout-note,
126
+ .callout-example,
127
+ .callout-dynconnect {
128
+ border-left-color: var(--callout-info);
129
+ background: var(--warm-blue-tint);
130
+ }
131
+
132
+ /* Rose family — alerts, counter-evidence */
133
+ .callout-warn,
134
+ .callout-counter {
135
+ border-left-color: var(--callout-warn);
136
+ background: var(--warm-rose-tint);
137
+ }
138
+
139
+ /* Green family — practical hints, experimental results */
140
+ .callout-tip,
141
+ .callout-result {
142
+ border-left-color: var(--callout-tip);
143
+ background: var(--warm-green-tint);
144
+ }
145
+
146
+ /* Plum — authoritative open questions */
147
+ .callout-openquestion {
148
+ border-left-color: var(--callout-official);
149
+ background: var(--warm-plum-tint);
150
+ }
151
+
152
+ /* Gold — surprising insights */
153
+ .callout-insight {
154
+ border-left-color: var(--callout-insight);
155
+ background: var(--warm-gold-tint);
156
+ }
157
+
158
+ /* Gray dashed — restated paper claims (external source) */
159
+ .callout-paper {
160
+ border-left-color: var(--color-border);
161
+ border-left-style: dashed;
162
+ background: var(--color-bg-subtle);
163
+ }
164
+
165
+ /* ===== Theorem family (Theorem.astro) =====
166
+ * Plain (theorem/proposition/lemma/corollary): italic body
167
+ * Definition family (definition/example/exercise/remark/proof): upright body
168
+ */
169
+ .theorem {
170
+ margin: var(--space-4) 0;
171
+ padding-left: var(--space-4);
172
+ border-left: var(--border-bar) solid var(--callout-official);
173
+ }
174
+ .theorem-label {
175
+ font-weight: 600;
176
+ font-style: normal;
177
+ display: inline;
178
+ margin-right: 0.4em;
179
+ }
180
+ .theorem-body {
181
+ display: block;
182
+ font-style: italic;
183
+ }
184
+ .theorem-body > *:first-child { display: inline; }
185
+
186
+ .theorem[data-kind="definition"] .theorem-body,
187
+ .theorem[data-kind="example"] .theorem-body,
188
+ .theorem[data-kind="exercise"] .theorem-body,
189
+ .theorem[data-kind="remark"] .theorem-body,
190
+ .theorem[data-kind="proof"] .theorem-body {
191
+ font-style: normal;
192
+ }
193
+
194
+ /* ===== Margin notes (MarginNote.astro) — variant: note/warning/tip ===== */
195
+ .margin-note {
196
+ font-size: var(--text-sm);
197
+ padding: var(--space-2) var(--space-3);
198
+ margin: var(--space-3) 0;
199
+ border-left: var(--border-bar) solid var(--callout-info);
200
+ background: var(--warm-blue-tint);
201
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
202
+ }
203
+ .margin-note[data-variant="warning"] {
204
+ border-left-color: var(--callout-warn);
205
+ background: var(--warm-rose-tint);
206
+ }
207
+ .margin-note[data-variant="tip"] {
208
+ border-left-color: var(--callout-tip);
209
+ background: var(--warm-green-tint);
210
+ }
211
+ .margin-note-label {
212
+ display: inline-block;
213
+ font-family: var(--font-code);
214
+ font-size: var(--text-xs);
215
+ font-weight: 600;
216
+ text-transform: uppercase;
217
+ letter-spacing: 0.04em;
218
+ color: var(--color-text-muted);
219
+ margin-right: var(--space-2);
220
+ }
221
+
222
+ /* ===== Status badges (StatusBadge.astro) — 3-state public taxonomy ===== */
223
+ .status-badge {
224
+ display: inline-block;
225
+ font-family: var(--font-code);
226
+ font-size: var(--text-xs);
227
+ font-weight: 600;
228
+ padding: 0.1em 0.55em;
229
+ border-radius: var(--radius-sm);
230
+ letter-spacing: 0.04em;
231
+ text-transform: uppercase;
232
+ color: var(--paper);
233
+ vertical-align: 0.1em;
234
+ }
235
+ .status-badge[data-status="published"] { background: var(--warm-green); }
236
+ .status-badge[data-status="draft"] { background: var(--warm-gold); }
237
+ .status-badge[data-status="coming-soon"] { background: var(--color-text-muted); }
238
+
239
+ /* ===== Figure component (Figure.astro) ===== */
240
+ figure.figure {
241
+ margin: var(--space-6) 0;
242
+ text-align: center;
243
+ }
244
+ figure.figure img,
245
+ figure.figure svg {
246
+ max-width: 100%;
247
+ height: auto;
248
+ }
249
+ figure.figure figcaption {
250
+ font-size: var(--text-sm);
251
+ color: var(--color-text-muted);
252
+ margin-top: var(--space-2);
253
+ font-style: italic;
254
+ text-align: left;
255
+ }
256
+
257
+ /* ===== CodeBlock header bar with GitHub link ===== */
258
+ .codeblock-frame {
259
+ margin: var(--space-4) 0;
260
+ border: 1px solid var(--color-border);
261
+ border-radius: var(--radius-md);
262
+ overflow: hidden;
263
+ background: var(--color-code-bg);
264
+ }
265
+ .codeblock-header {
266
+ display: flex;
267
+ align-items: center;
268
+ justify-content: space-between;
269
+ padding: var(--space-2) var(--space-3);
270
+ background: var(--color-bg-subtle);
271
+ border-bottom: 1px solid var(--color-border);
272
+ font-family: var(--font-code);
273
+ font-size: var(--text-xs);
274
+ color: var(--color-text-muted);
275
+ }
276
+ .codeblock-header a {
277
+ font-weight: 500;
278
+ }
279
+ .codeblock-frame pre {
280
+ margin: 0;
281
+ border-radius: 0;
282
+ border: 0;
283
+ }
284
+
285
+ /* ===== Practice tags (Tag.astro) ===== */
286
+ .practice-tag {
287
+ display: inline-block;
288
+ font-family: var(--font-code);
289
+ font-size: 0.75em;
290
+ font-weight: 500;
291
+ padding: 0 0.4em;
292
+ margin: 0 0.1em;
293
+ border-radius: var(--radius-sm);
294
+ background: var(--color-bg-subtle);
295
+ border: 1px solid var(--color-border);
296
+ color: var(--color-text-muted);
297
+ vertical-align: 0.1em;
298
+ letter-spacing: 0.04em;
299
+ text-transform: uppercase;
300
+ }
301
+ .practice-tag[data-kind="official"] { border-color: var(--callout-official); color: var(--callout-official); }
302
+ .practice-tag[data-kind="convergence"] { border-color: var(--callout-insight); color: var(--callout-insight); }
303
+ .practice-tag[data-kind="practitioner"] { border-color: var(--callout-tip); color: var(--callout-tip); }
@@ -0,0 +1,209 @@
1
+ /* chapter.css — Chapter-specific typography and structural styling.
2
+ * Uses tokens from tokens.css and the .prose wrapper from layout.css.
3
+ */
4
+
5
+ /* ===== Chapter header ===== */
6
+ .chapter-header {
7
+ margin-bottom: var(--space-8);
8
+ border-bottom: 1px solid var(--color-border);
9
+ padding-bottom: var(--space-6);
10
+ }
11
+ .chapter-header h1 {
12
+ margin-top: var(--space-2);
13
+ margin-bottom: var(--space-3);
14
+ }
15
+ .chapter-description {
16
+ font-size: var(--text-lg);
17
+ color: var(--color-text-muted);
18
+ margin: var(--space-3) 0;
19
+ text-indent: 0;
20
+ font-style: italic;
21
+ }
22
+ .chapter-meta {
23
+ display: flex;
24
+ flex-wrap: wrap;
25
+ gap: var(--space-4);
26
+ font-size: var(--text-sm);
27
+ color: var(--color-text-muted);
28
+ font-family: var(--font-code);
29
+ margin-bottom: var(--space-2);
30
+ }
31
+ .chapter-meta > span {
32
+ display: inline-flex;
33
+ gap: 0.3em;
34
+ }
35
+ .chapter-meta-label {
36
+ text-transform: uppercase;
37
+ letter-spacing: 0.05em;
38
+ font-size: 0.75em;
39
+ }
40
+
41
+ /* Volatility + tool badges: reuse .tool-badge from callouts.css */
42
+ .volatility-badge {
43
+ display: inline-block;
44
+ padding: 0.05em 0.45em;
45
+ border-radius: var(--radius-sm);
46
+ font-size: var(--text-xs);
47
+ font-weight: 500;
48
+ font-family: var(--font-code);
49
+ letter-spacing: 0;
50
+ }
51
+ .volatility-stable-principle {
52
+ background: var(--warm-green-tint);
53
+ color: var(--warm-green);
54
+ border: 1px solid var(--warm-green);
55
+ }
56
+ .volatility-architectural-pattern {
57
+ background: var(--warm-gold-tint);
58
+ color: var(--warm-gold);
59
+ border: 1px solid var(--warm-gold);
60
+ }
61
+ .volatility-feature-surface {
62
+ background: var(--warm-rose-tint);
63
+ color: var(--warm-rose);
64
+ border: 1px solid var(--warm-rose);
65
+ }
66
+
67
+ /* Freshness badge — inline with "Last verified" in chapter-meta.
68
+ * Three status bands map to the same warm palette as volatility:
69
+ * fresh → green (last_verified is recent relative to volatility threshold)
70
+ * verify-soon → gold (75-100% of threshold elapsed)
71
+ * stale → rose (past threshold; verify before trusting claims)
72
+ */
73
+ .freshness-badge {
74
+ display: inline-block;
75
+ padding: 0.02em 0.45em;
76
+ border-radius: var(--radius-sm);
77
+ font-size: 0.85em;
78
+ font-weight: 500;
79
+ letter-spacing: 0;
80
+ line-height: 1.4;
81
+ vertical-align: baseline;
82
+ cursor: help;
83
+ /* Prevent the "2026-04-17 · Fresh" pair from breaking across lines when
84
+ * chapter-meta wraps on narrow viewports. */
85
+ white-space: nowrap;
86
+ }
87
+ .freshness-badge[data-status='fresh'] {
88
+ background: var(--warm-green-tint);
89
+ color: var(--warm-green);
90
+ border: 1px solid var(--warm-green);
91
+ }
92
+ .freshness-badge[data-status='verify-soon'] {
93
+ background: var(--warm-gold-tint);
94
+ color: var(--warm-gold);
95
+ border: 1px solid var(--warm-gold);
96
+ }
97
+ .freshness-badge[data-status='stale'] {
98
+ background: var(--warm-rose-tint);
99
+ color: var(--warm-rose);
100
+ border: 1px solid var(--warm-rose);
101
+ font-weight: 600;
102
+ }
103
+
104
+ .chapter-badge-row {
105
+ display: flex;
106
+ flex-wrap: wrap;
107
+ gap: var(--space-2);
108
+ align-items: center;
109
+ margin-top: var(--space-3);
110
+ font-size: var(--text-sm);
111
+ color: var(--color-text-muted);
112
+ }
113
+ .chapter-badge-row-label {
114
+ font-weight: 500;
115
+ }
116
+
117
+ /* ===== Chapter TOC (collapsed <details> at top) ===== */
118
+ .chapter-toc {
119
+ margin: var(--space-4) 0 var(--space-6);
120
+ padding: var(--space-2) var(--space-3);
121
+ border: 1px solid var(--color-border);
122
+ border-radius: var(--radius-md);
123
+ background: var(--color-bg-subtle);
124
+ font-size: var(--text-sm);
125
+ }
126
+ .chapter-toc > summary {
127
+ cursor: pointer;
128
+ font-weight: 500;
129
+ color: var(--color-link);
130
+ list-style: none;
131
+ padding: var(--space-1) 0;
132
+ }
133
+ .chapter-toc > summary::-webkit-details-marker {
134
+ display: none;
135
+ }
136
+ .chapter-toc > summary::before {
137
+ content: "▸ ";
138
+ display: inline-block;
139
+ transition: transform 120ms ease;
140
+ }
141
+ .chapter-toc[open] > summary::before {
142
+ content: "▾ ";
143
+ }
144
+ .chapter-toc ol {
145
+ list-style: none;
146
+ padding-left: 0;
147
+ margin: var(--space-2) 0 0;
148
+ }
149
+ .chapter-toc li {
150
+ margin: var(--space-1) 0;
151
+ padding-left: var(--space-3);
152
+ border-left: 2px solid var(--color-border);
153
+ }
154
+ .chapter-toc li.toc-h3 {
155
+ padding-left: var(--space-6);
156
+ }
157
+ .chapter-toc a {
158
+ text-decoration: none;
159
+ color: var(--color-text);
160
+ }
161
+ .chapter-toc a:hover {
162
+ text-decoration: underline;
163
+ color: var(--color-link);
164
+ }
165
+
166
+ /* ===== Chapter nav (prev/next at bottom) ===== */
167
+ .chapter-nav {
168
+ display: grid;
169
+ grid-template-columns: 1fr 1fr;
170
+ gap: var(--space-4);
171
+ margin-top: var(--space-12);
172
+ padding-top: var(--space-6);
173
+ border-top: 1px solid var(--color-border);
174
+ }
175
+ .chapter-nav a {
176
+ display: block;
177
+ padding: var(--space-3);
178
+ border: 1px solid var(--color-border);
179
+ border-radius: var(--radius-md);
180
+ text-decoration: none;
181
+ color: var(--color-text);
182
+ transition: border-color 120ms ease, background 120ms ease;
183
+ }
184
+ .chapter-nav a:hover {
185
+ border-color: var(--color-link);
186
+ background: var(--color-bg-subtle);
187
+ }
188
+ .chapter-nav a .nav-label {
189
+ display: block;
190
+ font-size: var(--text-xs);
191
+ color: var(--color-text-muted);
192
+ text-transform: uppercase;
193
+ letter-spacing: 0.05em;
194
+ margin-bottom: 0.25em;
195
+ }
196
+ .chapter-nav a .nav-title {
197
+ font-weight: 500;
198
+ color: var(--color-link);
199
+ }
200
+ .chapter-nav .prev { text-align: left; }
201
+ .chapter-nav .next { text-align: right; grid-column: 2; }
202
+ .chapter-nav .prev:only-child { grid-column: 1; }
203
+
204
+ @media (max-width: 48rem) {
205
+ .chapter-nav {
206
+ grid-template-columns: 1fr;
207
+ }
208
+ .chapter-nav .next { grid-column: 1; text-align: left; }
209
+ }