@wxxb789/anc 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/astro.config.mjs +255 -0
- package/bin/anc.mjs +798 -0
- package/package.json +93 -0
- package/public/_headers +45 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/fonts/Temml.woff2 +0 -0
- package/scripts/build-snapshot.js +69 -0
- package/scripts/build-wasm.js +67 -0
- package/scripts/build-workspace.js +50 -0
- package/scripts/copy-snapshot.js +48 -0
- package/scripts/copy-wasm.js +57 -0
- package/scripts/emit-redirects.js +64 -0
- package/scripts/generate-corpus.js +421 -0
- package/scripts/init-repository.js +249 -0
- package/scripts/load-config.js +975 -0
- package/scripts/markdown-to-artifact.js +1286 -0
- package/scripts/preview-site.js +364 -0
- package/scripts/publish-set-review.js +157 -0
- package/scripts/resolve-links.js +640 -0
- package/scripts/run-pagefind.js +125 -0
- package/scripts/scan-residue.js +1016 -0
- package/scripts/scan-secrets.js +268 -0
- package/scripts/seed-gitignore.js +410 -0
- package/scripts/snapshot-rows.js +205 -0
- package/scripts/validate-content.js +95 -0
- package/scripts/vendor-provenance.js +307 -0
- package/scripts/verify-output-inventory.js +307 -0
- package/scripts/write-report.js +351 -0
- package/scripts/write-snapshot.js +136 -0
- package/src/components/CollectionExplorer.astro +149 -0
- package/src/components/FacetIndex.astro +45 -0
- package/src/components/LinkedNotes.astro +94 -0
- package/src/components/NoteGraph.astro +434 -0
- package/src/components/NoteList.astro +54 -0
- package/src/components/SiteMetadata.astro +143 -0
- package/src/components/TableOfContents.astro +69 -0
- package/src/components/TagBrowser.astro +70 -0
- package/src/layouts/Layout.astro +305 -0
- package/src/lib/artifact-source.js +72 -0
- package/src/lib/build-bindings.js +11 -0
- package/src/lib/collection-navigation.js +145 -0
- package/src/lib/content.js +92 -0
- package/src/lib/diagram-mode.js +18 -0
- package/src/lib/graph-layout.js +219 -0
- package/src/lib/graph-selection.js +84 -0
- package/src/lib/graph.js +42 -0
- package/src/lib/link-resolution.js +389 -0
- package/src/lib/markdown.js +1469 -0
- package/src/lib/math-mode.js +31 -0
- package/src/lib/math-output.js +131 -0
- package/src/lib/math.js +165 -0
- package/src/lib/mermaid-environment.js +475 -0
- package/src/lib/mermaid-render.js +769 -0
- package/src/lib/preview-model.js +96 -0
- package/src/lib/relations.js +242 -0
- package/src/lib/rendered-marker.js +55 -0
- package/src/lib/route-path.js +192 -0
- package/src/lib/routes.js +342 -0
- package/src/lib/schema.js +578 -0
- package/src/lib/site.js +578 -0
- package/src/lib/snapshot-contract.js +216 -0
- package/src/lib/snapshot-operations.js +147 -0
- package/src/lib/snapshot-queries.js +216 -0
- package/src/lib/snapshot-reader.js +210 -0
- package/src/lib/snapshot.js +233 -0
- package/src/lib/sqlite-warning.js +22 -0
- package/src/lib/sqlite.js +14 -0
- package/src/lib/tag-browser-model.js +18 -0
- package/src/lib/translations.js +533 -0
- package/src/lib/wasm-asset.js +47 -0
- package/src/lib/worker-protocol.js +93 -0
- package/src/pages/404.astro +35 -0
- package/src/pages/about.astro +100 -0
- package/src/pages/collections/[slug].astro +36 -0
- package/src/pages/collections/index.astro +33 -0
- package/src/pages/graph.astro +64 -0
- package/src/pages/index.astro +33 -0
- package/src/pages/notes/[slug].astro +509 -0
- package/src/pages/privacy.astro +121 -0
- package/src/pages/private.astro +59 -0
- package/src/pages/recent.astro +33 -0
- package/src/pages/robots.txt.js +15 -0
- package/src/pages/rss.xml.js +21 -0
- package/src/pages/sitemap.xml.js +20 -0
- package/src/pages/tags/[tag].astro +56 -0
- package/src/pages/tags/index.astro +37 -0
- package/src/scripts/code-copy.js +45 -0
- package/src/scripts/diagram-disabled.js +16 -0
- package/src/scripts/diagram.js +322 -0
- package/src/scripts/graph-client.js +222 -0
- package/src/scripts/graph-draw.js +166 -0
- package/src/scripts/link-preview.js +334 -0
- package/src/scripts/math-disabled.js +18 -0
- package/src/scripts/math.js +133 -0
- package/src/scripts/preferences.js +123 -0
- package/src/scripts/search-dialog.js +651 -0
- package/src/scripts/snapshot-client.js +157 -0
- package/src/scripts/snapshot-worker.js +341 -0
- package/src/scripts/tag-browser.js +153 -0
- package/src/scripts/theme-init.js +35 -0
- package/src/scripts/toc-active.js +38 -0
- package/src/styles/code.css +152 -0
- package/src/styles/diagram.css +149 -0
- package/src/styles/global.css +1739 -0
- package/src/styles/graph.css +317 -0
- package/src/styles/math.css +376 -0
- package/src/styles/tokens.css +171 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,1739 @@
|
|
|
1
|
+
@import './tokens.css';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Layout shell and reading surface.
|
|
5
|
+
*
|
|
6
|
+
* Everything here is plain CSS with no JavaScript dependency: the theme follows
|
|
7
|
+
* `prefers-color-scheme` through `light-dark()` in `tokens.css`, and the two
|
|
8
|
+
* preference toggles only override an attribute that CSS already understands.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
*,
|
|
12
|
+
*::before,
|
|
13
|
+
*::after {
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
html {
|
|
18
|
+
scroll-behavior: smooth;
|
|
19
|
+
/* Keeps an in-page anchor target clear of the sticky header. The header
|
|
20
|
+
wraps to several rows at 320 px, so the mobile-first value allows for
|
|
21
|
+
that and the wide-viewport override below tightens it. */
|
|
22
|
+
scroll-padding-top: 9rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
body {
|
|
26
|
+
margin: 0;
|
|
27
|
+
background: var(--color-bg);
|
|
28
|
+
color: var(--color-text);
|
|
29
|
+
font-family: var(--font-sans);
|
|
30
|
+
font-size: var(--text-base);
|
|
31
|
+
line-height: var(--leading-body);
|
|
32
|
+
/* A long unbroken token (a URL, a hash) must wrap rather than widen the page. */
|
|
33
|
+
overflow-wrap: break-word;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* Mixed zh-CN / English line breaking.
|
|
38
|
+
*
|
|
39
|
+
* `word-break: normal` keeps English words whole; CJK already breaks between
|
|
40
|
+
* characters, so nothing extra is needed to wrap Chinese text. `line-break:
|
|
41
|
+
* strict` stops a line from starting with a CJK closing bracket or ending with
|
|
42
|
+
* an opening one, which is the visible failure mode in bilingual prose.
|
|
43
|
+
*/
|
|
44
|
+
:where(body) {
|
|
45
|
+
word-break: normal;
|
|
46
|
+
line-break: strict;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
:is(h1, h2, h3, h4, h5, h6) {
|
|
50
|
+
line-height: var(--leading-tight);
|
|
51
|
+
text-wrap: balance;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
p,
|
|
55
|
+
li {
|
|
56
|
+
text-wrap: pretty;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
a {
|
|
60
|
+
color: var(--color-link);
|
|
61
|
+
text-underline-offset: 0.2em;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Focus is never signalled by colour alone: the ring is a shape change too. */
|
|
65
|
+
:focus-visible {
|
|
66
|
+
outline: 2px solid var(--color-focus);
|
|
67
|
+
outline-offset: 2px;
|
|
68
|
+
border-radius: var(--radius-sm);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
:is(img, svg, video) {
|
|
72
|
+
max-width: 100%;
|
|
73
|
+
height: auto;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.skip-link {
|
|
77
|
+
position: absolute;
|
|
78
|
+
top: var(--space-2);
|
|
79
|
+
left: var(--space-2);
|
|
80
|
+
z-index: 100;
|
|
81
|
+
padding: var(--space-2) var(--space-4);
|
|
82
|
+
border: 1px solid var(--color-line-strong);
|
|
83
|
+
border-radius: var(--radius-sm);
|
|
84
|
+
background: var(--color-surface);
|
|
85
|
+
color: var(--color-text);
|
|
86
|
+
box-shadow: var(--elevation-2);
|
|
87
|
+
/* Off-screen until focused, rather than `display: none`, so it stays tabbable. */
|
|
88
|
+
transform: translateY(calc(-100% - var(--space-4)));
|
|
89
|
+
transition: transform var(--motion-fast) var(--ease);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.skip-link:focus-visible {
|
|
93
|
+
transform: translateY(0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
* Controls that only work with JavaScript. `theme-init.js` sets `data-js` on
|
|
98
|
+
* the root before first paint, so they never appear as dead buttons.
|
|
99
|
+
*
|
|
100
|
+
* The selector deliberately outranks `.site-nav a, .site-nav button` below: a
|
|
101
|
+
* bare `[data-js-only]` is (0,1,0) and loses to that rule's (0,1,1), which
|
|
102
|
+
* would leave these visible with scripting disabled — the failure this rule
|
|
103
|
+
* exists to prevent. `tests/built-output.test.ts` resolves the cascade.
|
|
104
|
+
*/
|
|
105
|
+
:root:not([data-js='on']) [data-js-only] {
|
|
106
|
+
display: none;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ---------------------------------------------------------------- shell -- */
|
|
110
|
+
|
|
111
|
+
.site-header {
|
|
112
|
+
position: sticky;
|
|
113
|
+
top: 0;
|
|
114
|
+
z-index: 10;
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-wrap: wrap;
|
|
117
|
+
gap: var(--space-3);
|
|
118
|
+
justify-content: space-between;
|
|
119
|
+
align-items: center;
|
|
120
|
+
padding: var(--space-3) var(--gutter);
|
|
121
|
+
border-bottom: 1px solid var(--color-line);
|
|
122
|
+
background: var(--color-bg);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.brand {
|
|
126
|
+
color: var(--color-text);
|
|
127
|
+
font-weight: 700;
|
|
128
|
+
letter-spacing: -0.03em;
|
|
129
|
+
text-decoration: none;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.site-nav {
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-wrap: wrap;
|
|
135
|
+
gap: var(--space-2);
|
|
136
|
+
align-items: center;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.site-nav a,
|
|
140
|
+
.site-nav button {
|
|
141
|
+
display: inline-flex;
|
|
142
|
+
gap: var(--space-1);
|
|
143
|
+
align-items: center;
|
|
144
|
+
padding: var(--space-1) var(--space-2);
|
|
145
|
+
border: 1px solid transparent;
|
|
146
|
+
border-radius: var(--radius-sm);
|
|
147
|
+
background: none;
|
|
148
|
+
color: var(--color-muted);
|
|
149
|
+
font: inherit;
|
|
150
|
+
font-size: var(--text-sm);
|
|
151
|
+
text-decoration: none;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.site-nav a:hover,
|
|
156
|
+
.site-nav button:hover {
|
|
157
|
+
border-color: var(--color-line-strong);
|
|
158
|
+
color: var(--color-text);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Pressed state carries a border and a background, not just a colour shift. */
|
|
162
|
+
.site-nav button[aria-pressed='true'] {
|
|
163
|
+
border-color: var(--color-accent);
|
|
164
|
+
background: var(--color-wash);
|
|
165
|
+
color: var(--color-text);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main {
|
|
169
|
+
width: min(var(--width-page), 100%);
|
|
170
|
+
margin: 0 auto;
|
|
171
|
+
padding: var(--space-12) var(--gutter) var(--space-16);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* `#main` is focused programmatically by the skip link and carries
|
|
175
|
+
`tabindex="-1"`, so it is never reached by Tab. Suppress the ring only for
|
|
176
|
+
that programmatic case; `:focus-visible` keeps it wherever a browser judges
|
|
177
|
+
the focus worth showing. */
|
|
178
|
+
main:focus:not(:focus-visible) {
|
|
179
|
+
outline: none;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* ------------------------------------------------------------ explorer -- */
|
|
183
|
+
|
|
184
|
+
/*
|
|
185
|
+
* The collection rail (TK-05c).
|
|
186
|
+
*
|
|
187
|
+
* Below the two-column breakpoint it is a block after the article: one column,
|
|
188
|
+
* every group closed except the reader's own, and the disclosures *are* the
|
|
189
|
+
* drawer requirements section 16 asks for on mobile. The desktop treatment —
|
|
190
|
+
* sticky, beside the article — is the `@media (min-width: 64rem)` block at the
|
|
191
|
+
* end of this file.
|
|
192
|
+
*
|
|
193
|
+
* Nothing here needs JavaScript. `<details>` is the collapse, `position: sticky`
|
|
194
|
+
* is the follow, and `aria-current` is the location; there is no script to load
|
|
195
|
+
* and no state to persist.
|
|
196
|
+
*/
|
|
197
|
+
.explorer {
|
|
198
|
+
width: min(var(--width-page), 100%);
|
|
199
|
+
margin: 0 auto var(--space-16);
|
|
200
|
+
padding: 0 var(--gutter);
|
|
201
|
+
font-size: var(--text-sm);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.explorer-groups,
|
|
205
|
+
.explorer-notes {
|
|
206
|
+
margin: 0;
|
|
207
|
+
padding: 0;
|
|
208
|
+
list-style: none;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.explorer-group {
|
|
212
|
+
border-bottom: 1px solid var(--color-line);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.explorer-group > summary {
|
|
216
|
+
display: flex;
|
|
217
|
+
gap: var(--space-2);
|
|
218
|
+
justify-content: space-between;
|
|
219
|
+
align-items: baseline;
|
|
220
|
+
padding: var(--space-3) var(--space-1);
|
|
221
|
+
color: var(--color-text);
|
|
222
|
+
font-weight: 600;
|
|
223
|
+
cursor: pointer;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* Text, not a badge — same treatment as every other count on the site. */
|
|
227
|
+
.explorer-count {
|
|
228
|
+
color: var(--color-muted);
|
|
229
|
+
font-size: var(--text-xs);
|
|
230
|
+
font-variant-numeric: tabular-nums;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/*
|
|
234
|
+
* The open/close transition, on the native `::details-content` pseudo-element.
|
|
235
|
+
*
|
|
236
|
+
* This is Quartz's `0fr → 1fr` idea reached through the platform instead of
|
|
237
|
+
* through script: `interpolate-size: allow-keywords` lets `block-size` animate
|
|
238
|
+
* between `0` and `auto`, and `content-visibility` is in the transition list
|
|
239
|
+
* with `allow-discrete` so the content is not clipped away before the height
|
|
240
|
+
* has finished collapsing.
|
|
241
|
+
*
|
|
242
|
+
* A `transition`, deliberately **not** an `animation`, and that distinction is a
|
|
243
|
+
* WCAG fix rather than a stylistic one. An `animation` on `[open]` runs when the
|
|
244
|
+
* page *loads*, because the reader's own group is already open in the served
|
|
245
|
+
* HTML — so the rail faded in from `opacity: 0` on every navigation, and the
|
|
246
|
+
* out-of-band axe audit caught the links mid-fade at a computed 4.44:1 against
|
|
247
|
+
* the 4.5 threshold, on 245 nodes across both corpora. A transition fires only
|
|
248
|
+
* on a state change, so an already-open group is simply open at first paint and
|
|
249
|
+
* there is no partial-opacity frame for anything to measure. That audit is run
|
|
250
|
+
* from `.tmp/axe-audit.mjs` against an `axe.min.js` on disk — axe-core is in
|
|
251
|
+
* neither `dependencies` nor `devDependencies`, so no gate in this repository
|
|
252
|
+
* recomputes the number and TK-09 owns whether one should.
|
|
253
|
+
*
|
|
254
|
+
* `::details-content` rather than Quartz's `grid-template-rows: 0fr → 1fr`,
|
|
255
|
+
* which was the shape L5 named. Both are transitions and either would have
|
|
256
|
+
* fixed the contrast defect; the reason to prefer this one is that
|
|
257
|
+
* `grid-template-rows` needs a wrapper element to be the single grid row, and
|
|
258
|
+
* a `<details>` given `display: grid` loses the browser's own open/closed box
|
|
259
|
+
* behaviour — so that shape costs a `<div>` per group whose only job is to be
|
|
260
|
+
* animated. The trade is browser support: `interpolate-size` is Chromium-only
|
|
261
|
+
* today, where `grid-template-rows` is not. That is why the whole block sits
|
|
262
|
+
* behind `@supports` and the collapse itself is the native `<details>` — a
|
|
263
|
+
* browser that skips this snaps open exactly as it always has, which is a
|
|
264
|
+
* missing animation rather than a missing feature.
|
|
265
|
+
*
|
|
266
|
+
* The `prefers-reduced-motion` block at the end of this file turns the
|
|
267
|
+
* transition off. It needs `*::details-content` in its selector list to do so:
|
|
268
|
+
* a type selector matches elements and never a pseudo-element, so `*` alone
|
|
269
|
+
* left this transition running at its full duration under `reduce`.
|
|
270
|
+
*/
|
|
271
|
+
@supports (interpolate-size: allow-keywords) {
|
|
272
|
+
.explorer-group {
|
|
273
|
+
interpolate-size: allow-keywords;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.explorer-group::details-content {
|
|
277
|
+
block-size: 0;
|
|
278
|
+
/*
|
|
279
|
+
* `clip` with a margin rather than `hidden`, so the focus ring survives.
|
|
280
|
+
*
|
|
281
|
+
* This pseudo-element is a clip box that hugs the links exactly, and the
|
|
282
|
+
* global ring is a 2 px outline at `outline-offset: 2px` — so under
|
|
283
|
+
* `overflow: hidden` every link's ring was cut flat on both horizontal
|
|
284
|
+
* edges. Measured: 0 px of ring reached the left and right of a note link
|
|
285
|
+
* at every width, against the 4 px it needs. The rail's own padding cannot
|
|
286
|
+
* fix this one; it is a second, inner clip box.
|
|
287
|
+
*
|
|
288
|
+
* `overflow-clip-margin` lets the ring paint outside the box while the box
|
|
289
|
+
* still clips the collapse — which `scroll` or `visible` would not, since
|
|
290
|
+
* either would let a closed group's links bleed below its summary. WCAG
|
|
291
|
+
* 2.4.11.
|
|
292
|
+
*/
|
|
293
|
+
overflow: clip;
|
|
294
|
+
overflow-clip-margin: 4px;
|
|
295
|
+
transition:
|
|
296
|
+
block-size var(--motion-fast) var(--ease),
|
|
297
|
+
content-visibility var(--motion-fast) allow-discrete;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.explorer-group[open]::details-content {
|
|
301
|
+
block-size: auto;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.explorer-index,
|
|
306
|
+
.explorer-notes a {
|
|
307
|
+
display: block;
|
|
308
|
+
padding: var(--space-1) var(--space-2);
|
|
309
|
+
border-radius: var(--radius-sm);
|
|
310
|
+
color: var(--color-link);
|
|
311
|
+
text-decoration: none;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.explorer-index {
|
|
315
|
+
color: var(--color-muted);
|
|
316
|
+
font-size: var(--text-xs);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.explorer-notes {
|
|
320
|
+
padding-bottom: var(--space-2);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.explorer-index:hover,
|
|
324
|
+
.explorer-notes a:hover,
|
|
325
|
+
.explorer-index:focus-visible,
|
|
326
|
+
.explorer-notes a:focus-visible {
|
|
327
|
+
background: var(--color-wash);
|
|
328
|
+
text-decoration: underline;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/*
|
|
332
|
+
* A windowed group's own sentence: how many of its notes the rail drew, and
|
|
333
|
+
* where the rest are. Same muted treatment as `.explorer-index`, because it is
|
|
334
|
+
* the same kind of thing — a note about the list rather than a member of it.
|
|
335
|
+
*/
|
|
336
|
+
.explorer-bound {
|
|
337
|
+
display: flex;
|
|
338
|
+
flex-wrap: wrap;
|
|
339
|
+
gap: var(--space-2);
|
|
340
|
+
margin: 0;
|
|
341
|
+
padding: var(--space-1) var(--space-2) var(--space-2);
|
|
342
|
+
color: var(--color-muted);
|
|
343
|
+
font-size: var(--text-xs);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.explorer-bound a {
|
|
347
|
+
color: var(--color-link);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/*
|
|
351
|
+
* The page being read. Three signals, not one: a weight change, a left rule,
|
|
352
|
+
* and the text colour. WCAG 1.4.1 — a reader who cannot distinguish the colour
|
|
353
|
+
* still sees where they are, and `aria-current="page"` says it outright to a
|
|
354
|
+
* screen reader.
|
|
355
|
+
*/
|
|
356
|
+
.explorer-notes a[aria-current='page'] {
|
|
357
|
+
border-left: 3px solid var(--color-accent);
|
|
358
|
+
background: var(--color-wash);
|
|
359
|
+
color: var(--color-text);
|
|
360
|
+
font-weight: 700;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.site-footer {
|
|
364
|
+
padding: var(--space-8) var(--gutter);
|
|
365
|
+
border-top: 1px solid var(--color-line);
|
|
366
|
+
color: var(--color-muted);
|
|
367
|
+
font-size: var(--text-sm);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.site-footer p {
|
|
371
|
+
max-width: var(--measure);
|
|
372
|
+
margin: 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* The site map is the guarantee that no route is an orphan, so it lays out as
|
|
376
|
+
wrapping text links that stay operable at 320 px. */
|
|
377
|
+
.site-map {
|
|
378
|
+
display: flex;
|
|
379
|
+
flex-wrap: wrap;
|
|
380
|
+
gap: var(--space-2) var(--space-4);
|
|
381
|
+
margin-bottom: var(--space-4);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* Shown only on paper — the canonical route belongs on a printed page, not on
|
|
385
|
+
a screen where the address bar already carries it. */
|
|
386
|
+
.print-only {
|
|
387
|
+
display: none;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* ------------------------------------------------------------- surfaces -- */
|
|
391
|
+
|
|
392
|
+
.hero {
|
|
393
|
+
max-width: var(--measure);
|
|
394
|
+
padding-bottom: var(--space-12);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.eyebrow {
|
|
398
|
+
margin: 0;
|
|
399
|
+
color: var(--color-accent);
|
|
400
|
+
font: 700 var(--text-xs) / 1 var(--font-mono);
|
|
401
|
+
letter-spacing: 0.14em;
|
|
402
|
+
text-transform: uppercase;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.hero h1 {
|
|
406
|
+
margin: var(--space-4) 0 var(--space-4);
|
|
407
|
+
font-size: var(--text-3xl);
|
|
408
|
+
letter-spacing: -0.04em;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.hero > p:last-child {
|
|
412
|
+
color: var(--color-muted);
|
|
413
|
+
font-size: var(--text-lg);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.section-heading {
|
|
417
|
+
display: flex;
|
|
418
|
+
flex-wrap: wrap;
|
|
419
|
+
gap: var(--space-2);
|
|
420
|
+
justify-content: space-between;
|
|
421
|
+
align-items: baseline;
|
|
422
|
+
padding-bottom: var(--space-2);
|
|
423
|
+
border-bottom: 1px solid var(--color-line);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.section-heading h2 {
|
|
427
|
+
margin: 0;
|
|
428
|
+
font-size: var(--text-xl);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.section-heading span {
|
|
432
|
+
color: var(--color-muted);
|
|
433
|
+
font-size: var(--text-sm);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* `minmax(0, …)` rather than a fixed floor: a fixed floor is what pushes a
|
|
437
|
+
grid past a 320 px viewport. */
|
|
438
|
+
.note-grid {
|
|
439
|
+
display: grid;
|
|
440
|
+
grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
|
|
441
|
+
gap: var(--space-4);
|
|
442
|
+
margin-top: var(--space-6);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.note-card {
|
|
446
|
+
display: flex;
|
|
447
|
+
flex-direction: column;
|
|
448
|
+
gap: var(--space-2);
|
|
449
|
+
min-width: 0;
|
|
450
|
+
padding: var(--space-6);
|
|
451
|
+
border: 1px solid var(--color-line);
|
|
452
|
+
border-radius: var(--radius-lg);
|
|
453
|
+
background: var(--color-surface);
|
|
454
|
+
box-shadow: var(--elevation-1);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.note-card h3 {
|
|
458
|
+
margin: 0;
|
|
459
|
+
font-size: var(--text-lg);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.note-card h3 a {
|
|
463
|
+
color: var(--color-text);
|
|
464
|
+
text-decoration: none;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.note-card h3 a:hover {
|
|
468
|
+
text-decoration: underline;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.note-card p {
|
|
472
|
+
margin: 0;
|
|
473
|
+
color: var(--color-muted);
|
|
474
|
+
font-size: var(--text-sm);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.read-more {
|
|
478
|
+
margin-top: auto;
|
|
479
|
+
font-size: var(--text-sm);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/* ------------------------------------------------- facets and empty states -- */
|
|
483
|
+
|
|
484
|
+
/*
|
|
485
|
+
* An empty tag or collection index is the normal state for the current
|
|
486
|
+
* projection, not an error, so it reads as ordinary prose rather than as a
|
|
487
|
+
* warning. Nothing here is signalled by colour.
|
|
488
|
+
*/
|
|
489
|
+
.empty-state,
|
|
490
|
+
.facet-note {
|
|
491
|
+
max-width: var(--measure);
|
|
492
|
+
margin: var(--space-6) 0 0;
|
|
493
|
+
color: var(--color-muted);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.facet-list {
|
|
497
|
+
max-width: var(--measure);
|
|
498
|
+
margin: var(--space-6) 0 0;
|
|
499
|
+
padding: 0;
|
|
500
|
+
list-style: none;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.facet-list li {
|
|
504
|
+
display: flex;
|
|
505
|
+
flex-wrap: wrap;
|
|
506
|
+
gap: var(--space-2) var(--space-4);
|
|
507
|
+
justify-content: space-between;
|
|
508
|
+
align-items: baseline;
|
|
509
|
+
padding: var(--space-3) 0;
|
|
510
|
+
border-bottom: 1px solid var(--color-line);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/* Text, not a badge. Same treatment as the facet count, which is the same
|
|
514
|
+
information in the same relationship to a heading — one rule rather than two
|
|
515
|
+
that would drift. */
|
|
516
|
+
.facet-count,
|
|
517
|
+
.relations-count {
|
|
518
|
+
color: var(--color-muted);
|
|
519
|
+
font-size: var(--text-sm);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/* ---------------------------------------------------------------- prose -- */
|
|
523
|
+
|
|
524
|
+
.prose {
|
|
525
|
+
max-width: var(--measure);
|
|
526
|
+
margin-inline: auto;
|
|
527
|
+
min-width: 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.prose h1 {
|
|
531
|
+
font-size: var(--text-2xl);
|
|
532
|
+
letter-spacing: -0.03em;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.prose h2 {
|
|
536
|
+
margin-top: var(--space-12);
|
|
537
|
+
padding-bottom: var(--space-2);
|
|
538
|
+
border-bottom: 1px solid var(--color-line);
|
|
539
|
+
font-size: var(--text-xl);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.prose h3 {
|
|
543
|
+
margin-top: var(--space-8);
|
|
544
|
+
font-size: var(--text-lg);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/* In-content links keep their underline: the link is not signalled by colour
|
|
548
|
+
alone. Navigation and card links are distinguished by position and role. */
|
|
549
|
+
.prose a {
|
|
550
|
+
text-decoration: underline;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.prose pre {
|
|
554
|
+
max-width: 100%;
|
|
555
|
+
overflow-x: auto;
|
|
556
|
+
padding: var(--space-4);
|
|
557
|
+
border: 1px solid var(--color-line);
|
|
558
|
+
border-radius: var(--radius-md);
|
|
559
|
+
background: var(--color-surface-alt);
|
|
560
|
+
font-size: var(--text-sm);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.prose code {
|
|
564
|
+
font-family: var(--font-mono);
|
|
565
|
+
font-size: 0.925em;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.prose :not(pre) > code {
|
|
569
|
+
padding: 0.1em 0.35em;
|
|
570
|
+
border-radius: var(--radius-sm);
|
|
571
|
+
background: var(--color-surface-alt);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.prose blockquote {
|
|
575
|
+
margin-inline: 0;
|
|
576
|
+
padding-left: var(--space-4);
|
|
577
|
+
border-left: 3px solid var(--color-accent);
|
|
578
|
+
color: var(--color-muted);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/* --------------------------------------------------- rendered markdown -- */
|
|
582
|
+
|
|
583
|
+
/*
|
|
584
|
+
* Every class `src/lib/markdown.ts` can emit has a rule below, or is named in
|
|
585
|
+
* the "deliberately unstyled" block at the end of this section, or lives in a
|
|
586
|
+
* per-page stylesheet that loads only where it is needed: the `token-*` map in
|
|
587
|
+
* `src/styles/code.css`, the math classes in `src/styles/math.css`, and the
|
|
588
|
+
* diagram classes in `src/styles/diagram.css`. The allowlist at `markdown.ts`
|
|
589
|
+
* is the list: `heading-anchor`, `code-block`, `language-*`, `token` and
|
|
590
|
+
* `token-*`, `callout` and `callout-*`, `callout-title`, `contains-task-list`,
|
|
591
|
+
* `task-list-item`, `footnotes`, `data-footnote-backref`, `sr-only`,
|
|
592
|
+
* `math-inline`, `math-display`, `diagram`, `diagram-canvas`, `diagram-source`.
|
|
593
|
+
*
|
|
594
|
+
* Quartz's thirteen-kind callout vocabulary and its mask-image icon set are
|
|
595
|
+
* deliberately not imported: the corpus has zero callouts, and a colour-per-kind
|
|
596
|
+
* table is thirteen decisions for a construct nobody has written yet. One
|
|
597
|
+
* neutral callout treatment carries the label, which is the part that means
|
|
598
|
+
* something.
|
|
599
|
+
*/
|
|
600
|
+
|
|
601
|
+
/*
|
|
602
|
+
* The one live accessibility bug in this section. satteri emits
|
|
603
|
+
* `<h2 class="sr-only" id="footnote-label">Footnotes</h2>` and, with no rule for
|
|
604
|
+
* it, that heading rendered as a full-size visible heading above the footnote
|
|
605
|
+
* list — a piece of generated scaffolding presented to the reader as content.
|
|
606
|
+
*
|
|
607
|
+
* The clip-path recipe rather than `display: none` or `visibility: hidden`,
|
|
608
|
+
* because those remove the element from the accessibility tree too, which is
|
|
609
|
+
* the opposite of what a screen-reader-only heading is for.
|
|
610
|
+
*/
|
|
611
|
+
.sr-only {
|
|
612
|
+
position: absolute;
|
|
613
|
+
width: 1px;
|
|
614
|
+
height: 1px;
|
|
615
|
+
margin: -1px;
|
|
616
|
+
padding: 0;
|
|
617
|
+
border: 0;
|
|
618
|
+
overflow: hidden;
|
|
619
|
+
clip-path: inset(50%);
|
|
620
|
+
white-space: nowrap;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/*
|
|
624
|
+
* The heading anchor: a `#` that appears on hover or focus.
|
|
625
|
+
*
|
|
626
|
+
* Hidden until then rather than always visible, because fifteen of them down an
|
|
627
|
+
* article is visual noise — but `opacity`, not `display: none`, so the link
|
|
628
|
+
* stays in the tab order and in the accessibility tree. It carries its own
|
|
629
|
+
* `aria-label` from the renderer, so its `#` is decoration a screen reader never
|
|
630
|
+
* has to read as a heading's name.
|
|
631
|
+
*
|
|
632
|
+
* `.prose .heading-anchor`, not a bare `.heading-anchor`: `.prose a` is (0,1,1)
|
|
633
|
+
* and sets `text-decoration: underline` on every in-content link, so a (0,1,0)
|
|
634
|
+
* rule loses and the `#` ships underlined. The same trap TK-02 documented for
|
|
635
|
+
* `[data-js-only]` against `.site-nav button`.
|
|
636
|
+
*/
|
|
637
|
+
.prose .heading-anchor {
|
|
638
|
+
margin-left: var(--space-2);
|
|
639
|
+
color: var(--color-muted);
|
|
640
|
+
font-weight: 400;
|
|
641
|
+
text-decoration: none;
|
|
642
|
+
opacity: 0;
|
|
643
|
+
transition: opacity var(--motion-fast) var(--ease);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
:is(h1, h2, h3, h4, h5, h6):hover > .heading-anchor,
|
|
647
|
+
.heading-anchor:focus-visible {
|
|
648
|
+
opacity: 1;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/* Touch and keyboard have no hover, so the anchor is simply always there. */
|
|
652
|
+
@media (hover: none) {
|
|
653
|
+
.heading-anchor {
|
|
654
|
+
opacity: 1;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/*
|
|
659
|
+
* A fenced code block. `<figure>` carries a UA `margin-inline: 40px` that would
|
|
660
|
+
* indent every code block away from the prose, so the margin is set explicitly.
|
|
661
|
+
* `min-width: 0` stops the figure from being sized by its widest line — the
|
|
662
|
+
* classic way a code block widens the whole page — leaving `.prose pre`'s own
|
|
663
|
+
* `overflow-x: auto` to do the scrolling.
|
|
664
|
+
*/
|
|
665
|
+
.code-block {
|
|
666
|
+
margin: var(--space-6) 0;
|
|
667
|
+
min-width: 0;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.code-block > pre {
|
|
671
|
+
margin: 0;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/*
|
|
675
|
+
* Syntax highlighting is not here. The `token-*` map lives in
|
|
676
|
+
* `src/styles/code.css`, which `src/pages/notes/[slug].astro` links only when
|
|
677
|
+
* `RenderedNote.hasCode` is true — owner decision 7, heavy assets load per page.
|
|
678
|
+
* `.code-block` stays in this file on purpose: a Mermaid fence renders as one
|
|
679
|
+
* while setting `hasMermaid` rather than `hasCode`, so gating the figure's own
|
|
680
|
+
* rules would leave a diagram-only page with an unstyled block.
|
|
681
|
+
*/
|
|
682
|
+
|
|
683
|
+
/*
|
|
684
|
+
* A callout. One neutral treatment for every kind: the kind is announced by the
|
|
685
|
+
* title text the renderer promotes to `<strong>`, not by a colour a reader has
|
|
686
|
+
* to have learned. `data-callout` is on the element for a later ticket that
|
|
687
|
+
* decides whether differentiating them is worth a second visual vocabulary.
|
|
688
|
+
*
|
|
689
|
+
* The published corpus has none; TK-11's fixture has `note`, `tip`, and
|
|
690
|
+
* `warning`, which is what makes this rule observable at all. That does not
|
|
691
|
+
* change the treatment — it is what confirms the neutral one reads correctly
|
|
692
|
+
* when three kinds sit on one page.
|
|
693
|
+
*
|
|
694
|
+
* `.prose .callout`, not a bare `.callout`: a callout *is* a `<blockquote>`, and
|
|
695
|
+
* `.prose blockquote` (0,1,1) would otherwise win and paint it muted-on-muted.
|
|
696
|
+
*/
|
|
697
|
+
.prose .callout {
|
|
698
|
+
margin: var(--space-6) 0;
|
|
699
|
+
padding: var(--space-4);
|
|
700
|
+
border-left: 3px solid var(--color-accent);
|
|
701
|
+
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
|
702
|
+
background: var(--color-surface-alt);
|
|
703
|
+
color: var(--color-text);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.callout > :first-child {
|
|
707
|
+
margin-top: 0;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.callout > :last-child {
|
|
711
|
+
margin-bottom: 0;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.callout-title {
|
|
715
|
+
display: block;
|
|
716
|
+
color: var(--color-accent);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/*
|
|
720
|
+
* Task lists. The checkboxes are `disabled` by the renderer — they are printed
|
|
721
|
+
* state, not controls — so the list loses its bullet and the box sits in the
|
|
722
|
+
* margin rather than indenting the text away from every other list.
|
|
723
|
+
*/
|
|
724
|
+
.contains-task-list {
|
|
725
|
+
padding-left: 0;
|
|
726
|
+
list-style: none;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.task-list-item {
|
|
730
|
+
padding-left: 1.6em;
|
|
731
|
+
text-indent: -1.6em;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.task-list-item > input[type='checkbox'] {
|
|
735
|
+
width: 1em;
|
|
736
|
+
height: 1em;
|
|
737
|
+
margin-right: 0.6em;
|
|
738
|
+
text-indent: 0;
|
|
739
|
+
vertical-align: -0.1em;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/* Footnotes: a smaller, separated block at the end of the article. */
|
|
743
|
+
.footnotes {
|
|
744
|
+
margin-top: var(--space-12);
|
|
745
|
+
padding-top: var(--space-6);
|
|
746
|
+
border-top: 1px solid var(--color-line);
|
|
747
|
+
color: var(--color-muted);
|
|
748
|
+
font-size: var(--text-sm);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.footnotes li:target {
|
|
752
|
+
background: var(--color-wash);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* The `↩` back-reference is a link, so it gets the link treatment and a target
|
|
756
|
+
large enough to hit — the glyph alone is well under the 24 px minimum.
|
|
757
|
+
Scoped to `.prose` for the same specificity reason as `.heading-anchor`. */
|
|
758
|
+
.prose .data-footnote-backref {
|
|
759
|
+
display: inline-block;
|
|
760
|
+
min-width: 1.5em;
|
|
761
|
+
text-align: center;
|
|
762
|
+
text-decoration: none;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/*
|
|
766
|
+
* Math and diagrams are styled by `src/styles/math.css` and
|
|
767
|
+
* `src/styles/diagram.css`, which load only on the pages that carry one — owner
|
|
768
|
+
* decision 7. `math-inline`, `math-display`, `diagram`, and the classes the
|
|
769
|
+
* diagram flattening pass writes are therefore deliberately absent here rather
|
|
770
|
+
* than missing: a rule in this sheet would ship the styling to every page and
|
|
771
|
+
* defeat the gating.
|
|
772
|
+
*
|
|
773
|
+
* Until TK-15 this file styled `math-inline`/`math-display` as monospace source,
|
|
774
|
+
* which was correct while math was a downgrade and is wrong now that it is
|
|
775
|
+
* MathML: the monospace family and the literal colour would override the math
|
|
776
|
+
* font on every expression.
|
|
777
|
+
*/
|
|
778
|
+
|
|
779
|
+
/*
|
|
780
|
+
* Deliberately unstyled, with the reason:
|
|
781
|
+
*
|
|
782
|
+
* - `token` — the bare class Prism puts on every token alongside its specific
|
|
783
|
+
* one. A colour here would apply to *every* token, and the built `<head>`
|
|
784
|
+
* orders `code.css` before this sheet, so at equal specificity this rule
|
|
785
|
+
* would win and repaint the whole stream one colour. Inheriting the body
|
|
786
|
+
* colour is both the correct unhighlighted default and the safe one.
|
|
787
|
+
* - `language-*` — the fence language is already carried visually by the block
|
|
788
|
+
* itself and semantically by `data-code-language`.
|
|
789
|
+
* - `callout-*` — the per-kind classes. See the `.callout` note above: every
|
|
790
|
+
* kind gets one neutral treatment, and the kind is carried by the title text
|
|
791
|
+
* rather than by a colour. TK-11's fixture ships three of them, so this is
|
|
792
|
+
* now a deliberate choice observed at scale rather than an untested default.
|
|
793
|
+
*
|
|
794
|
+
* This block is the single record, including for the classes `code.css` styles:
|
|
795
|
+
* `tests/built-output.test.ts` reads it and asserts each name appears, so a
|
|
796
|
+
* second copy beside those rules would be an unchecked one.
|
|
797
|
+
*/
|
|
798
|
+
|
|
799
|
+
/* ponytail: a wide table still needs a scroll container, and a wrapper element
|
|
800
|
+
is the Markdown pipeline's to emit. TK-03 owns that; styling it here without
|
|
801
|
+
the wrapper would mean `display: block`, which strips table semantics. */
|
|
802
|
+
.prose table {
|
|
803
|
+
width: 100%;
|
|
804
|
+
border-collapse: collapse;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.prose :is(th, td) {
|
|
808
|
+
padding: var(--space-2) var(--space-3);
|
|
809
|
+
/* `--color-line-strong`, not `--color-line`: a gridline is the only thing
|
|
810
|
+
separating one cell's data from the next, so it carries information and
|
|
811
|
+
needs the 3:1 non-text contrast. `--color-line` is for decorative rules
|
|
812
|
+
where the layout already communicates the boundary. */
|
|
813
|
+
border: 1px solid var(--color-line-strong);
|
|
814
|
+
text-align: left;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/* ------------------------------------------------------- note anatomy -- */
|
|
818
|
+
|
|
819
|
+
/*
|
|
820
|
+
* Requirements section 9.2's thirteen elements, in the order the page emits
|
|
821
|
+
* them. Everything here is above or below `.prose` rather than inside it: the
|
|
822
|
+
* article body is rendered Markdown and its rules are the section above.
|
|
823
|
+
*
|
|
824
|
+
* `--measure` on each region, not a shared wrapper, so a region can opt out
|
|
825
|
+
* when its content is not prose — none does yet, and the day the table of
|
|
826
|
+
* contents moves into a rail (TK-05c) it stops being a measure-width block
|
|
827
|
+
* without dragging the header and footer with it.
|
|
828
|
+
*/
|
|
829
|
+
|
|
830
|
+
.note-header,
|
|
831
|
+
.note-footer {
|
|
832
|
+
max-width: var(--measure);
|
|
833
|
+
margin-inline: auto;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.note-header {
|
|
837
|
+
margin-bottom: var(--space-8);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/*
|
|
841
|
+
* The trail. `<ol>` because the order is the meaning, and the separator is
|
|
842
|
+
* generated content rather than markup so it is decoration a reader can never
|
|
843
|
+
* select or a screen reader mistake for a crumb.
|
|
844
|
+
*
|
|
845
|
+
* `content: '/' / ''` gives the generated string empty alternative text, so a
|
|
846
|
+
* screen reader that supports the syntax announces "Home, Notes, <title>"
|
|
847
|
+
* rather than interleaving "slash". In a browser that does not, the whole
|
|
848
|
+
* declaration is invalid and the separator simply does not render — the
|
|
849
|
+
* crumbs are still spaced `<li>` elements in an ordered list, which reads
|
|
850
|
+
* correctly. That is the safe direction for an unsupported value, and it is why
|
|
851
|
+
* this is worth using here and not on the table of contents' counters, where
|
|
852
|
+
* losing the number would cost a sighted reader the depth cue.
|
|
853
|
+
*/
|
|
854
|
+
.breadcrumbs ol {
|
|
855
|
+
display: flex;
|
|
856
|
+
flex-wrap: wrap;
|
|
857
|
+
gap: var(--space-1) var(--space-2);
|
|
858
|
+
margin: 0;
|
|
859
|
+
padding: 0;
|
|
860
|
+
color: var(--color-muted);
|
|
861
|
+
font-size: var(--text-sm);
|
|
862
|
+
list-style: none;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.breadcrumbs li + li::before {
|
|
866
|
+
content: '/' / '';
|
|
867
|
+
margin-right: var(--space-2);
|
|
868
|
+
color: var(--color-line-strong);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.breadcrumbs [aria-current='page'] {
|
|
872
|
+
color: var(--color-text);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
.note-title {
|
|
876
|
+
margin: var(--space-4) 0 0;
|
|
877
|
+
font-size: var(--text-2xl);
|
|
878
|
+
letter-spacing: -0.03em;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/*
|
|
882
|
+
* Public metadata: a description list, because each row genuinely is a term and
|
|
883
|
+
* its value. The label is text, never an icon alone, so nothing here depends on
|
|
884
|
+
* recognizing a glyph — and nothing is encoded by colour.
|
|
885
|
+
*/
|
|
886
|
+
.note-meta {
|
|
887
|
+
display: flex;
|
|
888
|
+
flex-wrap: wrap;
|
|
889
|
+
gap: var(--space-2) var(--space-6);
|
|
890
|
+
margin: var(--space-4) 0 0;
|
|
891
|
+
color: var(--color-muted);
|
|
892
|
+
font-size: var(--text-sm);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.note-meta > div {
|
|
896
|
+
display: flex;
|
|
897
|
+
flex-wrap: wrap;
|
|
898
|
+
gap: var(--space-2);
|
|
899
|
+
align-items: baseline;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.note-meta dt {
|
|
903
|
+
font-weight: 600;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.note-meta dd {
|
|
907
|
+
margin: 0;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/* Public label lists inside metadata rows: semantic lists, wrapping, comma-free. */
|
|
911
|
+
.note-tags,
|
|
912
|
+
.note-aliases {
|
|
913
|
+
display: flex;
|
|
914
|
+
flex-wrap: wrap;
|
|
915
|
+
gap: var(--space-2);
|
|
916
|
+
margin: 0;
|
|
917
|
+
padding: 0;
|
|
918
|
+
list-style: none;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.note-tags a {
|
|
922
|
+
display: inline-block;
|
|
923
|
+
padding: 0 var(--space-2);
|
|
924
|
+
border: 1px solid var(--color-line-strong);
|
|
925
|
+
border-radius: var(--radius-sm);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/*
|
|
929
|
+
* The optional summary, when the artifact carries a `description` distinct from
|
|
930
|
+
* the excerpt. Larger than body text and not italic: an italic run of two lines
|
|
931
|
+
* is harder to read and says nothing the position does not already say.
|
|
932
|
+
*/
|
|
933
|
+
.note-summary {
|
|
934
|
+
margin: var(--space-6) 0 0;
|
|
935
|
+
padding-left: var(--space-4);
|
|
936
|
+
border-left: 3px solid var(--color-accent);
|
|
937
|
+
color: var(--color-muted);
|
|
938
|
+
font-size: var(--text-lg);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/*
|
|
942
|
+
* The table of contents.
|
|
943
|
+
*
|
|
944
|
+
* `<details open>` is the collapse: native, keyboard-operable, announced as a
|
|
945
|
+
* disclosure, and zero JavaScript. The summary is the only interactive chrome,
|
|
946
|
+
* and it keeps a real focus ring because `:focus-visible` is global.
|
|
947
|
+
*/
|
|
948
|
+
.toc {
|
|
949
|
+
max-width: var(--measure);
|
|
950
|
+
margin: 0 auto var(--space-8);
|
|
951
|
+
padding: var(--space-4) var(--space-6);
|
|
952
|
+
border: 1px solid var(--color-line);
|
|
953
|
+
border-radius: var(--radius-md);
|
|
954
|
+
background: var(--color-surface);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.toc-details > summary {
|
|
958
|
+
color: var(--color-text);
|
|
959
|
+
font-size: var(--text-sm);
|
|
960
|
+
font-weight: 600;
|
|
961
|
+
letter-spacing: 0.04em;
|
|
962
|
+
text-transform: uppercase;
|
|
963
|
+
cursor: pointer;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/*
|
|
967
|
+
* Nested ordered lists, numbered by CSS counters rather than by the browser's
|
|
968
|
+
* default numbering, so a nested item reads `2.1` and depth is legible from the
|
|
969
|
+
* number as well as from the indentation — which matters at 320 px, where three
|
|
970
|
+
* levels of indent would cost more width than the text can spare.
|
|
971
|
+
*
|
|
972
|
+
* `list-style: none` removes the browser's own marker, which a screen reader
|
|
973
|
+
* would otherwise announce alongside this one. The list is still an `<ol>`, so
|
|
974
|
+
* the position and the count are conveyed by the element, not by the digits:
|
|
975
|
+
* nothing here is information a reader gets only from the generated content.
|
|
976
|
+
*/
|
|
977
|
+
.toc-list {
|
|
978
|
+
margin: var(--space-3) 0 0;
|
|
979
|
+
padding-left: var(--space-4);
|
|
980
|
+
counter-reset: toc-item;
|
|
981
|
+
list-style: none;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
.toc-list .toc-list {
|
|
985
|
+
margin-top: var(--space-1);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.toc-list li {
|
|
989
|
+
margin-top: var(--space-1);
|
|
990
|
+
counter-increment: toc-item;
|
|
991
|
+
font-size: var(--text-sm);
|
|
992
|
+
text-indent: -1.4em;
|
|
993
|
+
padding-left: 1.4em;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.toc-list li::before {
|
|
997
|
+
content: counters(toc-item, '.') '. ';
|
|
998
|
+
color: var(--color-muted);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/*
|
|
1002
|
+
* `--color-link`, declared rather than inherited. Quartz's unread
|
|
1003
|
+
* table-of-contents links measure 2.04:1 in light and 2.92:1 in dark — both
|
|
1004
|
+
* WCAG AA failures in the default state, on the component it is otherwise best
|
|
1005
|
+
* at. Its failure mode is inheritance: the list is styled as secondary chrome,
|
|
1006
|
+
* so its links take the dimmest colour in the palette. Naming the token here
|
|
1007
|
+
* costs one declaration and is what `tests/design-tokens.test.ts` can actually
|
|
1008
|
+
* check; ours measure 6.72:1 and 10.37:1.
|
|
1009
|
+
*/
|
|
1010
|
+
.toc-list a {
|
|
1011
|
+
color: var(--color-link);
|
|
1012
|
+
text-decoration: none;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
.toc-list a[aria-current='location'] {
|
|
1016
|
+
color: var(--color-text);
|
|
1017
|
+
font-weight: 700;
|
|
1018
|
+
text-decoration-thickness: 0.16em;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
.toc-list a:hover,
|
|
1022
|
+
.toc-list a:focus-visible {
|
|
1023
|
+
text-decoration: underline;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/*
|
|
1027
|
+
* The footer: public provenance and the canonical route.
|
|
1028
|
+
*
|
|
1029
|
+
* `.note-canonical` is the on-screen provenance line. The layout's own
|
|
1030
|
+
* `.print-only` paragraph carries the same route for paper, and TK-08 upgrades
|
|
1031
|
+
* both to the absolute canonical URL once `site:` is configured.
|
|
1032
|
+
*/
|
|
1033
|
+
.note-footer {
|
|
1034
|
+
margin-top: var(--space-16);
|
|
1035
|
+
padding-top: var(--space-6);
|
|
1036
|
+
border-top: 1px solid var(--color-line);
|
|
1037
|
+
color: var(--color-muted);
|
|
1038
|
+
font-size: var(--text-sm);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
.note-footer p {
|
|
1042
|
+
margin: 0 0 var(--space-2);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.note-canonical {
|
|
1046
|
+
font-family: var(--font-mono);
|
|
1047
|
+
word-break: break-all;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/* ------------------------------------------------ relationship surfaces -- */
|
|
1051
|
+
|
|
1052
|
+
/*
|
|
1053
|
+
* Requirements section 9.2's items 8, 9, 10, and 12: what this note links to,
|
|
1054
|
+
* what links to it, what it is near, and where it sits in its collection.
|
|
1055
|
+
*
|
|
1056
|
+
* The three lists are one component rendered three times and share one rule
|
|
1057
|
+
* set. They are visually identical on purpose: a reader distinguishes them by
|
|
1058
|
+
* the heading, which is text, rather than by a colour or a border treatment
|
|
1059
|
+
* that would encode the difference in a way section 17 forbids.
|
|
1060
|
+
*/
|
|
1061
|
+
|
|
1062
|
+
.relations,
|
|
1063
|
+
.collection-pager {
|
|
1064
|
+
max-width: var(--measure);
|
|
1065
|
+
margin-inline: auto;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
.relations {
|
|
1069
|
+
margin-top: var(--space-12);
|
|
1070
|
+
padding-top: var(--space-6);
|
|
1071
|
+
border-top: 1px solid var(--color-line);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/*
|
|
1075
|
+
* The graph region's frame, here rather than in `graph.css`.
|
|
1076
|
+
*
|
|
1077
|
+
* `graph.css` is conditional — it loads only on a page that actually draws a
|
|
1078
|
+
* figure (owner decision 7), which is the right trade for a stylesheet of SVG
|
|
1079
|
+
* styling that most pages have no use for. But the region itself renders on
|
|
1080
|
+
* *every* note page, carrying its heading and, where there is no neighbourhood,
|
|
1081
|
+
* an empty state. Leaving the frame in the conditional sheet shipped that empty
|
|
1082
|
+
* state unstyled: full width, no rule above it, sitting under three
|
|
1083
|
+
* `.relations` sections that all have one. The published corpus is a single
|
|
1084
|
+
* note with no edges, so that was the state on every page of the live site.
|
|
1085
|
+
*
|
|
1086
|
+
* Same rules as `.relations`, because it is the same kind of region and a
|
|
1087
|
+
* reader distinguishes them by their headings.
|
|
1088
|
+
*/
|
|
1089
|
+
.graph-region {
|
|
1090
|
+
max-width: var(--measure);
|
|
1091
|
+
margin-inline: auto;
|
|
1092
|
+
margin-top: var(--space-12);
|
|
1093
|
+
padding-top: var(--space-6);
|
|
1094
|
+
border-top: 1px solid var(--color-line);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.relations-heading {
|
|
1098
|
+
display: flex;
|
|
1099
|
+
flex-wrap: wrap;
|
|
1100
|
+
gap: var(--space-2) var(--space-4);
|
|
1101
|
+
justify-content: space-between;
|
|
1102
|
+
align-items: baseline;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
.relations-heading h2 {
|
|
1106
|
+
margin: 0;
|
|
1107
|
+
font-size: var(--text-lg);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/* Text, not a badge: nothing here is carried by colour or shape alone. Styled
|
|
1111
|
+
with `.facet-count` in the facets section — the same information in the same
|
|
1112
|
+
relationship to a heading. */
|
|
1113
|
+
|
|
1114
|
+
/*
|
|
1115
|
+
* The derived list states its rule. Only the related-note section carries one:
|
|
1116
|
+
* "Links to" and "Linked from" are authored edges and need no explanation,
|
|
1117
|
+
* while a list nobody wrote does — otherwise a reader cannot tell a suggestion
|
|
1118
|
+
* from an editorial choice.
|
|
1119
|
+
*/
|
|
1120
|
+
.relations-derivation {
|
|
1121
|
+
margin: var(--space-2) 0 0;
|
|
1122
|
+
color: var(--color-muted);
|
|
1123
|
+
font-size: var(--text-sm);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/*
|
|
1127
|
+
* The large-list treatment, and the whole of it.
|
|
1128
|
+
*
|
|
1129
|
+
* `auto-fit` with a `min()` floor is one column at 320 px and as many as fit
|
|
1130
|
+
* beyond it, so a note with forty backlinks is a block a reader takes in rather
|
|
1131
|
+
* than a screen of scrolling — and it needs no truncation, no "show more"
|
|
1132
|
+
* control, and therefore no JavaScript. The `min(…, 100%)` floor is what keeps
|
|
1133
|
+
* the track from pushing the page past a narrow viewport; a fixed floor is the
|
|
1134
|
+
* usual cause of exactly that.
|
|
1135
|
+
*
|
|
1136
|
+
* It stays one `<ul>` in document order, so the element still carries the count
|
|
1137
|
+
* and the reading order: multi-column is presentation, and a screen reader
|
|
1138
|
+
* announces "list, 40 items" either way.
|
|
1139
|
+
*/
|
|
1140
|
+
.relations-list {
|
|
1141
|
+
display: grid;
|
|
1142
|
+
grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
|
|
1143
|
+
gap: var(--space-1) var(--space-6);
|
|
1144
|
+
margin: var(--space-4) 0 0;
|
|
1145
|
+
padding: 0;
|
|
1146
|
+
list-style: none;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.relations-list li {
|
|
1150
|
+
min-width: 0;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/*
|
|
1154
|
+
* The collection pager: previous and next within one collection.
|
|
1155
|
+
*
|
|
1156
|
+
* Two links in a row that wraps to two rows at 320 px, and `margin-left: auto`
|
|
1157
|
+
* on the next link so a note with only a next neighbour keeps it on the right
|
|
1158
|
+
* where a reader expects it, with no placeholder standing in for the absent
|
|
1159
|
+
* one.
|
|
1160
|
+
*/
|
|
1161
|
+
.collection-pager {
|
|
1162
|
+
display: flex;
|
|
1163
|
+
flex-wrap: wrap;
|
|
1164
|
+
gap: var(--space-4);
|
|
1165
|
+
margin-top: var(--space-12);
|
|
1166
|
+
padding-top: var(--space-6);
|
|
1167
|
+
border-top: 1px solid var(--color-line);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.collection-pager a {
|
|
1171
|
+
display: flex;
|
|
1172
|
+
flex-direction: column;
|
|
1173
|
+
gap: var(--space-1);
|
|
1174
|
+
min-width: 0;
|
|
1175
|
+
max-width: 100%;
|
|
1176
|
+
padding: var(--space-3) var(--space-4);
|
|
1177
|
+
border: 1px solid var(--color-line);
|
|
1178
|
+
border-radius: var(--radius-md);
|
|
1179
|
+
background: var(--color-surface);
|
|
1180
|
+
text-decoration: none;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.collection-pager a:hover .pager-title,
|
|
1184
|
+
.collection-pager a:focus-visible .pager-title {
|
|
1185
|
+
text-decoration: underline;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/*
|
|
1189
|
+
* `.pager-next` is the only positional class the pager needs: `margin-left:
|
|
1190
|
+
* auto` is what keeps a lone next link on the right, where a reader expects it,
|
|
1191
|
+
* with no placeholder standing in for the absent previous one. There is no
|
|
1192
|
+
* `.pager-previous` — the previous link is the flex row's first item already,
|
|
1193
|
+
* and a class carrying no declaration is a hook nobody maintains.
|
|
1194
|
+
*/
|
|
1195
|
+
.pager-next {
|
|
1196
|
+
margin-left: auto;
|
|
1197
|
+
text-align: right;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
.pager-direction {
|
|
1201
|
+
color: var(--color-muted);
|
|
1202
|
+
font-size: var(--text-xs);
|
|
1203
|
+
letter-spacing: 0.08em;
|
|
1204
|
+
text-transform: uppercase;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
.pager-title {
|
|
1208
|
+
color: var(--color-link);
|
|
1209
|
+
font-size: var(--text-sm);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/* --------------------------------------------------------- enhancements -- */
|
|
1213
|
+
|
|
1214
|
+
/*
|
|
1215
|
+
* The link preview.
|
|
1216
|
+
*
|
|
1217
|
+
* `left`/`top` are set by `link-preview.ts` from `placePreview`, which clamps to
|
|
1218
|
+
* all four viewport edges. Two of this rule's declarations are what let that
|
|
1219
|
+
* arithmetic hold rather than merely try:
|
|
1220
|
+
*
|
|
1221
|
+
* - The width is bounded by `calc(100% - 1rem)`, twice the 8 px margin the
|
|
1222
|
+
* script clamps to. A percentage on a fixed-position element resolves against
|
|
1223
|
+
* the initial containing block, which — unlike `100vw` — excludes a classic
|
|
1224
|
+
* scrollbar, so the CSS and the script measure the same box. The previous
|
|
1225
|
+
* `100vw` spelling was a scrollbar wider than the space the script clamps to.
|
|
1226
|
+
* - The excerpt is bounded to four lines, so the panel's height cannot grow with
|
|
1227
|
+
* the payload. Requirements section 8.1 asks for a *bounded* excerpt, and an
|
|
1228
|
+
* unbounded one is also the case where the vertical clamp has to give up an
|
|
1229
|
+
* edge. Nothing is lost: the full excerpt is on the note's own page and on its
|
|
1230
|
+
* card, and the preview is never the only way to reach it.
|
|
1231
|
+
*
|
|
1232
|
+
* No transition and no animation: a reduced-motion reader has nothing here to
|
|
1233
|
+
* opt out of.
|
|
1234
|
+
*/
|
|
1235
|
+
#link-preview {
|
|
1236
|
+
position: fixed;
|
|
1237
|
+
z-index: 50;
|
|
1238
|
+
width: min(22rem, calc(100% - 1rem));
|
|
1239
|
+
padding: var(--space-4);
|
|
1240
|
+
border: 1px solid var(--color-line-strong);
|
|
1241
|
+
border-radius: var(--radius-md);
|
|
1242
|
+
background: var(--color-surface);
|
|
1243
|
+
box-shadow: var(--elevation-2);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
#link-preview p {
|
|
1247
|
+
display: -webkit-box;
|
|
1248
|
+
overflow: hidden;
|
|
1249
|
+
margin: var(--space-2) 0 0;
|
|
1250
|
+
-webkit-box-orient: vertical;
|
|
1251
|
+
-webkit-line-clamp: 4;
|
|
1252
|
+
line-clamp: 4;
|
|
1253
|
+
color: var(--color-muted);
|
|
1254
|
+
font-size: var(--text-sm);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/* The `#heading` a link lands on, shown verbatim because that is what the
|
|
1258
|
+
address bar will show; it is a route fragment, so it is set in the mono face
|
|
1259
|
+
the rest of the site uses for literal strings. */
|
|
1260
|
+
.preview-fragment {
|
|
1261
|
+
display: block;
|
|
1262
|
+
margin-top: var(--space-1);
|
|
1263
|
+
overflow-wrap: anywhere;
|
|
1264
|
+
color: var(--color-muted);
|
|
1265
|
+
font-family: var(--font-mono);
|
|
1266
|
+
font-size: var(--text-xs);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/* --------------------------------------------------------------- search -- */
|
|
1270
|
+
|
|
1271
|
+
/*
|
|
1272
|
+
* TK-06. The dialog's own chrome, plus the handful of Pagefind classes worth
|
|
1273
|
+
* restyling.
|
|
1274
|
+
*
|
|
1275
|
+
* Pagefind's `pagefind-modular-ui.css` ships alongside its bundle and is loaded
|
|
1276
|
+
* with it on first open, so most of the result layout is already handled. What
|
|
1277
|
+
* is below is only what the shipped sheet gets wrong for this site: it assumes
|
|
1278
|
+
* its own colours, and it has no notion of these tokens. Everything here is
|
|
1279
|
+
* scoped inside `#search-dialog` so none of it can reach the page.
|
|
1280
|
+
*
|
|
1281
|
+
* Nothing here is a CSP relaxation. `pagefind-highlight.js` — which creates a
|
|
1282
|
+
* `<style>` element and sets `innerText`, defaulting `addStyles` to true — is
|
|
1283
|
+
* never loaded: it is a separate opt-in bundle, and this file does not opt in.
|
|
1284
|
+
* The `<mark>` rule below is ours, on Pagefind's own excerpt markup.
|
|
1285
|
+
*/
|
|
1286
|
+
|
|
1287
|
+
#search-dialog {
|
|
1288
|
+
width: min(44rem, calc(100% - var(--space-4)));
|
|
1289
|
+
padding: var(--space-4);
|
|
1290
|
+
border: 1px solid var(--color-line-strong);
|
|
1291
|
+
border-radius: var(--radius-lg);
|
|
1292
|
+
background: var(--color-surface);
|
|
1293
|
+
color: var(--color-text);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
#search-dialog::backdrop {
|
|
1297
|
+
background: rgb(0 0 0 / 0.55);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
.search-close {
|
|
1301
|
+
display: flex;
|
|
1302
|
+
justify-content: flex-end;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
.search-label {
|
|
1306
|
+
display: block;
|
|
1307
|
+
margin-bottom: var(--space-1);
|
|
1308
|
+
color: var(--color-muted);
|
|
1309
|
+
font-size: var(--text-sm);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
#search-input {
|
|
1313
|
+
width: 100%;
|
|
1314
|
+
padding: var(--space-2) var(--space-3);
|
|
1315
|
+
border: 1px solid var(--color-line-strong);
|
|
1316
|
+
border-radius: var(--radius-md);
|
|
1317
|
+
background: var(--color-bg);
|
|
1318
|
+
color: var(--color-text);
|
|
1319
|
+
font: inherit;
|
|
1320
|
+
/* 16px minimum, or iOS Safari zooms the page on focus and the reader lands
|
|
1321
|
+
on a layout they then have to pinch back out of. */
|
|
1322
|
+
font-size: max(1rem, var(--text-base));
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
#search-filters {
|
|
1326
|
+
display: flex;
|
|
1327
|
+
flex-wrap: wrap;
|
|
1328
|
+
gap: var(--space-2);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/*
|
|
1332
|
+
* The filter groups collapse to nothing when the index carries no values for
|
|
1333
|
+
* them — which is the published corpus, whose artifact has no `tags` and no
|
|
1334
|
+
* `collection`.
|
|
1335
|
+
*
|
|
1336
|
+
* The two child rules do the work: the script builds a pill group into a
|
|
1337
|
+
* container only when the index proves it has values for that filter, so an
|
|
1338
|
+
* unused container stays genuinely empty. The parent rule is what stops the
|
|
1339
|
+
* flex container contributing its own `gap` and padding around two empty
|
|
1340
|
+
* children, and it needs `:has()` rather than `:empty` — `#search-filters`
|
|
1341
|
+
* always has both child elements, so `:empty` could never match it.
|
|
1342
|
+
*/
|
|
1343
|
+
#search-filters:not(:has(*:not(:empty))) {
|
|
1344
|
+
display: none;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
#search-filter-tag:empty,
|
|
1348
|
+
#search-filter-collection:empty {
|
|
1349
|
+
display: none;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/*
|
|
1353
|
+
* The status line: one sentence, always in the same place.
|
|
1354
|
+
*
|
|
1355
|
+
* It keeps its box even when empty — `min-height` rather than collapsing —
|
|
1356
|
+
* because the `ready` state writes an empty string, and a line that vanishes
|
|
1357
|
+
* would shift every result down by its height on the first keystroke.
|
|
1358
|
+
*/
|
|
1359
|
+
#search-status {
|
|
1360
|
+
min-height: 1.5em;
|
|
1361
|
+
margin: var(--space-3) 0 0;
|
|
1362
|
+
color: var(--color-muted);
|
|
1363
|
+
font-size: var(--text-sm);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
#search-results {
|
|
1367
|
+
max-height: min(60vh, 32rem);
|
|
1368
|
+
margin: 0;
|
|
1369
|
+
padding: 0;
|
|
1370
|
+
overflow-y: auto;
|
|
1371
|
+
list-style: none;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
#search-dialog .pagefind-modular-list-result {
|
|
1375
|
+
padding: var(--space-3) 0;
|
|
1376
|
+
border-top: 1px solid var(--color-line);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
#search-dialog .pagefind-modular-list-title {
|
|
1380
|
+
margin: 0;
|
|
1381
|
+
font-size: var(--text-base);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
#search-dialog .pagefind-modular-list-link {
|
|
1385
|
+
color: var(--color-link);
|
|
1386
|
+
font-weight: 600;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
#search-dialog .pagefind-modular-list-excerpt {
|
|
1390
|
+
margin: var(--space-1) 0 0;
|
|
1391
|
+
color: var(--color-muted);
|
|
1392
|
+
font-size: var(--text-sm);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/*
|
|
1396
|
+
* The matched term. `--color-wash` behind ordinary text rather than a browser
|
|
1397
|
+
* default yellow, which fails contrast against this site's dark theme, and a
|
|
1398
|
+
* weight change beside it so the match is never signalled by colour alone.
|
|
1399
|
+
*/
|
|
1400
|
+
#search-dialog mark {
|
|
1401
|
+
background: var(--color-wash);
|
|
1402
|
+
color: var(--color-text);
|
|
1403
|
+
font-weight: 600;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/* The filter pills. Pagefind renders real `<button>` elements, so they are
|
|
1407
|
+
keyboard operable already; this is colour and shape only. `aria-pressed`
|
|
1408
|
+
carries the selected state, and the border weight shows it without colour. */
|
|
1409
|
+
#search-dialog .pagefind-modular-filter-pill {
|
|
1410
|
+
padding: var(--space-1) var(--space-3);
|
|
1411
|
+
border: 1px solid var(--color-line-strong);
|
|
1412
|
+
border-radius: var(--radius-lg);
|
|
1413
|
+
background: var(--color-surface-alt);
|
|
1414
|
+
color: var(--color-text);
|
|
1415
|
+
font: inherit;
|
|
1416
|
+
font-size: var(--text-sm);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
#search-dialog .pagefind-modular-filter-pill[aria-pressed='true'] {
|
|
1420
|
+
border-color: var(--color-accent);
|
|
1421
|
+
border-width: 2px;
|
|
1422
|
+
font-weight: 600;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/* Pagefind's own hidden markers. Its stylesheet already implements these, but
|
|
1426
|
+
this site's rules are more specific inside the dialog, so restating them is
|
|
1427
|
+
what keeps a suppressed control suppressed. */
|
|
1428
|
+
#search-dialog [data-pfmod-hidden] {
|
|
1429
|
+
display: none;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
#search-dialog [data-pfmod-suppressed] {
|
|
1433
|
+
visibility: hidden;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/* --------------------------------------------------------- reader mode -- */
|
|
1437
|
+
|
|
1438
|
+
/*
|
|
1439
|
+
* Reader mode strips the page to the reading surface: the header's route links
|
|
1440
|
+
* go, the header stops following the scroll, and card chrome flattens. The
|
|
1441
|
+
* toggles themselves stay — hiding the control that turns the mode off is how
|
|
1442
|
+
* a keyboard user gets stranded — and the footer site map stays too, so every
|
|
1443
|
+
* route this site owns is still one link away and reader mode removes chrome
|
|
1444
|
+
* rather than information.
|
|
1445
|
+
*/
|
|
1446
|
+
:root[data-reader='on'] {
|
|
1447
|
+
--measure: 65ch;
|
|
1448
|
+
--leading-body: 1.85;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
:root[data-reader='on'] .site-nav a,
|
|
1452
|
+
/* The collection rail, for the same reason as the header's route links: it is
|
|
1453
|
+
navigation into the rest of the site rather than part of the page being
|
|
1454
|
+
read. No route is stranded — the footer site map survives reader mode, and
|
|
1455
|
+
every note is one link from the home page it lists them on. */
|
|
1456
|
+
:root[data-reader='on'] .explorer,
|
|
1457
|
+
:root[data-reader='on'] .site-footer p:not(.print-only) {
|
|
1458
|
+
display: none;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/*
|
|
1462
|
+
* And the two-column track goes with it. `:has(> .explorer)` still matches a
|
|
1463
|
+
* `display: none` child, so without this reader mode would keep a 240 px column
|
|
1464
|
+
* reserved for a rail it just hid, and push the article off centre — the same
|
|
1465
|
+
* defect the `:has()` guard exists to prevent on a corpus with no rail.
|
|
1466
|
+
*/
|
|
1467
|
+
:root[data-reader='on'] .page-shell {
|
|
1468
|
+
display: block;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
:root[data-reader='on'] .site-header {
|
|
1472
|
+
position: static;
|
|
1473
|
+
border-bottom-color: transparent;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
:root[data-reader='on'] .note-card {
|
|
1477
|
+
border-color: transparent;
|
|
1478
|
+
box-shadow: none;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/* ------------------------------------------------------------ responsive -- */
|
|
1482
|
+
|
|
1483
|
+
/*
|
|
1484
|
+
* Mobile-first: the rules above are the 320 px layout — one column, a nav that
|
|
1485
|
+
* wraps rather than scrolls, and no fixed width anywhere.
|
|
1486
|
+
*
|
|
1487
|
+
* ponytail: the nav's five text links wrap to two or three rows at 320 px and
|
|
1488
|
+
* stay fully readable and operable, so no drawer or disclosure is built for
|
|
1489
|
+
* them; a `<details>` around five links is more machinery than the problem.
|
|
1490
|
+
*
|
|
1491
|
+
* The rail is TK-05c and reverses this block's earlier "no rail, and no empty
|
|
1492
|
+
* media query pretending otherwise" note, which was written when the content
|
|
1493
|
+
* that would fill one did not exist yet. It does now — `.page-shell` below is
|
|
1494
|
+
* the three layouts requirements section 16 asks for, and each is a property of
|
|
1495
|
+
* one grid rather than three separate treatments:
|
|
1496
|
+
*
|
|
1497
|
+
* - **mobile** (below 48rem): one column, the explorer below the article as a
|
|
1498
|
+
* stack of closed `<details>` — which is the "drawer" the requirement asks
|
|
1499
|
+
* for, built out of the disclosure the rail already is rather than out of a
|
|
1500
|
+
* second navigation model with its own script and focus trap;
|
|
1501
|
+
* - **tablet** (48rem to 64rem): still one column, but the article gains its
|
|
1502
|
+
* wide padding while the panels stay collapsed and collapsible;
|
|
1503
|
+
* - **desktop** (64rem and up): two columns, article centred on its own measure,
|
|
1504
|
+
* rail sticky beside it.
|
|
1505
|
+
*
|
|
1506
|
+
* 64rem rather than 48rem for the two-column switch because the rail costs
|
|
1507
|
+
* 15rem: at 48rem the article would be left with under 33rem, which is below
|
|
1508
|
+
* `--measure` and would narrow the prose to buy chrome.
|
|
1509
|
+
*/
|
|
1510
|
+
@media (min-width: 48rem) {
|
|
1511
|
+
main {
|
|
1512
|
+
padding-block: var(--space-16);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/* The header is one row once there is width for it. */
|
|
1516
|
+
html {
|
|
1517
|
+
scroll-padding-top: 5rem;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/*
|
|
1521
|
+
* Tablet: the groups become panels beside each other rather than a single
|
|
1522
|
+
* stack, which is the "collapsible side panels" of requirements section 16.
|
|
1523
|
+
*
|
|
1524
|
+
* This band gets its own treatment rather than inheriting the mobile one
|
|
1525
|
+
* because it has width the mobile layout cannot use and not enough width to
|
|
1526
|
+
* put the rail beside the article: measured, `--measure` resolves to 623 px,
|
|
1527
|
+
* and a 240 px rail at 768 px would leave the prose 528 px — narrowing the
|
|
1528
|
+
* reading column to buy chrome, which is the wrong trade in the one band
|
|
1529
|
+
* where the reader is most likely reading. Laying the panels out
|
|
1530
|
+
* horizontally spends the extra width on the navigation instead, and costs
|
|
1531
|
+
* the article nothing.
|
|
1532
|
+
*/
|
|
1533
|
+
.explorer-groups {
|
|
1534
|
+
display: grid;
|
|
1535
|
+
grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
|
|
1536
|
+
gap: 0 var(--space-6);
|
|
1537
|
+
align-items: start;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/*
|
|
1542
|
+
* Desktop: the article and its rail, side by side.
|
|
1543
|
+
*
|
|
1544
|
+
* `:has(> .explorer)` is load-bearing, not a flourish. The rail renders nothing
|
|
1545
|
+
* on a corpus with fewer than two notes and nothing in reader mode, and an
|
|
1546
|
+
* unconditional two-column grid then reserves a 240 px track for an element
|
|
1547
|
+
* that is not there — measured on the *published* build, which is exactly that
|
|
1548
|
+
* corpus: the article sat 136 px left of centre at 1440 px with an empty column
|
|
1549
|
+
* beside it. The requirement says "centered article", so the grid engages only
|
|
1550
|
+
* when there is a rail to put in it, and every other case keeps the centred
|
|
1551
|
+
* single column it had before this ticket.
|
|
1552
|
+
*
|
|
1553
|
+
* `minmax(0, …)` on the article track rather than `1fr` alone: a `1fr` track has
|
|
1554
|
+
* an automatic minimum of its content's min-content width, so one long unbroken
|
|
1555
|
+
* token inside the article would push the whole grid wider than the viewport
|
|
1556
|
+
* instead of wrapping. That is the fixed-track overflow the 320 px gate exists
|
|
1557
|
+
* to catch, arriving at 1280 px instead.
|
|
1558
|
+
*/
|
|
1559
|
+
@media (min-width: 64rem) {
|
|
1560
|
+
.page-shell:has(> .explorer) {
|
|
1561
|
+
display: grid;
|
|
1562
|
+
grid-template-columns: minmax(0, 1fr) var(--width-rail);
|
|
1563
|
+
gap: var(--gutter);
|
|
1564
|
+
width: min(calc(var(--width-page) + var(--width-rail) + var(--gutter)), 100%);
|
|
1565
|
+
margin-inline: auto;
|
|
1566
|
+
align-items: start;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/* `main` is centred inside its own track rather than by the shell, so the
|
|
1570
|
+
article keeps the measure it has at every other width and the rail does not
|
|
1571
|
+
shift the prose off centre relative to itself. */
|
|
1572
|
+
.page-shell:has(> .explorer) > main {
|
|
1573
|
+
width: 100%;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
.explorer {
|
|
1577
|
+
/* `sticky`, not a scroll listener: the rail follows the reader down a long
|
|
1578
|
+
article at zero JavaScript. The offset clears the sticky header. */
|
|
1579
|
+
position: sticky;
|
|
1580
|
+
top: 5rem;
|
|
1581
|
+
/* The page gutter goes: it is the page's own edge padding, and inside a
|
|
1582
|
+
grid track the shell's `gap` already separates the rail from the article.
|
|
1583
|
+
Keeping it would spend roughly a quarter of `--width-rail` on whitespace
|
|
1584
|
+
and wrap titles that would otherwise fit.
|
|
1585
|
+
|
|
1586
|
+
Not zero, though. `overflow-y: auto` below makes this a scroll container,
|
|
1587
|
+
which clips on *both* axes — and the global focus ring is a 2 px outline
|
|
1588
|
+
at `outline-offset: 2px`, so it reaches 4 px outside the element it is
|
|
1589
|
+
drawn on. Measured with no padding: the ring on a summary sat exactly
|
|
1590
|
+
4 px left of the rail's box and was cut flat. This is the space it needs
|
|
1591
|
+
to be seen (WCAG 2.4.11). */
|
|
1592
|
+
margin: var(--space-16) 0 0;
|
|
1593
|
+
padding: 0 0 0 4px;
|
|
1594
|
+
/* Its own scroll container, so a rail longer than the viewport scrolls
|
|
1595
|
+
within itself instead of stretching the grid row and stranding the
|
|
1596
|
+
bottom of the list where no scroll can reach it. */
|
|
1597
|
+
max-height: calc(100vh - 7rem);
|
|
1598
|
+
overflow-y: auto;
|
|
1599
|
+
/* Reserve the gutter the scrollbar takes, so appearing and disappearing
|
|
1600
|
+
does not reflow the list under the reader's pointer. */
|
|
1601
|
+
scrollbar-gutter: stable;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/* Back to one stack: the tablet band's side-by-side panels are for width the
|
|
1605
|
+
rail does not have once it is a 15rem column. Without this the `auto-fit`
|
|
1606
|
+
grid above would still apply and each group would be squeezed into a
|
|
1607
|
+
fraction of the rail. */
|
|
1608
|
+
.explorer-groups {
|
|
1609
|
+
display: block;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1614
|
+
html {
|
|
1615
|
+
scroll-behavior: auto;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
*,
|
|
1619
|
+
*::before,
|
|
1620
|
+
*::after {
|
|
1621
|
+
animation-duration: 0.01ms !important;
|
|
1622
|
+
animation-iteration-count: 1 !important;
|
|
1623
|
+
transition-duration: 0.01ms !important;
|
|
1624
|
+
scroll-behavior: auto !important;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/*
|
|
1628
|
+
* The explorer's open transition, in its own rule and **not** appended to the
|
|
1629
|
+
* list above.
|
|
1630
|
+
*
|
|
1631
|
+
* Two facts make that separation load-bearing. A type selector matches
|
|
1632
|
+
* elements and never a pseudo-element, so `*` alone does not reach
|
|
1633
|
+
* `::details-content` — measured, the transition still computed to 0.12s
|
|
1634
|
+
* under `reduce` until this rule existed. And an unrecognized pseudo-element
|
|
1635
|
+
* invalidates the *entire* selector list it appears in, not just its own
|
|
1636
|
+
* compound — also measured: `#a, #a::before, #a::unknown { … }` applies to
|
|
1637
|
+
* nothing at all. `::details-content` is new enough that plenty of browsers
|
|
1638
|
+
* do not know it, so putting it in the list above would have traded a
|
|
1639
|
+
* rail-only reduced-motion gap for a site-wide one on exactly those
|
|
1640
|
+
* browsers. Alone, it fails alone.
|
|
1641
|
+
*/
|
|
1642
|
+
*::details-content {
|
|
1643
|
+
animation-duration: 0.01ms !important;
|
|
1644
|
+
transition-duration: 0.01ms !important;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/* ---------------------------------------------------------------- print -- */
|
|
1649
|
+
|
|
1650
|
+
@media print {
|
|
1651
|
+
/* `:root:root` is (0,2,0), which ties `:root[data-theme='dark']` in
|
|
1652
|
+
tokens.css and wins on order. A plain `:root` here is (0,1,0) and loses,
|
|
1653
|
+
which would print the dark palette — light text on white paper — for
|
|
1654
|
+
anyone who has chosen the dark theme. A media query adds no specificity. */
|
|
1655
|
+
:root:root {
|
|
1656
|
+
color-scheme: light;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
body {
|
|
1660
|
+
background: #fff;
|
|
1661
|
+
color: #000;
|
|
1662
|
+
font-size: 11pt;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
.site-header,
|
|
1666
|
+
.skip-link,
|
|
1667
|
+
.breadcrumbs,
|
|
1668
|
+
.toc,
|
|
1669
|
+
/* Site-wide navigation, like the breadcrumbs above it: a list of every other
|
|
1670
|
+
page on the site is the definition of chrome on paper. */
|
|
1671
|
+
.explorer,
|
|
1672
|
+
/* Navigation into the site, like the breadcrumbs above it: on paper "Next"
|
|
1673
|
+
points at nothing. The relationship lists stay — those are information, and
|
|
1674
|
+
the rule below prints each one's route. */
|
|
1675
|
+
.collection-pager,
|
|
1676
|
+
/* The layout's `.print-only` paragraph already prints this page's route.
|
|
1677
|
+
Printing the on-screen provenance line too would put the same path on the
|
|
1678
|
+
page twice, in two different wordings. TK-08 upgrades both to the absolute
|
|
1679
|
+
canonical URL; whichever survives, it should be one of them. */
|
|
1680
|
+
.note-canonical,
|
|
1681
|
+
#search-dialog,
|
|
1682
|
+
#link-preview {
|
|
1683
|
+
display: none !important;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/* The two-column track goes with the rail it was reserved for. `:has(>
|
|
1687
|
+
.explorer)` still matches the `display: none` child above, so without this
|
|
1688
|
+
a landscape sheet keeps a 240 px column for something that is not printed
|
|
1689
|
+
— measured at 1056 px, 272 px of blank paper down the right-hand edge. The
|
|
1690
|
+
`:has()` selector is repeated rather than written plain because
|
|
1691
|
+
`.page-shell` alone loses to it on specificity. Same defect and same fix as
|
|
1692
|
+
the reader-mode reset above. */
|
|
1693
|
+
.page-shell:has(> .explorer) {
|
|
1694
|
+
display: block;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
main {
|
|
1698
|
+
width: auto;
|
|
1699
|
+
max-width: none;
|
|
1700
|
+
padding: 0;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
.prose,
|
|
1704
|
+
.note-header,
|
|
1705
|
+
.note-footer,
|
|
1706
|
+
.relations {
|
|
1707
|
+
max-width: none;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
.print-only {
|
|
1711
|
+
display: block;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
/* ponytail: prints each link's route, since the absolute canonical origin is
|
|
1715
|
+
configured by TK-08. Swap `attr(href)` for the canonical URL there. */
|
|
1716
|
+
.prose a[href]::after,
|
|
1717
|
+
.note-header a[href]::after,
|
|
1718
|
+
.relations a[href]::after {
|
|
1719
|
+
content: ' (' attr(href) ')';
|
|
1720
|
+
font-size: 0.85em;
|
|
1721
|
+
word-break: break-all;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
.prose a[href^='#']::after {
|
|
1725
|
+
content: '';
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
.prose pre {
|
|
1729
|
+
white-space: pre-wrap;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
:is(h1, h2, h3) {
|
|
1733
|
+
break-after: avoid;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
.prose :is(pre, blockquote, table) {
|
|
1737
|
+
break-inside: avoid;
|
|
1738
|
+
}
|
|
1739
|
+
}
|