create-website-build-kit 0.1.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 (91) hide show
  1. package/README.md +54 -0
  2. package/index.mjs +149 -0
  3. package/package.json +42 -0
  4. package/template/.dev.vars.example +3 -0
  5. package/template/.github/workflows/gates.yml +58 -0
  6. package/template/.node-version +1 -0
  7. package/template/.pa11yci.json +24 -0
  8. package/template/BUILD-STATE.md +47 -0
  9. package/template/CLAUDE.md +153 -0
  10. package/template/astro.config.mjs +150 -0
  11. package/template/docs/analytics.md +86 -0
  12. package/template/docs/content.md +138 -0
  13. package/template/docs/handover.md +182 -0
  14. package/template/docs/handover.pdf +0 -0
  15. package/template/docs/runbook.md +661 -0
  16. package/template/docs/traps.md +903 -0
  17. package/template/gitignore +31 -0
  18. package/template/package-lock.json +8159 -0
  19. package/template/package.json +53 -0
  20. package/template/public/_headers +61 -0
  21. package/template/public/_redirects +39 -0
  22. package/template/public/site.webmanifest +13 -0
  23. package/template/scripts/a11y-evidence.mjs +258 -0
  24. package/template/scripts/check-console.mjs +125 -0
  25. package/template/scripts/check-env.mjs +99 -0
  26. package/template/scripts/check-reflow.mjs +148 -0
  27. package/template/scripts/check-sitemap.mjs +113 -0
  28. package/template/scripts/dns-snapshot.mjs +267 -0
  29. package/template/scripts/extract.mjs +317 -0
  30. package/template/scripts/indexnow.mjs +154 -0
  31. package/template/scripts/lastmod.mjs +147 -0
  32. package/template/scripts/lib/inventory.mjs +104 -0
  33. package/template/scripts/lib/preserved.mjs +42 -0
  34. package/template/scripts/lib/routes.mjs +92 -0
  35. package/template/scripts/md-to-pdf.mjs +335 -0
  36. package/template/scripts/og-cards.config.mjs +114 -0
  37. package/template/scripts/og-cards.mjs +487 -0
  38. package/template/scripts/optimize-media.mjs +380 -0
  39. package/template/scripts/recon.mjs +480 -0
  40. package/template/scripts/redirects.mjs +298 -0
  41. package/template/scripts/shots.mjs +447 -0
  42. package/template/scripts/staging-headers.mjs +102 -0
  43. package/template/scripts/tells.mjs +268 -0
  44. package/template/scripts/verify.mjs +1069 -0
  45. package/template/src/components/ContactForm.astro +405 -0
  46. package/template/src/components/CtaBand.astro +82 -0
  47. package/template/src/components/EnvBadge.astro +146 -0
  48. package/template/src/components/Footer.astro +210 -0
  49. package/template/src/components/Header.astro +530 -0
  50. package/template/src/components/Icon.astro +56 -0
  51. package/template/src/components/Img.astro +129 -0
  52. package/template/src/components/PageHero.astro +88 -0
  53. package/template/src/components/Seo.astro +119 -0
  54. package/template/src/components/StructuredData.astro +173 -0
  55. package/template/src/content/blog/.gitkeep +5 -0
  56. package/template/src/content/legal/.gitkeep +0 -0
  57. package/template/src/content.config.ts +81 -0
  58. package/template/src/data/areas.ts +31 -0
  59. package/template/src/data/business.ts +121 -0
  60. package/template/src/data/categories.ts +37 -0
  61. package/template/src/data/fonts.ts +25 -0
  62. package/template/src/data/image-manifest.json +1 -0
  63. package/template/src/data/lastmod.json +1 -0
  64. package/template/src/data/nav.ts +49 -0
  65. package/template/src/data/services.ts +39 -0
  66. package/template/src/data/site.ts +136 -0
  67. package/template/src/env.d.ts +28 -0
  68. package/template/src/layouts/Base.astro +223 -0
  69. package/template/src/lib/brevo.ts +96 -0
  70. package/template/src/lib/hast-media.mjs +55 -0
  71. package/template/src/lib/lastmod.mjs +47 -0
  72. package/template/src/lib/lead.ts +92 -0
  73. package/template/src/lib/legal-routes.mjs +31 -0
  74. package/template/src/lib/legal.ts +75 -0
  75. package/template/src/lib/posts.ts +64 -0
  76. package/template/src/lib/runtime.ts +33 -0
  77. package/template/src/pages/404.astro +51 -0
  78. package/template/src/pages/[slug].astro +111 -0
  79. package/template/src/pages/accessibility.astro +128 -0
  80. package/template/src/pages/api/contact.ts +191 -0
  81. package/template/src/pages/api/leads.csv.ts +82 -0
  82. package/template/src/pages/contact.astro +112 -0
  83. package/template/src/pages/index.astro +84 -0
  84. package/template/src/pages/robots.txt.ts +38 -0
  85. package/template/src/pages/rss.xml.ts +27 -0
  86. package/template/src/styles/global.css +463 -0
  87. package/template/src/styles/project.css +14 -0
  88. package/template/src/styles/prose.css +182 -0
  89. package/template/src/styles/tokens.css +218 -0
  90. package/template/tsconfig.json +5 -0
  91. package/template/wrangler.jsonc +63 -0
@@ -0,0 +1,463 @@
1
+ @import './tokens.css';
2
+ @import './prose.css';
3
+
4
+ /* ───────────────────────────────────────────────────────────────────────────
5
+ Reset, states and layout primitives.
6
+
7
+ What is here: things that are wrong by default in a browser, things that are
8
+ easy to forget (focus, reduced motion, skip link, the states an interactive
9
+ element needs), and the container/section scaffolding every page needs.
10
+
11
+ What is deliberately NOT here: a look. No card style, no button style beyond
12
+ its states, no eyebrow, no section decoration. Those are project decisions —
13
+ the moment they live here, every site built from this template opens the
14
+ same way. Add them per project, in the project's own components.
15
+ ─────────────────────────────────────────────────────────────────────────── */
16
+
17
+ /* ── Fonts ────────────────────────────────────────────────────────────────
18
+ Self-hosted, variable, subset to the scripts you need. Add the @font-face
19
+ blocks HERE and the matching entries to src/data/fonts.ts, which drives the
20
+ preload tags — same-origin means a preload is the whole story.
21
+
22
+ Nothing ships. Two families, display and body; see docs/design.md for the
23
+ subsetting recipe and the licence check.
24
+
25
+ @font-face {
26
+ font-family: 'Display Face';
27
+ src: url('/fonts/display.woff2') format('woff2-variations');
28
+ font-weight: 400 800;
29
+ font-style: normal;
30
+ font-display: swap;
31
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
32
+ U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
33
+ U+2212, U+2215, U+FEFF, U+FFFD;
34
+ }
35
+ ───────────────────────────────────────────────────────────────────────── */
36
+
37
+ /* ── Reset ───────────────────────────────────────────────────────────────── */
38
+
39
+ *,
40
+ *::before,
41
+ *::after {
42
+ box-sizing: border-box;
43
+ }
44
+
45
+ * {
46
+ margin: 0;
47
+ }
48
+
49
+ html {
50
+ -webkit-text-size-adjust: 100%;
51
+ scroll-behavior: smooth;
52
+ /* Anchored headings must clear the sticky header. */
53
+ scroll-padding-top: calc(var(--header-h) + var(--space-m));
54
+ }
55
+
56
+ body {
57
+ font-family: var(--font-body);
58
+ font-size: var(--step-0);
59
+ line-height: var(--leading-normal);
60
+ font-weight: var(--weight-normal);
61
+ color: var(--text);
62
+ background-color: var(--bg);
63
+ -webkit-font-smoothing: antialiased;
64
+ text-rendering: optimizeLegibility;
65
+ font-synthesis-weight: none;
66
+ /*
67
+ * NOT `overflow-x: hidden`. That makes <body> a scroll container and every
68
+ * viewport-rooted IntersectionObserver silently stops firing — which is how
69
+ * scroll-reveal animations die with a clean build and no error. Individual
70
+ * bleeding sections clip themselves instead.
71
+ */
72
+ overflow-x: clip;
73
+ min-height: 100svh;
74
+ display: flex;
75
+ flex-direction: column;
76
+ }
77
+
78
+ img,
79
+ picture,
80
+ video,
81
+ canvas,
82
+ svg {
83
+ display: block;
84
+ max-width: 100%;
85
+ height: auto;
86
+ }
87
+
88
+ input,
89
+ button,
90
+ textarea,
91
+ select {
92
+ font: inherit;
93
+ color: inherit;
94
+ }
95
+
96
+ button {
97
+ background: none;
98
+ border: none;
99
+ cursor: pointer;
100
+ }
101
+
102
+ p,
103
+ h1,
104
+ h2,
105
+ h3,
106
+ h4,
107
+ h5,
108
+ h6,
109
+ li {
110
+ overflow-wrap: break-word;
111
+ }
112
+
113
+ ul[role='list'],
114
+ ol[role='list'] {
115
+ list-style: none;
116
+ padding: 0;
117
+ }
118
+
119
+ :target {
120
+ scroll-margin-top: calc(var(--header-h) + var(--space-m));
121
+ }
122
+
123
+ /* ── Type ────────────────────────────────────────────────────────────────
124
+ The scale binding only. Sizes come from the tokens; the hierarchy a given
125
+ page needs is the page's business. */
126
+
127
+ h1,
128
+ h2,
129
+ h3,
130
+ h4,
131
+ h5,
132
+ h6 {
133
+ font-family: var(--font-display);
134
+ font-weight: var(--weight-semibold);
135
+ line-height: var(--leading-snug);
136
+ letter-spacing: var(--tracking-tight);
137
+ text-wrap: balance;
138
+ color: var(--text);
139
+ }
140
+
141
+ h1 {
142
+ font-size: var(--step-5);
143
+ line-height: var(--leading-tight);
144
+ }
145
+ h2 {
146
+ font-size: var(--step-4);
147
+ line-height: 1.12;
148
+ }
149
+ h3 {
150
+ font-size: var(--step-2);
151
+ }
152
+ h4 {
153
+ font-size: var(--step-1);
154
+ }
155
+ h5,
156
+ h6 {
157
+ font-size: var(--step-0);
158
+ }
159
+
160
+ p {
161
+ text-wrap: pretty;
162
+ }
163
+
164
+ a {
165
+ color: inherit;
166
+ text-decoration-thickness: 1px;
167
+ text-underline-offset: 0.2em;
168
+ }
169
+
170
+ strong,
171
+ b {
172
+ font-weight: var(--weight-semibold);
173
+ }
174
+
175
+ /* ── Focus ───────────────────────────────────────────────────────────────
176
+ Style this to match the design — do not remove it. A focus ring that is the
177
+ browser default, or absent, is on the tells list for a reason. */
178
+
179
+ :focus-visible {
180
+ outline: 3px solid var(--focus);
181
+ outline-offset: 3px;
182
+ border-radius: var(--radius-xs);
183
+ }
184
+
185
+ /* Never remove the outline outright — only defer to :focus-visible. */
186
+ :focus:not(:focus-visible) {
187
+ outline: none;
188
+ }
189
+
190
+ .skip-link {
191
+ position: absolute;
192
+ top: var(--space-2xs);
193
+ left: var(--space-2xs);
194
+ z-index: var(--z-skip);
195
+ padding: var(--space-2xs) var(--space-s);
196
+ background: var(--ink-900);
197
+ color: var(--white);
198
+ border-radius: var(--radius-s);
199
+ font-weight: var(--weight-medium);
200
+ text-decoration: none;
201
+ transform: translateY(-200%);
202
+ }
203
+ .skip-link:focus {
204
+ transform: translateY(0);
205
+ }
206
+
207
+ /* ── Layout primitives ───────────────────────────────────────────────────── */
208
+
209
+ .container {
210
+ width: 100%;
211
+ max-width: calc(var(--width-max) + var(--gutter) * 2);
212
+ margin-inline: auto;
213
+ padding-inline: var(--gutter);
214
+ }
215
+
216
+ .container--wide {
217
+ max-width: calc(var(--width-wide) + var(--gutter) * 2);
218
+ }
219
+
220
+ .container--text {
221
+ max-width: calc(var(--width-text) + var(--gutter) * 2);
222
+ }
223
+
224
+ /* Three rhythms, not one. Alternating them is what gives a page a structure
225
+ the eye can navigate; every section on --section-y is the tell that nobody
226
+ decided. */
227
+ .section {
228
+ padding-block: var(--section-y);
229
+ }
230
+
231
+ .section--tight {
232
+ padding-block: var(--section-y-tight);
233
+ }
234
+
235
+ .section--loose {
236
+ padding-block: var(--section-y-loose);
237
+ }
238
+
239
+ /*
240
+ * The header is fixed, so every page's first section has to reserve its
241
+ * height or the heading sits behind the nav. Applied by each opening section
242
+ * rather than a blanket `main > :first-child` rule, which a scoped component
243
+ * style would out-specify.
244
+ */
245
+ .under-header {
246
+ padding-block-start: calc(var(--header-h) + var(--space-2xl));
247
+ }
248
+
249
+ .stack > * + * {
250
+ margin-block-start: var(--flow, var(--space-s));
251
+ }
252
+
253
+ .visually-hidden {
254
+ position: absolute;
255
+ width: 1px;
256
+ height: 1px;
257
+ padding: 0;
258
+ margin: -1px;
259
+ overflow: hidden;
260
+ clip-path: inset(50%);
261
+ white-space: nowrap;
262
+ border: 0;
263
+ }
264
+
265
+ /* ── Interactive states ──────────────────────────────────────────────────
266
+ * These carry the STATES, not the style.
267
+ *
268
+ * Every interactive element needs hover, focus-visible, active, disabled and
269
+ * busy, and the busy state is the one that gets forgotten until a visitor
270
+ * double-submits a form. What a button LOOKS like is a project decision —
271
+ * restyle these freely, override them entirely, but do not ship a control
272
+ * that is missing a state.
273
+ *
274
+ * They live in the global sheet on purpose: Astro stamps its scoping
275
+ * attribute only on elements written in a component's own template, so a
276
+ * class passed *into* a child as a prop — or an element created at runtime by
277
+ * JS — never matches the parent's scoped rule. Anything crossing a component
278
+ * boundary has to be global or it silently does nothing.
279
+ */
280
+
281
+ .btn {
282
+ --btn-bg: var(--ink-900);
283
+ --btn-fg: var(--white);
284
+ --btn-border: transparent;
285
+ display: inline-flex;
286
+ align-items: center;
287
+ justify-content: center;
288
+ gap: var(--space-2xs);
289
+ /* 44px minimum tap target, from the padding rather than a fixed height, so
290
+ it still works when the label wraps. */
291
+ min-block-size: 2.75rem;
292
+ padding: 0.7em 1.4em;
293
+ font-family: var(--font-body);
294
+ font-size: var(--step-0);
295
+ font-weight: var(--weight-semibold);
296
+ line-height: 1.1;
297
+ text-decoration: none;
298
+ color: var(--btn-fg);
299
+ background: var(--btn-bg);
300
+ border: 1.5px solid var(--btn-border);
301
+ border-radius: var(--radius-s);
302
+ cursor: pointer;
303
+ transition:
304
+ background-color var(--dur-base) var(--ease-out),
305
+ border-color var(--dur-base) var(--ease-out),
306
+ color var(--dur-base) var(--ease-out);
307
+ }
308
+
309
+ /*
310
+ * ── Third-party CSS defence. Do not "simplify" this away. ──────────────────
311
+ *
312
+ * Vendor widgets — chat, booking, reviews — inject their own stylesheet into
313
+ * the page, and it lands on YOUR elements. Measured on a live site: enabling a
314
+ * chat widget took three pages from 0 accessibility errors to failing colour
315
+ * contrast on `.btn` and `.btn--ghost`. Proven by removing the widget and
316
+ * re-running — the same three pages returned to 0, nothing else changed.
317
+ *
318
+ * `.btn.btn` is the same element matched twice, purely to raise specificity
319
+ * above anything a vendor drops in at (0,1,0). It costs nothing and it is the
320
+ * only thing standing between a supplier's release and your contrast ratios.
321
+ *
322
+ * It looks exactly like something a tidy-minded person deletes. It is not.
323
+ */
324
+ .btn.btn {
325
+ color: var(--btn-fg);
326
+ background: var(--btn-bg);
327
+ border-color: var(--btn-border);
328
+ }
329
+
330
+
331
+ .btn:hover {
332
+ --btn-bg: var(--ink-700);
333
+ }
334
+
335
+ .btn[aria-disabled='true'],
336
+ .btn:disabled,
337
+ .btn[data-busy] {
338
+ opacity: 0.6;
339
+ pointer-events: none;
340
+ }
341
+
342
+ /* A second variant, because "primary and one other" is the minimum a page
343
+ needs and getting the borders right on an inverted section is fiddly. */
344
+ .btn--ghost {
345
+ --btn-bg: transparent;
346
+ --btn-fg: var(--text);
347
+ --btn-border: var(--border-strong);
348
+ }
349
+ .btn--ghost:hover {
350
+ --btn-bg: transparent;
351
+ --btn-border: var(--text);
352
+ }
353
+
354
+ .on-dark .btn {
355
+ --btn-bg: var(--white);
356
+ --btn-fg: var(--ink-900);
357
+ }
358
+ .on-dark .btn:hover {
359
+ --btn-bg: var(--ink-100);
360
+ }
361
+ .on-dark .btn--ghost {
362
+ --btn-bg: transparent;
363
+ --btn-fg: var(--white);
364
+ --btn-border: var(--border-strong);
365
+ }
366
+
367
+ /* ── Motion ──────────────────────────────────────────────────────────────
368
+ * Reveal-on-scroll. The hidden state is applied by the script in Base.astro
369
+ * at runtime (it sets [data-reveal-ready]), NEVER by this stylesheet alone —
370
+ * a CSS rule that JS is expected to undo shows a blank page whenever the
371
+ * script fails to load. Without JS these elements are simply visible.
372
+ *
373
+ * Opt in per element with data-reveal. Nothing in the template uses it.
374
+ */
375
+
376
+ [data-reveal-ready] [data-reveal] {
377
+ opacity: 0;
378
+ transform: translateY(1.25rem);
379
+ transition:
380
+ opacity var(--dur-slow) var(--ease-out),
381
+ transform var(--dur-slow) var(--ease-out);
382
+ transition-delay: var(--reveal-delay, 0ms);
383
+ will-change: opacity, transform;
384
+ }
385
+
386
+ [data-reveal-ready] [data-reveal].is-revealed {
387
+ opacity: 1;
388
+ transform: none;
389
+ will-change: auto;
390
+ }
391
+
392
+ /* ── Page transitions ────────────────────────────────────────────────────
393
+ * Cross-fade with a short lift. Deliberately quick — a navigation that takes
394
+ * 500ms to resolve feels broken, not designed. The header is marked
395
+ * `transition:persist`, so it stays put while the page beneath it changes.
396
+ */
397
+
398
+ ::view-transition-old(root) {
399
+ animation: page-out 180ms var(--ease-in-out) both;
400
+ }
401
+
402
+ ::view-transition-new(root) {
403
+ animation: page-in 320ms var(--ease-out) both;
404
+ }
405
+
406
+ @keyframes page-out {
407
+ to {
408
+ opacity: 0;
409
+ transform: translateY(-6px);
410
+ }
411
+ }
412
+
413
+ @keyframes page-in {
414
+ from {
415
+ opacity: 0;
416
+ transform: translateY(10px);
417
+ }
418
+ }
419
+
420
+ /* The persisted header must not be animated by the root transition. */
421
+ ::view-transition-group(site-header) {
422
+ animation-duration: 0ms;
423
+ }
424
+
425
+ /* Astro's fallback path for browsers without native view transitions. */
426
+ [data-astro-transition-fallback='old'] main {
427
+ animation: page-out 180ms var(--ease-in-out) both;
428
+ }
429
+
430
+ [data-astro-transition-fallback='new'] main {
431
+ animation: page-in 320ms var(--ease-out) both;
432
+ }
433
+
434
+ @media (prefers-reduced-motion: reduce) {
435
+ html {
436
+ scroll-behavior: auto;
437
+ }
438
+
439
+ ::view-transition-old(root),
440
+ ::view-transition-new(root),
441
+ [data-astro-transition-fallback='old'] main,
442
+ [data-astro-transition-fallback='new'] main {
443
+ animation: none !important;
444
+ }
445
+ *,
446
+ *::before,
447
+ *::after {
448
+ animation-duration: 0.01ms !important;
449
+ animation-iteration-count: 1 !important;
450
+ transition-duration: 0.01ms !important;
451
+ scroll-behavior: auto !important;
452
+ }
453
+ [data-reveal-ready] [data-reveal] {
454
+ opacity: 1;
455
+ transform: none;
456
+ }
457
+ }
458
+
459
+ @media print {
460
+ .no-print {
461
+ display: none !important;
462
+ }
463
+ }
@@ -0,0 +1,14 @@
1
+ /*
2
+ * ── THIS PROJECT'S LOOK. Everything that is not the kit. ───────────────────
3
+ *
4
+ * global.css carries interactive STATES — focus rings, disabled, hover,
5
+ * reduced-motion. It is not the place for a card, a hero or a section style.
6
+ *
7
+ * The seam exists because without it those land in global.css anyway, and the
8
+ * next site built from this kit inherits the last one's furniture.
9
+ *
10
+ * Load order: tokens.css → prose.css → global.css → this file. The first two are
11
+ * `@import`ed at the top of global.css, so global's own rules land after both.
12
+ * If a rule here needs !important to win, the rule it is fighting probably
13
+ * belongs here too.
14
+ */
@@ -0,0 +1,182 @@
1
+ /*
2
+ * Long-form article body.
3
+ *
4
+ * Global, not scoped: markdown renders to elements Astro never sees in a
5
+ * component template, so a scoped rule would never match them.
6
+ */
7
+
8
+ .prose {
9
+ font-size: var(--step-0);
10
+ line-height: var(--leading-relaxed);
11
+ color: var(--ink-700);
12
+ max-inline-size: var(--width-text);
13
+ }
14
+
15
+ .prose > * + * {
16
+ margin-block-start: var(--space-m);
17
+ }
18
+
19
+ .prose h2 {
20
+ margin-block-start: var(--space-2xl);
21
+ font-size: var(--step-3);
22
+ line-height: 1.15;
23
+ color: var(--ink-900);
24
+ scroll-margin-block-start: calc(var(--header-h) + var(--space-m));
25
+ }
26
+
27
+ .prose h3 {
28
+ margin-block-start: var(--space-xl);
29
+ font-size: var(--step-1);
30
+ color: var(--ink-900);
31
+ scroll-margin-block-start: calc(var(--header-h) + var(--space-m));
32
+ }
33
+
34
+ .prose h4 {
35
+ margin-block-start: var(--space-l);
36
+ font-size: var(--step-0);
37
+ font-family: var(--font-body);
38
+ font-weight: var(--weight-semibold);
39
+ letter-spacing: var(--tracking-wide);
40
+ text-transform: uppercase;
41
+ color: var(--text-muted);
42
+ }
43
+
44
+ .prose h2 + *,
45
+ .prose h3 + *,
46
+ .prose h4 + * {
47
+ margin-block-start: var(--space-s);
48
+ }
49
+
50
+ .prose p {
51
+ text-wrap: pretty;
52
+ }
53
+
54
+ .prose a {
55
+ color: var(--brand-700);
56
+ font-weight: var(--weight-medium);
57
+ text-decoration: underline;
58
+ text-decoration-color: color-mix(in oklab, var(--brand-500) 45%, transparent);
59
+ text-decoration-thickness: 1.5px;
60
+ text-underline-offset: 0.18em;
61
+ transition: text-decoration-color var(--dur-fast) var(--ease-out);
62
+ }
63
+
64
+ .prose a:hover {
65
+ text-decoration-color: var(--brand-500);
66
+ }
67
+
68
+ .prose strong {
69
+ font-weight: var(--weight-semibold);
70
+ color: var(--ink-900);
71
+ }
72
+
73
+ .prose ul,
74
+ .prose ol {
75
+ padding-inline-start: var(--space-m);
76
+ }
77
+
78
+ .prose li + li {
79
+ margin-block-start: var(--space-2xs);
80
+ }
81
+
82
+ .prose li::marker {
83
+ color: var(--brand-500);
84
+ }
85
+
86
+ .prose ol li::marker {
87
+ font-weight: var(--weight-semibold);
88
+ }
89
+
90
+ .prose img {
91
+ inline-size: 100%;
92
+ border-radius: var(--radius-l);
93
+ margin-block: var(--space-xl);
94
+ }
95
+
96
+ .prose figure {
97
+ margin-block: var(--space-xl);
98
+ }
99
+
100
+ .prose figcaption {
101
+ margin-block-start: var(--space-2xs);
102
+ font-size: var(--step--1);
103
+ color: var(--text-muted);
104
+ text-align: center;
105
+ }
106
+
107
+ .prose blockquote {
108
+ margin-block: var(--space-xl);
109
+ padding: var(--space-m) var(--space-l);
110
+ border-inline-start: 3px solid var(--brand-500);
111
+ background: var(--brand-50);
112
+ border-radius: 0 var(--radius-m) var(--radius-m) 0;
113
+ font-size: var(--step-1);
114
+ line-height: var(--leading-normal);
115
+ color: var(--ink-800);
116
+ }
117
+
118
+ .prose blockquote p + p {
119
+ margin-block-start: var(--space-s);
120
+ }
121
+
122
+ .prose hr {
123
+ margin-block: var(--space-2xl);
124
+ border: 0;
125
+ border-block-start: 1px solid var(--border);
126
+ }
127
+
128
+ .prose code {
129
+ padding: 0.15em 0.4em;
130
+ font-family: var(--font-mono);
131
+ font-size: 0.9em;
132
+ background: var(--bg-subtle);
133
+ border: 1px solid var(--border);
134
+ border-radius: var(--radius-xs);
135
+ }
136
+
137
+ .prose pre {
138
+ margin-block: var(--space-l);
139
+ padding: var(--space-m);
140
+ overflow-x: auto;
141
+ background: var(--ink-900);
142
+ border-radius: var(--radius-m);
143
+ font-size: var(--step--1);
144
+ }
145
+
146
+ .prose pre code {
147
+ padding: 0;
148
+ background: none;
149
+ border: 0;
150
+ }
151
+
152
+ /* Wide tables scroll inside their own box; the page body never does. */
153
+ .prose .table-scroll {
154
+ margin-block: var(--space-l);
155
+ overflow-x: auto;
156
+ border: 1px solid var(--border);
157
+ border-radius: var(--radius-m);
158
+ }
159
+
160
+ .prose table {
161
+ inline-size: 100%;
162
+ border-collapse: collapse;
163
+ font-size: var(--step--1);
164
+ }
165
+
166
+ .prose th,
167
+ .prose td {
168
+ padding: var(--space-2xs) var(--space-s);
169
+ text-align: start;
170
+ border-block-end: 1px solid var(--border);
171
+ }
172
+
173
+ .prose th {
174
+ font-weight: var(--weight-semibold);
175
+ color: var(--ink-900);
176
+ background: var(--bg-subtle);
177
+ white-space: nowrap;
178
+ }
179
+
180
+ .prose tr:last-child td {
181
+ border-block-end: 0;
182
+ }