@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,349 @@
1
+ /* convergence.css — styling for the /convergence dashboard and the
2
+ * PatternTimeline cards it embeds. Follows the same warm-Tol language
3
+ * and spacing conventions as the rest of the book.
4
+ */
5
+
6
+ /* ===== Dashboard page layout ===== */
7
+ .convergence-header {
8
+ margin-bottom: var(--space-8);
9
+ border-bottom: 1px solid var(--color-border);
10
+ padding-bottom: var(--space-6);
11
+ }
12
+ .convergence-lede {
13
+ font-size: var(--text-lg);
14
+ color: var(--color-text-muted);
15
+ max-width: 65ch;
16
+ }
17
+ .convergence-metric {
18
+ font-size: var(--text-sm);
19
+ color: var(--color-text-muted);
20
+ max-width: 65ch;
21
+ margin-top: var(--space-3);
22
+ text-indent: 0;
23
+ }
24
+
25
+ .convergence-category {
26
+ margin: var(--space-8) 0;
27
+ }
28
+ .convergence-category-heading {
29
+ font-size: var(--text-sm);
30
+ font-family: var(--font-code);
31
+ text-transform: uppercase;
32
+ letter-spacing: 0.08em;
33
+ color: var(--color-text-muted);
34
+ border-bottom: 1px solid var(--color-border);
35
+ padding-bottom: var(--space-2);
36
+ margin-bottom: var(--space-4);
37
+ display: flex;
38
+ align-items: baseline;
39
+ gap: var(--space-3);
40
+ }
41
+ .convergence-category-label {
42
+ font-weight: 500;
43
+ color: var(--color-text);
44
+ }
45
+ .convergence-category-count {
46
+ font-size: var(--text-xs);
47
+ color: var(--color-text-muted);
48
+ }
49
+ .convergence-category-empty {
50
+ color: var(--color-text-muted);
51
+ font-style: italic;
52
+ padding: var(--space-3);
53
+ background: var(--color-bg-subtle);
54
+ border-radius: var(--radius-sm);
55
+ text-indent: 0;
56
+ }
57
+ .convergence-card-list {
58
+ display: grid;
59
+ gap: var(--space-4);
60
+ }
61
+
62
+ .convergence-method {
63
+ margin-top: var(--space-10);
64
+ padding-top: var(--space-6);
65
+ border-top: 1px solid var(--color-border);
66
+ }
67
+
68
+ /* ===== Pattern card ===== */
69
+ .pattern-card {
70
+ padding: var(--space-4);
71
+ border: 1px solid var(--color-border);
72
+ border-radius: var(--radius-md);
73
+ background: var(--color-bg);
74
+ }
75
+
76
+ .pattern-card-header {
77
+ margin-bottom: var(--space-3);
78
+ }
79
+ .pattern-card-title-row {
80
+ display: flex;
81
+ align-items: center;
82
+ flex-wrap: wrap;
83
+ gap: var(--space-3);
84
+ margin-bottom: var(--space-2);
85
+ }
86
+ .pattern-card-title {
87
+ font-size: var(--text-lg);
88
+ font-weight: 500;
89
+ margin: 0;
90
+ color: var(--color-link);
91
+ }
92
+ .pattern-card-meta {
93
+ display: flex;
94
+ flex-wrap: wrap;
95
+ gap: var(--space-3);
96
+ align-items: center;
97
+ font-size: var(--text-sm);
98
+ }
99
+
100
+ /* Category badges — use the warm palette semantically. */
101
+ .pattern-category-badge {
102
+ display: inline-block;
103
+ padding: 0.1em 0.55em;
104
+ font-family: var(--font-code);
105
+ font-size: 0.75em;
106
+ font-weight: 500;
107
+ letter-spacing: 0.04em;
108
+ border-radius: var(--radius-sm);
109
+ border: 1px solid var(--color-border);
110
+ background: var(--color-bg);
111
+ color: var(--color-text);
112
+ }
113
+ .pattern-category-safety {
114
+ background: var(--warm-rose-tint);
115
+ color: var(--warm-rose);
116
+ border-color: var(--warm-rose);
117
+ }
118
+ .pattern-category-scale {
119
+ background: var(--warm-blue-tint);
120
+ color: var(--warm-blue);
121
+ border-color: var(--warm-blue);
122
+ }
123
+ .pattern-category-context {
124
+ background: var(--warm-gold-tint);
125
+ color: var(--warm-gold);
126
+ border-color: var(--warm-gold);
127
+ }
128
+ .pattern-category-interaction {
129
+ background: var(--warm-green-tint);
130
+ color: var(--warm-green);
131
+ border-color: var(--warm-green);
132
+ }
133
+ .pattern-category-extension {
134
+ background: var(--warm-plum-tint);
135
+ color: var(--warm-plum);
136
+ border-color: var(--warm-plum);
137
+ }
138
+ .pattern-category-other {
139
+ /* use default neutral styling */
140
+ }
141
+
142
+ /* Convergence badge — green for converged, gold for not-yet. */
143
+ .pattern-convergence-badge {
144
+ display: inline-block;
145
+ padding: 0.1em 0.55em;
146
+ border-radius: var(--radius-sm);
147
+ font-family: var(--font-code);
148
+ font-size: 0.85em;
149
+ font-weight: 500;
150
+ letter-spacing: 0.03em;
151
+ }
152
+ .pattern-convergence-badge.converged {
153
+ background: var(--warm-green-tint);
154
+ color: var(--warm-green);
155
+ border: 1px solid var(--warm-green);
156
+ }
157
+ .pattern-convergence-badge.not-converged {
158
+ background: var(--warm-gold-tint);
159
+ color: var(--warm-gold);
160
+ border: 1px solid var(--warm-gold);
161
+ }
162
+ .pattern-adoption-count {
163
+ font-family: var(--font-code);
164
+ font-size: 0.85em;
165
+ color: var(--color-text-muted);
166
+ }
167
+
168
+ .pattern-description {
169
+ color: var(--color-text);
170
+ margin: 0 0 var(--space-3) 0;
171
+ text-indent: 0;
172
+ line-height: 1.5;
173
+ }
174
+
175
+ .pattern-empty-timeline {
176
+ color: var(--color-text-muted);
177
+ font-style: italic;
178
+ padding: var(--space-2) var(--space-3);
179
+ background: var(--color-bg-subtle);
180
+ border-radius: var(--radius-sm);
181
+ text-indent: 0;
182
+ margin: 0;
183
+ }
184
+
185
+ /* ===== Timeline ===== */
186
+ .pattern-timeline {
187
+ display: grid;
188
+ grid-template-columns: 9rem 1fr;
189
+ gap: var(--space-2) var(--space-3);
190
+ margin-top: var(--space-3);
191
+ padding: var(--space-3);
192
+ background: var(--color-bg-subtle);
193
+ border-radius: var(--radius-sm);
194
+ }
195
+
196
+ .pattern-timeline-track {
197
+ display: contents;
198
+ }
199
+ .pattern-timeline-tool-label {
200
+ font-family: var(--font-code);
201
+ font-size: var(--text-sm);
202
+ color: var(--color-text-muted);
203
+ align-self: center;
204
+ white-space: nowrap;
205
+ }
206
+ .pattern-timeline-track[data-adopted='true'] .pattern-timeline-tool-label {
207
+ color: var(--color-text);
208
+ }
209
+
210
+ .pattern-timeline-rail {
211
+ position: relative;
212
+ min-height: 2.2rem;
213
+ border-bottom: 1px dashed var(--color-border);
214
+ display: flex;
215
+ align-items: center;
216
+ }
217
+
218
+ .pattern-timeline-not-yet {
219
+ font-style: italic;
220
+ color: var(--color-text-muted);
221
+ font-size: var(--text-xs);
222
+ padding-left: var(--space-1);
223
+ }
224
+
225
+ .pattern-timeline-dot {
226
+ position: absolute;
227
+ top: 50%;
228
+ transform: translate(-50%, -50%);
229
+ width: 0.85rem;
230
+ height: 0.85rem;
231
+ border-radius: 50%;
232
+ background: var(--warm-blue);
233
+ border: 2px solid var(--color-bg);
234
+ cursor: help;
235
+ display: inline-flex;
236
+ align-items: center;
237
+ justify-content: center;
238
+ transition: transform 120ms ease;
239
+ }
240
+ .pattern-timeline-dot:hover {
241
+ transform: translate(-50%, -50%) scale(1.3);
242
+ z-index: 5;
243
+ }
244
+
245
+ .pattern-timeline-dot[data-kind='added'] {
246
+ background: var(--warm-green);
247
+ }
248
+ .pattern-timeline-dot[data-kind='changed'] {
249
+ background: var(--warm-gold);
250
+ }
251
+ .pattern-timeline-dot[data-kind='removed'],
252
+ .pattern-timeline-dot[data-kind='deprecated'] {
253
+ background: var(--warm-rose);
254
+ }
255
+
256
+ .pattern-timeline-dot-label {
257
+ position: absolute;
258
+ top: calc(100% + 0.2em);
259
+ left: 50%;
260
+ transform: translateX(-50%);
261
+ font-family: var(--font-code);
262
+ font-size: 0.65rem;
263
+ color: var(--color-text-muted);
264
+ white-space: nowrap;
265
+ pointer-events: none;
266
+ }
267
+
268
+ .pattern-timeline-axis {
269
+ grid-column: 2;
270
+ display: flex;
271
+ justify-content: space-between;
272
+ font-family: var(--font-code);
273
+ font-size: 0.7rem;
274
+ color: var(--color-text-muted);
275
+ margin-top: var(--space-2);
276
+ padding-top: var(--space-1);
277
+ border-top: 1px solid var(--color-border);
278
+ }
279
+
280
+ /* ===== Mobile layout ===== */
281
+ @media (max-width: 48rem) {
282
+ .pattern-timeline {
283
+ grid-template-columns: 1fr;
284
+ gap: var(--space-3);
285
+ }
286
+ .pattern-timeline-track {
287
+ display: grid;
288
+ grid-template-columns: 8rem 1fr;
289
+ gap: var(--space-2);
290
+ }
291
+ .pattern-timeline-axis {
292
+ grid-column: 1;
293
+ }
294
+
295
+ .convergence-category-heading {
296
+ flex-direction: column;
297
+ align-items: flex-start;
298
+ gap: var(--space-1);
299
+ }
300
+ }
301
+
302
+ /* Narrow phones (iPhone 5 / small folds): stack tool label above rail
303
+ * entirely. 8rem sidebar was still sharing ~200px with the rail on
304
+ * a 320px viewport, causing dots to crowd when multiple events
305
+ * fell within a tight date range. */
306
+ @media (max-width: 26rem) {
307
+ .pattern-timeline-track {
308
+ grid-template-columns: 1fr;
309
+ gap: 0;
310
+ }
311
+ .pattern-timeline-tool-label {
312
+ font-size: var(--text-xs);
313
+ padding-top: var(--space-1);
314
+ }
315
+ .pattern-timeline-rail {
316
+ min-height: 2.6rem; /* a bit more vertical room so labels don't collide */
317
+ }
318
+ }
319
+
320
+ /* ===== Print ===== */
321
+ @media print {
322
+ .pattern-card {
323
+ break-inside: avoid;
324
+ background: transparent !important;
325
+ border-width: 0.5pt;
326
+ border-color: #aaa;
327
+ }
328
+ .pattern-timeline {
329
+ background: transparent !important;
330
+ border: 0.5pt solid #aaa;
331
+ }
332
+ .pattern-timeline-dot {
333
+ border-color: #fff;
334
+ background: #333 !important;
335
+ }
336
+ .pattern-timeline-dot:hover {
337
+ transform: translate(-50%, -50%); /* no scale on paper */
338
+ }
339
+ }
340
+
341
+ /* ===== Reduced motion ===== */
342
+ @media (prefers-reduced-motion: reduce) {
343
+ .pattern-timeline-dot {
344
+ transition: none;
345
+ }
346
+ .pattern-timeline-dot:hover {
347
+ transform: translate(-50%, -50%);
348
+ }
349
+ }
@@ -0,0 +1,156 @@
1
+ /* layout.css — Tufte-inspired 2-column layout + optional docs-style sidebar
2
+ *
3
+ * Three structural layers:
4
+ *
5
+ * 1. Page chrome at very top: theme toggle (top-right, position: fixed).
6
+ *
7
+ * 2. Optional left sidebar (Sidebar.astro). At ≥64rem (1024px), grid
8
+ * pins it to the left edge; below 1024px it's hidden (mobile pages
9
+ * don't carry the nav). Toggle per-page via Base.astro's `showSidebar`
10
+ * prop — default true; the landing page sets false.
11
+ *
12
+ * 3. Main content (.prose) — Tufte 2-column with sidenotes in right
13
+ * margin on desktop (≥48rem), inline on mobile (<48rem). See
14
+ * tokens.css for the three-tier --measure-main width strategy.
15
+ *
16
+ * Zero JavaScript. Pure CSS. Respects Gwern's "no effort" principle:
17
+ * sidenotes are always visible at reading speed; the only thing that
18
+ * changes across breakpoints is where they sit.
19
+ */
20
+
21
+ /* ===== Optional left sidebar (docs-style nav) ===== */
22
+
23
+ /* Default layout when sidebar is present but viewport too narrow:
24
+ * stack vertically, hide sidebar. Below 1024px the sidebar component
25
+ * is display:none; the slot occupies full width. */
26
+ .layout-with-sidebar {
27
+ display: block;
28
+ }
29
+ .layout-with-sidebar > .sidebar {
30
+ display: none;
31
+ }
32
+
33
+ /* At 1024px+: pin sidebar to left, main content fills remainder. */
34
+ @media (min-width: 64rem) {
35
+ .layout-with-sidebar {
36
+ display: grid;
37
+ grid-template-columns: 16rem 1fr;
38
+ grid-template-areas: 'sidebar main';
39
+ min-height: 100vh;
40
+ }
41
+ .layout-with-sidebar > .sidebar {
42
+ display: block;
43
+ }
44
+ .layout-with-sidebar > .layout-main {
45
+ grid-area: main;
46
+ min-width: 0; /* prevent overflow from long pre/code */
47
+ }
48
+ }
49
+
50
+ /* Slightly wider sidebar at 1440px+ — more breathing room for chapter
51
+ * titles. */
52
+ @media (min-width: 90rem) {
53
+ .layout-with-sidebar {
54
+ grid-template-columns: 18rem 1fr;
55
+ }
56
+ }
57
+
58
+ .prose {
59
+ /* Container width = main + gutter + sidenote column + outer padding */
60
+ max-width: calc(
61
+ var(--measure-main) + var(--space-6) + var(--measure-side)
62
+ + 2 * var(--space-4)
63
+ );
64
+ margin: var(--space-8) auto;
65
+ padding: 0 var(--space-4);
66
+ position: relative;
67
+ }
68
+
69
+ /* Default: constrain direct children to the main-text measure.
70
+ * The remaining ~31ch becomes the right margin for sidenotes. */
71
+ .prose > * {
72
+ max-width: var(--measure-main);
73
+ }
74
+
75
+ /* Sidenote: floats into the right margin on desktop.
76
+ * Float + negative margin-right is the classic Tufte-CSS technique —
77
+ * the element aligns to where it appears in document flow (roughly
78
+ * next to its referencing paragraph), not to a fixed row. */
79
+ .sidenote {
80
+ float: right;
81
+ clear: right;
82
+ width: var(--measure-side);
83
+ margin-right: calc(-1 * (var(--measure-side) + var(--space-6)));
84
+ margin-top: 0.3rem;
85
+ margin-bottom: var(--space-3);
86
+ padding-left: var(--space-3);
87
+ border-left: var(--border-bar) solid var(--color-border);
88
+ font-size: var(--text-sm);
89
+ line-height: var(--leading-normal);
90
+ font-style: italic;
91
+ color: var(--color-text-muted);
92
+ display: block;
93
+ position: relative;
94
+ }
95
+
96
+ /* ===== Auto-numbered sidenote markers (CSS counters) =====
97
+ * Counter resets per .prose so each chapter numbers from 1. The marker
98
+ * and its matching sidenote share the same counter value because only
99
+ * the marker increments it. */
100
+ .prose {
101
+ counter-reset: sidenote;
102
+ }
103
+
104
+ .sidenote-marker {
105
+ counter-increment: sidenote;
106
+ color: var(--color-link);
107
+ cursor: help;
108
+ font-size: 0.75em;
109
+ vertical-align: super;
110
+ line-height: 0;
111
+ margin-left: 0.1em;
112
+ }
113
+ .sidenote-marker::after {
114
+ content: counter(sidenote);
115
+ }
116
+
117
+ /* Sidenote itself prefixes with the counter so readers can match
118
+ * marker-in-main-text with note-in-margin. */
119
+ .sidenote::before {
120
+ content: counter(sidenote) ". ";
121
+ font-weight: 500;
122
+ color: var(--color-link);
123
+ margin-right: 0.15em;
124
+ }
125
+
126
+ /* ===== Mobile breakpoint: inline-flow asides =====
127
+ * Sidenotes break out of float and become visually-distinct inline
128
+ * blocks immediately after their position in the document flow.
129
+ * No tap, no reveal, no layout shift — Gwern's "no effort" principle. */
130
+ @media (max-width: 48rem) {
131
+ .prose {
132
+ max-width: var(--measure-main);
133
+ }
134
+
135
+ .sidenote {
136
+ float: none;
137
+ clear: none;
138
+ width: auto;
139
+ max-width: none;
140
+ margin: var(--space-3) 0 var(--space-3) var(--space-3);
141
+ padding: var(--space-2) var(--space-3);
142
+ border-left: var(--border-bar) solid var(--callout-info);
143
+ background: var(--warm-blue-tint);
144
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
145
+ font-size: var(--text-sm);
146
+ font-style: italic;
147
+ color: var(--color-text);
148
+ }
149
+ }
150
+
151
+ /* Escape class for wide content (figures, large tables, code blocks
152
+ * that need the full container width including the sidenote column). */
153
+ .wide {
154
+ max-width: none !important;
155
+ width: 100%;
156
+ }
@@ -0,0 +1,203 @@
1
+ /* print.css — Print + Paged.js styling.
2
+ *
3
+ * Applied by the browser when the user prints, and by pagedjs-cli
4
+ * when rendering the PDF edition. `@page` rules configure the page
5
+ * size, margins, and running headers; `@media print` overrides
6
+ * screen-specific layout (Tufte 2-column becomes a single flow with
7
+ * sidenotes floating in the page margin).
8
+ */
9
+
10
+ @page {
11
+ size: letter;
12
+ margin: 1in 0.9in 1in 0.9in;
13
+
14
+ @bottom-center {
15
+ content: counter(page);
16
+ font-family: var(--font-body);
17
+ font-size: 9pt;
18
+ color: #555;
19
+ }
20
+ }
21
+
22
+ /* Front-matter pages without page numbers (optional — expand later). */
23
+ @page :first {
24
+ @bottom-center { content: none; }
25
+ }
26
+
27
+ @media print {
28
+ /* Hide interactive chrome */
29
+ .chrome-buttons,
30
+ .chapter-nav,
31
+ .chapter-toc,
32
+ .pagefind-ui,
33
+ .version-selector,
34
+ nav[aria-label="On this page"],
35
+ script {
36
+ display: none !important;
37
+ }
38
+
39
+ html, body {
40
+ background: white !important;
41
+ color: black !important;
42
+ font-size: 10.5pt;
43
+ }
44
+
45
+ /* Reset Tufte container for page flow */
46
+ .prose {
47
+ max-width: 100%;
48
+ margin: 0;
49
+ padding: 0;
50
+ }
51
+ .prose > * {
52
+ max-width: 100%;
53
+ }
54
+
55
+ /* Sidenotes stay floated in the right margin of the printed page.
56
+ * 30% width leaves the main column ~65% — similar proportion to
57
+ * the screen Tufte grid. */
58
+ .sidenote {
59
+ float: right;
60
+ clear: right;
61
+ width: 30%;
62
+ margin-right: -35%;
63
+ margin-top: 0;
64
+ padding-left: 0.5em;
65
+ border-left: 2px solid #aaa;
66
+ font-size: 9pt;
67
+ font-style: italic;
68
+ color: #333;
69
+ background: transparent;
70
+ }
71
+
72
+ /* Sidenote marker stays inline as superscript. */
73
+ .sidenote-marker {
74
+ color: #000;
75
+ font-size: 0.7em;
76
+ }
77
+
78
+ /* Page breaks — chapters start on a fresh page; keep headings + blocks together. */
79
+ section.chapter-print {
80
+ break-before: page;
81
+ }
82
+ section.chapter-print:first-of-type {
83
+ break-before: auto;
84
+ }
85
+ h1 {
86
+ break-before: page;
87
+ break-after: avoid;
88
+ }
89
+ h2, h3, h4 {
90
+ break-after: avoid;
91
+ }
92
+ pre, table, .callout, blockquote {
93
+ break-inside: avoid;
94
+ }
95
+
96
+ /* Links: show the URL alongside the link text for print. */
97
+ a[href]:not([href^="#"])::after {
98
+ content: " (" attr(href) ")";
99
+ font-size: 0.85em;
100
+ color: #555;
101
+ word-break: break-all;
102
+ }
103
+
104
+ /* Don't print the URL for in-page anchors or images. */
105
+ .citation-link::after,
106
+ .sidenote a::after {
107
+ content: "" !important;
108
+ }
109
+
110
+ /* Callouts: keep palette but reduce visual weight for print economy */
111
+ .callout {
112
+ padding: 0.4em 0.7em;
113
+ margin: 0.6em 0;
114
+ border-left-width: 2px;
115
+ background: #f6f4ee !important;
116
+ color: black !important;
117
+ }
118
+ .callout-title {
119
+ color: black;
120
+ }
121
+
122
+ /* Tables: light borders, normal weight */
123
+ table {
124
+ page-break-inside: avoid;
125
+ font-size: 9pt;
126
+ }
127
+ th, td {
128
+ border-bottom: 0.5pt solid #aaa;
129
+ }
130
+
131
+ /* Code blocks: keep the background subtle on paper. */
132
+ pre {
133
+ background: #f6f4ee !important;
134
+ border: 0.5pt solid #ccc;
135
+ font-size: 8.5pt;
136
+ /* Long lines wrap rather than overflow the page edge. */
137
+ white-space: pre-wrap !important;
138
+ word-wrap: break-word;
139
+ overflow-x: visible !important;
140
+ }
141
+ code {
142
+ background: transparent;
143
+ border: 0;
144
+ }
145
+ .astro-code,
146
+ .astro-code span {
147
+ color: black !important;
148
+ background: transparent !important;
149
+ }
150
+
151
+ /* ===== Polish pass (commit 17) =====
152
+ * Widows/orphans, keep-with-next rules, and a few edge-cases that
153
+ * only surface in real paginated output. */
154
+
155
+ /* Keep at least 3 lines together at page boundaries so paragraphs
156
+ * don't leave a stranded first/last line. */
157
+ p, li, blockquote {
158
+ orphans: 3;
159
+ widows: 3;
160
+ }
161
+
162
+ /* Callout titles should stay with their body; don't let a pagebreak
163
+ * split the title from its explanation. */
164
+ .callout-title {
165
+ break-after: avoid;
166
+ }
167
+
168
+ /* Chapter header is introductory material; the page break should
169
+ * land either before it or after the header's content area — never
170
+ * mid-header, and never orphaning the h1 from the first paragraph. */
171
+ .chapter-header {
172
+ break-after: avoid;
173
+ }
174
+ .chapter-header h1 {
175
+ break-before: auto; /* overrides the general h1 break-before:page */
176
+ }
177
+
178
+ /* Headings followed by content: don't let a break leave a heading
179
+ * alone at the end of a page. */
180
+ h2, h3, h4 {
181
+ break-inside: avoid;
182
+ }
183
+ h2 + p, h3 + p, h4 + p {
184
+ break-before: avoid;
185
+ }
186
+
187
+ /* Lists: keep small lists together. */
188
+ ul, ol {
189
+ break-inside: avoid-page;
190
+ }
191
+
192
+ /* Tables: try not to split short tables across pages. */
193
+ table {
194
+ break-inside: avoid-page;
195
+ }
196
+
197
+ /* Hide the caret on <details> TOC — it's already hidden by chrome
198
+ * display:none above, but belt-and-suspenders for printers that
199
+ * ignore the rule. */
200
+ details > summary {
201
+ list-style: none;
202
+ }
203
+ }