@runbooks/design 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.
- package/README.md +14 -0
- package/dist/color.d.ts +17 -0
- package/dist/color.js +24 -0
- package/dist/geometry.test.d.ts +1 -0
- package/dist/geometry.test.js +155 -0
- package/dist/icons.d.ts +69 -0
- package/dist/icons.js +103 -0
- package/dist/icons.test.d.ts +1 -0
- package/dist/icons.test.js +140 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/mark.d.ts +42 -0
- package/dist/mark.js +102 -0
- package/dist/primitives.d.ts +75 -0
- package/dist/primitives.js +126 -0
- package/dist/primitives.test.d.ts +1 -0
- package/dist/primitives.test.js +134 -0
- package/dist/render.d.ts +97 -0
- package/dist/render.js +1085 -0
- package/dist/render.test.d.ts +1 -0
- package/dist/render.test.js +179 -0
- package/dist/specimen.d.ts +2 -0
- package/dist/specimen.gen.d.ts +1 -0
- package/dist/specimen.gen.js +9 -0
- package/dist/specimen.js +81 -0
- package/dist/stylesheet.d.ts +95 -0
- package/dist/stylesheet.js +987 -0
- package/dist/stylesheet.test.d.ts +1 -0
- package/dist/stylesheet.test.js +265 -0
- package/dist/text.d.ts +28 -0
- package/dist/text.js +89 -0
- package/dist/tokens.d.ts +104 -0
- package/dist/tokens.js +142 -0
- package/dist/tokens.test.d.ts +1 -0
- package/dist/tokens.test.js +125 -0
- package/fonts/IBMPlexMono-Regular-Latin1.woff2 +0 -0
- package/fonts/IBMPlexMono-SemiBold-Latin1.woff2 +0 -0
- package/fonts/IBMPlexSans-Italic-Latin1.woff2 +0 -0
- package/fonts/IBMPlexSans-Medium-Latin1.woff2 +0 -0
- package/fonts/IBMPlexSans-Regular-Latin1.woff2 +0 -0
- package/fonts/IBMPlexSans-SemiBold-Latin1.woff2 +0 -0
- package/fonts/LICENSE.txt +93 -0
- package/package.json +40 -0
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tokens, as the stylesheet the pages actually get.
|
|
3
|
+
*
|
|
4
|
+
* D-01 produced `PALETTES`, `TYPE_SCALE`, `FONT_STACKS` and `NUMERIC_FEATURES`, and its
|
|
5
|
+
* contrast tests measured them against each other. Nothing consumed them. The site
|
|
6
|
+
* shipped with no stylesheet at all — default serif, no measure, no tabular figures — so
|
|
7
|
+
* every token in this package was a value the product had agreed on and never applied,
|
|
8
|
+
* and the tests passed because they were checking the agreement rather than the page.
|
|
9
|
+
*
|
|
10
|
+
* Emitted rather than written beside the tokens, for the reason the rest of this
|
|
11
|
+
* repository keeps arriving at: a hand-kept CSS file with `#141A1F` in it is a second
|
|
12
|
+
* copy of a decision, and the copy is the one that goes stale. Every colour, size and
|
|
13
|
+
* stack below is read from the token objects; a test asserts the output contains no
|
|
14
|
+
* literal that did not come from one.
|
|
15
|
+
*
|
|
16
|
+
* §18.2's prohibitions are kept the way that file keeps them — by having nothing to
|
|
17
|
+
* reach for. There is no shadow token, no gradient except the risk legend's, and no
|
|
18
|
+
* uppercase transform, so none of the three can be spelled here.
|
|
19
|
+
*/
|
|
20
|
+
import { PALETTES, RISK_COLORS, TYPE_SCALE, FONT_STACKS, NUMERIC_FEATURES, NODE_RADIUS } from "./tokens.js";
|
|
21
|
+
import { RISK_ORDER } from "@runbooks/schema";
|
|
22
|
+
/** Where the self-hosted faces are served from. */
|
|
23
|
+
export const FONT_DIR = "/fonts";
|
|
24
|
+
/**
|
|
25
|
+
* Where they are kept, relative to this package.
|
|
26
|
+
*
|
|
27
|
+
* Source, not build output: `apps/web/public` is emitted in its entirety and gitignored
|
|
28
|
+
* as such, so a font left there would vanish on a fresh clone and the site would render
|
|
29
|
+
* in the fallback stack with nothing failing. The package that declares the faces owns
|
|
30
|
+
* the files, and the catalog build copies them out with the rest of what it serves.
|
|
31
|
+
*/
|
|
32
|
+
export const FONT_SOURCE_DIR = "fonts";
|
|
33
|
+
/**
|
|
34
|
+
* The faces, and only the ones the type scale asks for.
|
|
35
|
+
*
|
|
36
|
+
* 400 and 500 are the scale's two weights; 600 is what `strong` needs and italic is what
|
|
37
|
+
* `em` needs, and synthesising either from a regular face is how a careful type choice
|
|
38
|
+
* ends up looking careless. Six files, Latin1 subsets, ~124 kB in total.
|
|
39
|
+
*/
|
|
40
|
+
export const FACES = [
|
|
41
|
+
{ family: "IBM Plex Sans", file: "IBMPlexSans-Regular-Latin1.woff2", weight: 400, style: "normal" },
|
|
42
|
+
{ family: "IBM Plex Sans", file: "IBMPlexSans-Italic-Latin1.woff2", weight: 400, style: "italic" },
|
|
43
|
+
{ family: "IBM Plex Sans", file: "IBMPlexSans-Medium-Latin1.woff2", weight: 500, style: "normal" },
|
|
44
|
+
{ family: "IBM Plex Sans", file: "IBMPlexSans-SemiBold-Latin1.woff2", weight: 600, style: "normal" },
|
|
45
|
+
{ family: "IBM Plex Mono", file: "IBMPlexMono-Regular-Latin1.woff2", weight: 400, style: "normal" },
|
|
46
|
+
{ family: "IBM Plex Mono", file: "IBMPlexMono-SemiBold-Latin1.woff2", weight: 600, style: "normal" },
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* Upstream's own range for the Latin1 split, copied rather than narrowed.
|
|
50
|
+
*
|
|
51
|
+
* It carries U+2013-2014 and U+2018-201E and U+2026 — the dashes, the curly quotes and
|
|
52
|
+
* the ellipsis this catalog's prose is full of. A range invented here would be a
|
|
53
|
+
* subsetting decision made twice, and the half that is wrong shows up as one character in
|
|
54
|
+
* a different typeface in the middle of a sentence.
|
|
55
|
+
*/
|
|
56
|
+
export const LATIN1_RANGE = "U+0000, U+000D, U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, " +
|
|
57
|
+
"U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, " +
|
|
58
|
+
"U+20AC, U+2122, U+2212, U+FB01-FB02";
|
|
59
|
+
function fontFaces() {
|
|
60
|
+
return FACES.map((face) => `@font-face{
|
|
61
|
+
font-family:"${face.family}";
|
|
62
|
+
font-style:${face.style};
|
|
63
|
+
font-weight:${face.weight};
|
|
64
|
+
font-display:swap;
|
|
65
|
+
src:url("${FONT_DIR}/${face.file}") format("woff2");
|
|
66
|
+
unicode-range:${LATIN1_RANGE};
|
|
67
|
+
}`).join("\n");
|
|
68
|
+
}
|
|
69
|
+
/** One theme's palette as custom properties. */
|
|
70
|
+
function palette(theme) {
|
|
71
|
+
const values = PALETTES[theme];
|
|
72
|
+
return [
|
|
73
|
+
`--rb-background:${values.background}`,
|
|
74
|
+
`--rb-surface:${values.surface}`,
|
|
75
|
+
`--rb-border:${values.border}`,
|
|
76
|
+
`--rb-text:${values.text}`,
|
|
77
|
+
`--rb-text-secondary:${values.textSecondary}`,
|
|
78
|
+
`--rb-trust:${values.trust}`,
|
|
79
|
+
/**
|
|
80
|
+
* The risk scale as properties too, so a graph follows the theme.
|
|
81
|
+
*
|
|
82
|
+
* The SVG is server-rendered — §18.4 puts it above the fold and a picture that waits
|
|
83
|
+
* for script is invisible during the incident it is opened in — so it is drawn once,
|
|
84
|
+
* before anyone has chosen anything. Emitting `var(--rb-risk-…)` with the drawn
|
|
85
|
+
* theme's value as the fallback is what lets one drawing serve both: in a page it
|
|
86
|
+
* takes the theme in force, and exported on its own it keeps the colours it was
|
|
87
|
+
* drawn with.
|
|
88
|
+
*/
|
|
89
|
+
...RISK_ORDER.map((risk) => `--rb-risk-${risk}:${RISK_COLORS[theme][risk]}`),
|
|
90
|
+
].join(";");
|
|
91
|
+
}
|
|
92
|
+
/** The type scale as custom properties, in the units the scale is stated in. */
|
|
93
|
+
function typeScale() {
|
|
94
|
+
return Object.entries(TYPE_SCALE)
|
|
95
|
+
.flatMap(([name, step]) => {
|
|
96
|
+
const kebab = name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
97
|
+
const parts = [
|
|
98
|
+
`--rb-${kebab}-size:${step.size}px`,
|
|
99
|
+
`--rb-${kebab}-leading:${step.leading}px`,
|
|
100
|
+
`--rb-${kebab}-weight:${step.weight}`,
|
|
101
|
+
];
|
|
102
|
+
if ("maxLineLength" in step)
|
|
103
|
+
parts.push(`--rb-measure:${step.maxLineLength}ch`);
|
|
104
|
+
return parts;
|
|
105
|
+
})
|
|
106
|
+
.join(";");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The whole stylesheet.
|
|
110
|
+
*
|
|
111
|
+
* Deliberately one string rather than a file Next imports. A generated file on disk is a
|
|
112
|
+
* file somebody edits, and then the tokens and the page disagree again with nothing to
|
|
113
|
+
* catch it. Inlined in the document head it also costs no second request and cannot
|
|
114
|
+
* flash unstyled, which on a static catalog of this size is the better trade anyway.
|
|
115
|
+
*/
|
|
116
|
+
export function stylesheet(options) {
|
|
117
|
+
return `${fontFaces()}
|
|
118
|
+
|
|
119
|
+
:root{
|
|
120
|
+
${palette(options.defaultTheme)};
|
|
121
|
+
${typeScale()};
|
|
122
|
+
--rb-sans:${FONT_STACKS.sans};
|
|
123
|
+
--rb-mono:${FONT_STACKS.mono};
|
|
124
|
+
--rb-radius:${NODE_RADIUS}px;
|
|
125
|
+
--rb-step:8px;
|
|
126
|
+
/* The page, which is not the measure: prose is capped, a diagram is not. */
|
|
127
|
+
color-scheme:${options.defaultTheme === "dark" ? "dark light" : "light dark"};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The other theme is reached by choosing it, not by an operating-system setting.
|
|
132
|
+
*
|
|
133
|
+
* Following \`prefers-color-scheme\` made the catalog's default whatever the desktop
|
|
134
|
+
* happened to be set to, which is not a default at all — the site had one look on this
|
|
135
|
+
* machine and another on the next, and neither was the one it was designed in.
|
|
136
|
+
*
|
|
137
|
+
* Q10 refused to *lock* a theme, and that still holds: the toggle in the header sets
|
|
138
|
+
* \`data-theme\` and it wins, in either direction, and both palettes are measured against
|
|
139
|
+
* the same contrast floors. What changed is only which one you get before you choose.
|
|
140
|
+
*/
|
|
141
|
+
:root[data-theme="light"]{ ${palette("light")}; color-scheme:light; }
|
|
142
|
+
:root[data-theme="dark"]{ ${palette("dark")}; color-scheme:dark; }
|
|
143
|
+
|
|
144
|
+
*,*::before,*::after{box-sizing:border-box;}
|
|
145
|
+
|
|
146
|
+
body{
|
|
147
|
+
margin:0;
|
|
148
|
+
background:var(--rb-background);
|
|
149
|
+
color:var(--rb-text);
|
|
150
|
+
font-family:var(--rb-sans);
|
|
151
|
+
font-size:var(--rb-body-size);
|
|
152
|
+
line-height:var(--rb-body-leading);
|
|
153
|
+
font-weight:var(--rb-body-weight);
|
|
154
|
+
/* Numbers appear in sequence in every table, every trust level and every step id. */
|
|
155
|
+
font-variant-numeric:${NUMERIC_FEATURES};
|
|
156
|
+
-webkit-text-size-adjust:100%;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The page is as wide as the screen, and the padding is the only thing that scales.
|
|
161
|
+
*
|
|
162
|
+
* Two different limits, and conflating them cost the site most of its display twice over.
|
|
163
|
+
* First a single column at the measure made the graph, the table and the step list all as
|
|
164
|
+
* narrow as a paragraph. Then a 1180px shell fixed that for a laptop and spent two thirds
|
|
165
|
+
* of a 4K display on margins.
|
|
166
|
+
*
|
|
167
|
+
* So width belongs to the page, and each kind of content is held by the rule that suits
|
|
168
|
+
* it: prose by --rb-measure, cards by the width one card needs before it stops being
|
|
169
|
+
* readable, the graph by its column. The header is inside this rule so the navigation
|
|
170
|
+
* runs edge to edge — its underline is a line across the screen rather than a line across
|
|
171
|
+
* a column floating in the middle of one.
|
|
172
|
+
*
|
|
173
|
+
* A clamp rather than breakpoints: 16px of padding on a phone, growing with the viewport,
|
|
174
|
+
* and stopping at 80px so a very large screen does not push the content into a frame.
|
|
175
|
+
*/
|
|
176
|
+
header,main{
|
|
177
|
+
max-width:none;
|
|
178
|
+
margin:0;
|
|
179
|
+
padding:0 clamp(calc(var(--rb-step) * 2), 3vw, calc(var(--rb-step) * 10));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
header{
|
|
183
|
+
display:flex;
|
|
184
|
+
flex-wrap:wrap;
|
|
185
|
+
gap:var(--rb-step) calc(var(--rb-step) * 2);
|
|
186
|
+
align-items:baseline;
|
|
187
|
+
justify-content:space-between;
|
|
188
|
+
padding-top:calc(var(--rb-step) * 3);
|
|
189
|
+
padding-bottom:calc(var(--rb-step) * 2);
|
|
190
|
+
border-bottom:1px solid var(--rb-border);
|
|
191
|
+
margin-bottom:calc(var(--rb-step) * 4);
|
|
192
|
+
}
|
|
193
|
+
/* The nav ran together as one word without this: "runbooks.directoryCatalogTrust…". */
|
|
194
|
+
header nav{ display:flex; flex-wrap:wrap; gap:calc(var(--rb-step) * 2); }
|
|
195
|
+
header nav[aria-label="For machines"]{
|
|
196
|
+
gap:var(--rb-step);
|
|
197
|
+
font-family:var(--rb-mono);
|
|
198
|
+
font-size:var(--rb-meta-size);
|
|
199
|
+
}
|
|
200
|
+
header nav[aria-label="For machines"] a{ color:var(--rb-text-secondary); }
|
|
201
|
+
header [data-slot="theme"]{
|
|
202
|
+
padding:calc(var(--rb-step) * 0.3) calc(var(--rb-step) * 0.9);
|
|
203
|
+
font-size:var(--rb-meta-size);
|
|
204
|
+
font-family:var(--rb-sans);
|
|
205
|
+
color:var(--rb-text-secondary);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
main{ padding-bottom:calc(var(--rb-step) * 8); }
|
|
209
|
+
|
|
210
|
+
/*
|
|
211
|
+
* A long title has to break somewhere rather than push the page sideways — but only where
|
|
212
|
+
* it must.
|
|
213
|
+
*
|
|
214
|
+
* This was anywhere on every piece of prose, and that value does more than permit a
|
|
215
|
+
* break: it makes the narrowest letter the element's minimum width, so a table column
|
|
216
|
+
* collapses to nothing and takes the ordinary words with it. The status page printed
|
|
217
|
+
* "not meas / ured" in a column that had a whole screen beside it.
|
|
218
|
+
*/
|
|
219
|
+
h1,h2,h3,p,li,dt,dd,td,th{ overflow-wrap:break-word; }
|
|
220
|
+
|
|
221
|
+
/*
|
|
222
|
+
* A ref, a capability URN and a content hash carry no break of their own and are each
|
|
223
|
+
* longer than a phone is wide, so these may break anywhere. They are also the only things
|
|
224
|
+
* on the page that need to.
|
|
225
|
+
*/
|
|
226
|
+
code,kbd,samp{ overflow-wrap:anywhere; }
|
|
227
|
+
|
|
228
|
+
h1{
|
|
229
|
+
font-size:var(--rb-page-title-size);
|
|
230
|
+
line-height:var(--rb-page-title-leading);
|
|
231
|
+
font-weight:var(--rb-page-title-weight);
|
|
232
|
+
margin:0 0 calc(var(--rb-step) * 2);
|
|
233
|
+
letter-spacing:-0.01em;
|
|
234
|
+
}
|
|
235
|
+
h2{
|
|
236
|
+
font-size:var(--rb-section-title-size);
|
|
237
|
+
line-height:var(--rb-section-title-leading);
|
|
238
|
+
font-weight:var(--rb-section-title-weight);
|
|
239
|
+
margin:calc(var(--rb-step) * 5) 0 calc(var(--rb-step) * 1.5);
|
|
240
|
+
}
|
|
241
|
+
h3{
|
|
242
|
+
font-size:var(--rb-body-size);
|
|
243
|
+
line-height:var(--rb-body-leading);
|
|
244
|
+
font-weight:600;
|
|
245
|
+
margin:calc(var(--rb-step) * 3) 0 var(--rb-step);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
p,li{ max-width:var(--rb-measure); }
|
|
249
|
+
p{ margin:0 0 calc(var(--rb-step) * 1.5); }
|
|
250
|
+
ul,ol{ margin:0 0 calc(var(--rb-step) * 2); padding-left:calc(var(--rb-step) * 3); }
|
|
251
|
+
li{ margin-bottom:calc(var(--rb-step) * 0.5); }
|
|
252
|
+
|
|
253
|
+
a{ color:var(--rb-trust); text-underline-offset:2px; }
|
|
254
|
+
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,summary:focus-visible{
|
|
255
|
+
outline:2px solid var(--rb-trust);
|
|
256
|
+
outline-offset:2px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
strong{ font-weight:600; }
|
|
260
|
+
|
|
261
|
+
code,kbd,pre,samp{
|
|
262
|
+
font-family:var(--rb-mono);
|
|
263
|
+
font-size:var(--rb-code-size);
|
|
264
|
+
line-height:var(--rb-code-leading);
|
|
265
|
+
font-variant-numeric:${NUMERIC_FEATURES};
|
|
266
|
+
}
|
|
267
|
+
code{
|
|
268
|
+
background:var(--rb-surface);
|
|
269
|
+
border:1px solid var(--rb-border);
|
|
270
|
+
border-radius:var(--rb-radius);
|
|
271
|
+
padding:0.1em 0.35em;
|
|
272
|
+
}
|
|
273
|
+
pre{
|
|
274
|
+
background:var(--rb-surface);
|
|
275
|
+
border:1px solid var(--rb-border);
|
|
276
|
+
border-radius:var(--rb-radius);
|
|
277
|
+
padding:calc(var(--rb-step) * 2);
|
|
278
|
+
overflow-x:auto;
|
|
279
|
+
}
|
|
280
|
+
pre code{ background:none; border:0; padding:0; }
|
|
281
|
+
|
|
282
|
+
/* Meta text: provenance, trust lines, counts. Secondary, never smaller than the scale. */
|
|
283
|
+
dl,[data-slot="fidelity"],[data-slot="pointer"],[data-slot="graph-note"],[data-section="trust"] p{
|
|
284
|
+
font-size:var(--rb-meta-size);
|
|
285
|
+
line-height:var(--rb-meta-leading);
|
|
286
|
+
color:var(--rb-text-secondary);
|
|
287
|
+
}
|
|
288
|
+
/*
|
|
289
|
+
* The value column may shrink below what is in it.
|
|
290
|
+
*
|
|
291
|
+
* A bare 1fr track never goes under its content's min-content width, and provenance holds
|
|
292
|
+
* source URLs, which have no break in them. At 390px the track sized itself to the longest
|
|
293
|
+
* URL - 471px inside a 263px list - and the record page scrolled sideways to 604px.
|
|
294
|
+
* Every other grid in this stylesheet was already written minmax(0,...); this was the one
|
|
295
|
+
* that was not, and it is the only page-level overflow the phone pass found.
|
|
296
|
+
*/
|
|
297
|
+
dl{ display:grid; grid-template-columns:auto minmax(0,1fr); gap:var(--rb-step) calc(var(--rb-step) * 2); }
|
|
298
|
+
dt{ color:var(--rb-text); font-weight:500; }
|
|
299
|
+
dd{ margin:0; }
|
|
300
|
+
|
|
301
|
+
/* A wide table scrolls inside its own box; it never widens the page. */
|
|
302
|
+
table{ border-collapse:collapse; font-variant-numeric:${NUMERIC_FEATURES}; width:100%; }
|
|
303
|
+
main :where(table){ display:block; overflow-x:auto; }
|
|
304
|
+
th,td{ border-bottom:1px solid var(--rb-border); padding:var(--rb-step) calc(var(--rb-step) * 2) var(--rb-step) 0; text-align:left; vertical-align:top; }
|
|
305
|
+
th{ font-weight:500; }
|
|
306
|
+
/*
|
|
307
|
+
* Prose is prose in a cell too.
|
|
308
|
+
*
|
|
309
|
+
* The measure above is set on p and li, which is where prose lives on every page but one.
|
|
310
|
+
* The status table holds its sentences in the cell directly, so its definition column ran
|
|
311
|
+
* at about 120 characters against the 75 the design holds everywhere else - the widest
|
|
312
|
+
* line on the site, on the page whose subject is honesty about itself. The table gets
|
|
313
|
+
* taller and every line stays readable, which is the trade the measure exists to make.
|
|
314
|
+
*/
|
|
315
|
+
td{ max-width:var(--rb-measure); }
|
|
316
|
+
|
|
317
|
+
/* Anything the build hands over as markup: the graph, a card, a specimen. */
|
|
318
|
+
svg{ max-width:100%; height:auto; }
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The step being read, marked in both places at once.
|
|
322
|
+
*
|
|
323
|
+
* Set by a script and by nothing else: absent, every node keeps its own weight and the
|
|
324
|
+
* page is what it was. The graph already draws a hovered node heavier, so this is the same
|
|
325
|
+
* emphasis reaching the same element from the prose — weight, never hue, because hue is
|
|
326
|
+
* the risk channel and nothing else may borrow it (§18.2).
|
|
327
|
+
*/
|
|
328
|
+
[data-slot="graph-column"][data-reading] [data-node]>path:first-of-type{ stroke-opacity:.45; }
|
|
329
|
+
[data-slot="graph-column"][data-reading] [data-node][data-reading="true"]>path:first-of-type{
|
|
330
|
+
stroke-opacity:1;
|
|
331
|
+
stroke-width:2.6;
|
|
332
|
+
}
|
|
333
|
+
/* A rule, not a shadow: §18.2 has no shadows to reach for, and this is the trust accent
|
|
334
|
+
doing what it does everywhere else — marking the thing the reader is on. */
|
|
335
|
+
/* Drawn as a 2px rule plus 14px of padding, pulled back by the 16px that adds up to, so
|
|
336
|
+
marking a step moves nothing on the page. */
|
|
337
|
+
[data-step]{ border-left:2px solid transparent; padding-left:calc(var(--rb-step) * 2 - 2px); margin-left:calc(var(--rb-step) * -2); }
|
|
338
|
+
[data-step][data-reading="true"]{ border-left-color:var(--rb-trust); }
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* A tool page: the thing you are working on, and everything about it beside it.
|
|
342
|
+
*
|
|
343
|
+
* The submit page put a document editor down the left and its gate list, its promise and
|
|
344
|
+
* its instructions underneath, one after another, with two thirds of the screen empty
|
|
345
|
+
* next to them — so a reader fixing a failing gate scrolled away from the document to read
|
|
346
|
+
* which gate had failed. Drawn in the canvas as two columns for that reason.
|
|
347
|
+
*
|
|
348
|
+
* 420px is the canvas's number. The rule shipped 32rem — 512px — from the commit that made
|
|
349
|
+
* these two columns, half an hour after the artboard landed and without a word about the
|
|
350
|
+
* width, and the parity check for this page compared that there were two columns without
|
|
351
|
+
* comparing how wide. The record page's check does compare the number, which is why its
|
|
352
|
+
* 600px never drifted. Owner's call, 2026-09-05: the drawing is right.
|
|
353
|
+
*/
|
|
354
|
+
[data-layout="tool"]{
|
|
355
|
+
display:grid;
|
|
356
|
+
grid-template-columns:minmax(0,1fr) minmax(0,420px);
|
|
357
|
+
column-gap:calc(var(--rb-step) * 5);
|
|
358
|
+
align-items:start;
|
|
359
|
+
}
|
|
360
|
+
/*
|
|
361
|
+
* Placement through :where(), so the narrow-screen rule below can take it back.
|
|
362
|
+
*
|
|
363
|
+
* It could not before: a selector naming the slot outranks one naming every child, so
|
|
364
|
+
* grid-column:2 survived the media query, column 2 became an implicit auto track, and the
|
|
365
|
+
* 1fr had nothing left to distribute. Measured in a 390px viewport, the work column - the
|
|
366
|
+
* matcher on /stack, the paste field on /import, the gate runner on /submit - was 0 pixels
|
|
367
|
+
* wide. Not narrow: gone.
|
|
368
|
+
*
|
|
369
|
+
* :where() contributes no specificity, so these place the panels and the media query wins
|
|
370
|
+
* on source order, which is what a media query is for.
|
|
371
|
+
*/
|
|
372
|
+
[data-layout="tool"]>:where(h1){ grid-column:1 / -1; grid-row:1; }
|
|
373
|
+
[data-layout="tool"]>:where(p){ grid-column:1 / -1; grid-row:2; }
|
|
374
|
+
/*
|
|
375
|
+
* The wide column is named by where it sits, not by what one page happens to put in it.
|
|
376
|
+
*
|
|
377
|
+
* It was [data-section="run"], which is the gate runner on /submit and nothing at all on
|
|
378
|
+
* the other three routes this layout covers. /stack and /import shipped one column for
|
|
379
|
+
* that reason: the drawing had always shown two and there was no name for the left one.
|
|
380
|
+
*/
|
|
381
|
+
[data-layout="tool"]>:where([data-slot="work"]){ grid-column:1; grid-row:3; }
|
|
382
|
+
[data-layout="tool"]>:where([data-slot="beside"]){ grid-column:2; grid-row:3; min-width:0; }
|
|
383
|
+
|
|
384
|
+
@media (max-width:1100px){
|
|
385
|
+
[data-layout="tool"]{ grid-template-columns:minmax(0,1fr); }
|
|
386
|
+
[data-layout="tool"]>*{ grid-column:1; grid-row:auto; position:static; }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The constructor is three panels, and the page already says so.
|
|
391
|
+
*
|
|
392
|
+
* Library, canvas, inspector: the markup names them, and stacking them put a step palette
|
|
393
|
+
* above an editor above an inspector, each as wide as a paragraph, on a screen with room
|
|
394
|
+
* for all three. The page itself warns that editing needs a wider window — this is what a
|
|
395
|
+
* wider window is for.
|
|
396
|
+
*/
|
|
397
|
+
[data-page="constructor"]{
|
|
398
|
+
display:grid;
|
|
399
|
+
grid-template-columns:minmax(0,16rem) minmax(0,1fr) minmax(0,22rem);
|
|
400
|
+
column-gap:calc(var(--rb-step) * 4);
|
|
401
|
+
align-items:start;
|
|
402
|
+
}
|
|
403
|
+
/* Through :where() for the reason the tool layout gives above. Without it the inspector
|
|
404
|
+
stayed in column 3 on a phone, three tracks were computed at 390px - 0px, 114px, 219px -
|
|
405
|
+
and the heading, the notice and the step library were all 0 pixels wide. */
|
|
406
|
+
[data-page="constructor"]>:where(h1, [data-section="mobile-notice"], [data-section="draft-notice"]){ grid-column:1 / -1; }
|
|
407
|
+
[data-page="constructor"]>:where([data-panel="library"]){ grid-column:1; }
|
|
408
|
+
[data-page="constructor"]>:where([data-panel="canvas"]){ grid-column:2; }
|
|
409
|
+
[data-page="constructor"]>:where([data-panel="inspector"]){ grid-column:3; }
|
|
410
|
+
|
|
411
|
+
/* Below this the three do not fit, and the page is honest about being read-only there. */
|
|
412
|
+
@media (max-width:1100px){
|
|
413
|
+
[data-page="constructor"]{ grid-template-columns:minmax(0,1fr); }
|
|
414
|
+
[data-page="constructor"]>*{ grid-column:1; position:static; }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* A record page is two things read together, so it is two columns.
|
|
419
|
+
*
|
|
420
|
+
* The graph and the prose about it were stacked, which meant that reading step five put
|
|
421
|
+
* the picture of the procedure off the top of the screen — exactly when a reader wants to
|
|
422
|
+
* see where step five sits. The graph column is sticky, and that is the whole reason for
|
|
423
|
+
* the layout: it stays while the steps scroll past it.
|
|
424
|
+
*
|
|
425
|
+
* Width goes to the graph, not the prose. Prose is capped by its measure whatever the
|
|
426
|
+
* column allows, so surplus width does nothing for it and everything for a diagram.
|
|
427
|
+
*
|
|
428
|
+
* The heading spans both columns and comes first in the markup, so a reader with no CSS
|
|
429
|
+
* meets the record's name, then its picture, then its prose — the order the page had
|
|
430
|
+
* before this rule existed. The grid is what arranges them; without it they stack.
|
|
431
|
+
*/
|
|
432
|
+
[data-layout="record"]{
|
|
433
|
+
display:grid;
|
|
434
|
+
grid-template-columns:minmax(0,1fr) minmax(0,600px);
|
|
435
|
+
align-items:start;
|
|
436
|
+
}
|
|
437
|
+
[data-layout="record"]>:where([data-slot="head"]){ grid-column:1 / -1; }
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* The graph column is a panel the height of the screen, and the rule between the columns
|
|
441
|
+
* is what separates them — not a gap. Drawn this way in the canvas, and the height is what
|
|
442
|
+
* lets the picture be fitted rather than panned: a graph that fills the column needs no
|
|
443
|
+
* scrollbar of its own, and the page keeps the only one on the screen.
|
|
444
|
+
*/
|
|
445
|
+
[data-layout="record"]>[data-slot="graph-column"]{
|
|
446
|
+
min-width:0;
|
|
447
|
+
display:flex;
|
|
448
|
+
flex-direction:column;
|
|
449
|
+
border-right:1px solid var(--rb-border);
|
|
450
|
+
}
|
|
451
|
+
/*
|
|
452
|
+
* The prose is what stays put.
|
|
453
|
+
*
|
|
454
|
+
* The graph is the long thing now that it takes the width it is given, so sticking it
|
|
455
|
+
* would pin the tall half and scroll the short one. The steps are the reference a reader
|
|
456
|
+
* keeps glancing at, so they are the half that stays while the picture goes past. Nothing
|
|
457
|
+
* here scrolls on its own: the page has the only scrollbar, and a column taller than the
|
|
458
|
+
* screen simply travels with it.
|
|
459
|
+
*/
|
|
460
|
+
[data-layout="record"]>[data-slot="prose-column"]{
|
|
461
|
+
min-width:0;
|
|
462
|
+
padding:calc(var(--rb-step) * 5) calc(var(--rb-step) * 5) calc(var(--rb-step) * 12);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* The right column stays put, on every page that has one.
|
|
467
|
+
*
|
|
468
|
+
* The record page had this rule and no other page did, so the same two-column reading —
|
|
469
|
+
* picture on the left, what is said about it on the right — behaved one way on a record
|
|
470
|
+
* and another way on the dry pass, the home page and the submit page. The reason given
|
|
471
|
+
* for the record is not about records: the left half is the long one, so sticking it
|
|
472
|
+
* would pin the tall column and scroll the short one.
|
|
473
|
+
*
|
|
474
|
+
* One declaration rather than five, because it is one rule. Each layout below only says
|
|
475
|
+
* which of its children is the right column; what happens to it is decided here.
|
|
476
|
+
*
|
|
477
|
+
* Every one of these selectors names a SINGLE element. A sticky column has to be one
|
|
478
|
+
* element: a grid whose right-hand children are flat, each sticky on its own, pins them
|
|
479
|
+
* all to the same top edge and stacks them on top of each other. That is why the markup
|
|
480
|
+
* of the pages that had flat children now wraps them.
|
|
481
|
+
*
|
|
482
|
+
* Nothing scrolls on its own: the page keeps the only scrollbar, and a column taller than
|
|
483
|
+
* its own row simply travels with the page — sticky gives it no room to move, which is
|
|
484
|
+
* the correct behaviour rather than a special case.
|
|
485
|
+
*/
|
|
486
|
+
[data-layout="record"]>[data-slot="prose-column"],
|
|
487
|
+
[data-section="simulation"]>[data-slot="beside"],
|
|
488
|
+
[data-section="hero"]>[data-slot="beside"],
|
|
489
|
+
[data-layout="tool"]>[data-slot="beside"],
|
|
490
|
+
[data-page="constructor"]>[data-panel="inspector"]{
|
|
491
|
+
position:sticky;
|
|
492
|
+
top:0;
|
|
493
|
+
align-self:start;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/*
|
|
497
|
+
* And it stops staying put where there is no second column to stay beside.
|
|
498
|
+
*
|
|
499
|
+
* One declaration takes back the one above, with the same selectors so it wins on source
|
|
500
|
+
* order rather than on a specificity trick. Each of these layouts already collapses to one
|
|
501
|
+
* column at this width; the sticky rule did not follow, because the position:static in
|
|
502
|
+
* those media queries names every child and lost to the five selectors above.
|
|
503
|
+
*
|
|
504
|
+
* What that looked like: on a phone the record page stacked its two halves and then pinned
|
|
505
|
+
* the prose to the top edge, so scrolling drew the rest of the page underneath it.
|
|
506
|
+
*/
|
|
507
|
+
@media (max-width:1100px){
|
|
508
|
+
[data-layout="record"]>[data-slot="prose-column"],
|
|
509
|
+
[data-section="simulation"]>[data-slot="beside"],
|
|
510
|
+
[data-section="hero"]>[data-slot="beside"],
|
|
511
|
+
[data-layout="tool"]>[data-slot="beside"],
|
|
512
|
+
[data-page="constructor"]>[data-panel="inspector"]{
|
|
513
|
+
position:static;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/*
|
|
518
|
+
* The picture first and the controls under it, which is the order the canvas draws.
|
|
519
|
+
*
|
|
520
|
+
* A grid rather than a flex column, and the reason is the fitting below: a percentage
|
|
521
|
+
* height resolves against a grid row that was given a size, and does not resolve against
|
|
522
|
+
* a flex item that merely ended up one. Measured in the browser both ways — flexed, the
|
|
523
|
+
* picture kept its own 1064px inside a 664px frame and was clipped.
|
|
524
|
+
*/
|
|
525
|
+
[data-layout="record"] [data-slot="graph-column"] [data-section="graph-viewer"]{
|
|
526
|
+
min-height:0;
|
|
527
|
+
display:grid;
|
|
528
|
+
grid-template-rows:auto auto;
|
|
529
|
+
}
|
|
530
|
+
/* The step being read, named at the end of the bar the way the canvas names it. */
|
|
531
|
+
[data-layout="record"] [data-slot="graph-column"] [data-slot="reading"]{
|
|
532
|
+
margin-left:auto;
|
|
533
|
+
color:var(--rb-trust);
|
|
534
|
+
font-family:var(--rb-mono);
|
|
535
|
+
font-size:var(--rb-meta-size);
|
|
536
|
+
}
|
|
537
|
+
[data-layout="record"] [data-slot="graph-column"] [data-slot="controls"]{
|
|
538
|
+
order:1;
|
|
539
|
+
margin:0;
|
|
540
|
+
border-top:1px solid var(--rb-border);
|
|
541
|
+
padding:calc(var(--rb-step) * 1.5) calc(var(--rb-step) * 3);
|
|
542
|
+
}
|
|
543
|
+
[data-layout="record"] [data-slot="graph-column"] [data-slot="frame"]{
|
|
544
|
+
min-height:0;
|
|
545
|
+
max-height:none;
|
|
546
|
+
border:0;
|
|
547
|
+
border-radius:0;
|
|
548
|
+
background:transparent;
|
|
549
|
+
display:flex;
|
|
550
|
+
align-items:center;
|
|
551
|
+
justify-content:center;
|
|
552
|
+
padding:calc(var(--rb-step) * 3);
|
|
553
|
+
}
|
|
554
|
+
/*
|
|
555
|
+
* A graph takes the width it is given, and its container takes its height. Everywhere.
|
|
556
|
+
*
|
|
557
|
+
* This was written inside the record layout and left every other graph — the dry pass, a
|
|
558
|
+
* full-screen view — drawn at its own size in the left third of a frame that ran the width
|
|
559
|
+
* of the screen. The default is the rule; the record page only adds the border and the
|
|
560
|
+
* order of the controls.
|
|
561
|
+
*
|
|
562
|
+
* Nothing drags and nothing scrolls until somebody zooms in: at rest the whole picture is
|
|
563
|
+
* there, so the page keeps the only scrollbar on the screen.
|
|
564
|
+
*
|
|
565
|
+
* And the page keeps the wheel too. Hidden overflow leaves the frame a scroll container
|
|
566
|
+
* that cannot scroll, and contained overscroll stops the chaining out of it, so a wheel
|
|
567
|
+
* over the graph did nothing at all: not the picture, not the page. Clipping is the same
|
|
568
|
+
* visual result and is not a scroll container, and touch is handed back for the state that
|
|
569
|
+
* has no gesture to protect.
|
|
570
|
+
*
|
|
571
|
+
* Containment is gone from the frame entirely, and that is the fix rather than a detail of
|
|
572
|
+
* it. It was written for a pan that runs past its end, and it also fires wherever the
|
|
573
|
+
* frame happens not to be scrollable — which the record layout guarantees, because there
|
|
574
|
+
* the frame grows instead of scrolling. Measured in a browser: zoomed in, the wheel moved
|
|
575
|
+
* neither the picture nor the page. Chaining at the end of a scroll is what a reader
|
|
576
|
+
* expects; a wheel that does nothing is not.
|
|
577
|
+
*/
|
|
578
|
+
[data-section="graph-viewer"] [data-slot="frame"][data-zoomed="false"]{
|
|
579
|
+
overflow:clip;
|
|
580
|
+
cursor:default;
|
|
581
|
+
max-height:none;
|
|
582
|
+
touch-action:auto;
|
|
583
|
+
}
|
|
584
|
+
[data-section="graph-viewer"] [data-slot="frame"][data-zoomed="false"] [data-slot="canvas"],
|
|
585
|
+
[data-section="graph-viewer"] [data-slot="frame"][data-zoomed="false"] [data-slot="scaled"]{
|
|
586
|
+
display:block;
|
|
587
|
+
min-width:0;
|
|
588
|
+
width:100%;
|
|
589
|
+
height:auto;
|
|
590
|
+
}
|
|
591
|
+
/*
|
|
592
|
+
* Width leads and height follows: a graph is drawn to be read, and a picture that fits by
|
|
593
|
+
* height leaves half its column empty beside a tall procedure. It still may not exceed the
|
|
594
|
+
* screen, so the height is capped rather than fixed — below that cap the width is what
|
|
595
|
+
* decides.
|
|
596
|
+
*/
|
|
597
|
+
/*
|
|
598
|
+
* The picture fills the column it was given.
|
|
599
|
+
*
|
|
600
|
+
* This said down-to-fit-never-up for a while, and the reason was real: the renderer picks
|
|
601
|
+
* the drawing size for legibility — node widths, and type at 14 — so scaling the picture
|
|
602
|
+
* up scales its labels with it, and a five-node dry pass across a wide column comes out
|
|
603
|
+
* with type larger than the page's own headings. The trade was made the other way on
|
|
604
|
+
* purpose: a graph that stops halfway across its column reads as a picture that failed to
|
|
605
|
+
* load, and half an empty column is a worse answer than large labels. The viewBox does the
|
|
606
|
+
* scaling, so the drawing keeps its proportions and nothing is cropped.
|
|
607
|
+
*/
|
|
608
|
+
[data-section="graph-viewer"] [data-slot="frame"][data-zoomed="false"] svg{
|
|
609
|
+
width:100%;
|
|
610
|
+
height:auto;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* One column below the breakpoint, and nothing sticky.
|
|
615
|
+
*
|
|
616
|
+
* A sticky graph on a phone is a graph occupying the screen the prose needs. The two
|
|
617
|
+
* columns become the order they are written in, which is the order they read in.
|
|
618
|
+
*/
|
|
619
|
+
@media (max-width:1100px){
|
|
620
|
+
[data-layout="record"]{ grid-template-columns:minmax(0,1fr); }
|
|
621
|
+
[data-layout="record"]>*{ grid-column:1; position:static; }
|
|
622
|
+
/*
|
|
623
|
+
* One column has nothing to separate, and nothing to indent away from.
|
|
624
|
+
*
|
|
625
|
+
* The rule down the graph column is what stands between the two columns. Stacked, it
|
|
626
|
+
* became a vertical line down the right edge of a full-width block, separating the page
|
|
627
|
+
* from the window. The prose column's own 40px sat on top of the page padding, which
|
|
628
|
+
* already tightens with the viewport: measured at 390px, 56 pixels a side went to
|
|
629
|
+
* margin, leaving 259 for the text.
|
|
630
|
+
*/
|
|
631
|
+
[data-layout="record"]>[data-slot="graph-column"]{ border-right:0; }
|
|
632
|
+
[data-layout="record"]>[data-slot="prose-column"]{
|
|
633
|
+
padding:calc(var(--rb-step) * 3) 0 calc(var(--rb-step) * 6);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/*
|
|
638
|
+
* The composition, drawn member by member.
|
|
639
|
+
*
|
|
640
|
+
* The playbook borrows the record's grid, so the column and the sticky prose beside it
|
|
641
|
+
* come for free; what is its own is the stack inside. Each member is a figure with its
|
|
642
|
+
* caption above the picture, because the caption says whose procedure this is and whose
|
|
643
|
+
* trust level it carries — read after the drawing it names, it is a footnote to something
|
|
644
|
+
* the reader has already taken on faith.
|
|
645
|
+
*
|
|
646
|
+
* The pictures take the column's width the way every other graph on the site does.
|
|
647
|
+
*/
|
|
648
|
+
[data-page="playbook"] [data-slot="graph-column"] [data-section="drawn"]{
|
|
649
|
+
padding:calc(var(--rb-step) * 3);
|
|
650
|
+
}
|
|
651
|
+
[data-page="playbook"] figure[data-member-picture]{
|
|
652
|
+
margin:0 0 calc(var(--rb-step) * 4);
|
|
653
|
+
}
|
|
654
|
+
[data-page="playbook"] figure[data-member-picture] figcaption{
|
|
655
|
+
margin-bottom:var(--rb-step);
|
|
656
|
+
font-family:var(--rb-mono);
|
|
657
|
+
font-size:var(--rb-meta-size);
|
|
658
|
+
color:var(--rb-text-secondary);
|
|
659
|
+
}
|
|
660
|
+
[data-page="playbook"] figure[data-member-picture] svg{ width:100%; height:auto; }
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* The dry pass reads in two columns, because it was drawn in two.
|
|
664
|
+
*
|
|
665
|
+
* The picture and the questions about it are the record page's shape again: width belongs
|
|
666
|
+
* to the graph, and what a reader answers stays beside it rather than under it. The
|
|
667
|
+
* artboard has had these two columns since the page was designed; the page shipped as one,
|
|
668
|
+
* and nothing compared them, so a reader answered a fork with the graph scrolled off the
|
|
669
|
+
* top — which is the moment the picture matters most.
|
|
670
|
+
*
|
|
671
|
+
* Placed rather than wrapped: the runner's children are flat, so the picture takes the
|
|
672
|
+
* first column and everything else the second, the way the hero does. Neither half is
|
|
673
|
+
* sticky: the graph is bounded and the answers are short, so there is nothing long enough
|
|
674
|
+
* to travel past the other.
|
|
675
|
+
*/
|
|
676
|
+
[data-section="simulation"]{
|
|
677
|
+
display:grid;
|
|
678
|
+
grid-template-columns:minmax(0,1fr) minmax(0,380px);
|
|
679
|
+
column-gap:calc(var(--rb-step) * 5);
|
|
680
|
+
align-items:start;
|
|
681
|
+
}
|
|
682
|
+
[data-section="simulation"]>[data-slot="picture"]{ grid-column:1; grid-row:1; }
|
|
683
|
+
[data-section="simulation"]>[data-slot="beside"]{ grid-column:2; grid-row:1; }
|
|
684
|
+
|
|
685
|
+
@media (max-width:1100px){
|
|
686
|
+
[data-section="simulation"]{ grid-template-columns:minmax(0,1fr); }
|
|
687
|
+
[data-section="simulation"]>*{ grid-column:1; grid-row:auto; position:static; }
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* The home page's hero graph, bounded by height.
|
|
692
|
+
*
|
|
693
|
+
* §18.4 puts the graph above the fold, and a width cap alone does not keep it there: a
|
|
694
|
+
* procedure laid out top to bottom is a thousand pixels tall, so scaling it to the column
|
|
695
|
+
* width still leaves a picture that runs off the first screen and pushes everything the
|
|
696
|
+
* page has to say below it. The hero had no rule of its own at all — it was drawn at
|
|
697
|
+
* whatever size the renderer produced, and the day the layout turned vertical it stopped
|
|
698
|
+
* being a poster and became a wall.
|
|
699
|
+
*
|
|
700
|
+
* Height is the constraint, width follows it, and the viewBox does the scaling. It is a
|
|
701
|
+
* poster rather than a document: the reader is meant to see that a procedure is a graph
|
|
702
|
+
* with a branch and a dangerous step in it, and to open the record for the rest.
|
|
703
|
+
*/
|
|
704
|
+
/**
|
|
705
|
+
* The hero is the graph and the words about it, side by side.
|
|
706
|
+
*
|
|
707
|
+
* Centred in a full-width page the picture became an island: the prose sat at its measure
|
|
708
|
+
* on the left, the graph floated in the middle, and the space between them was the widest
|
|
709
|
+
* thing on the screen. Drawn in the canvas as two columns, and that is what makes the
|
|
710
|
+
* width worth having — the half that gains from it takes it, and the half that would be
|
|
711
|
+
* harmed by it keeps the measure.
|
|
712
|
+
*/
|
|
713
|
+
[data-section="hero"]{
|
|
714
|
+
display:grid;
|
|
715
|
+
grid-template-columns:minmax(0,1fr) minmax(0,420px);
|
|
716
|
+
column-gap:calc(var(--rb-step) * 5);
|
|
717
|
+
align-items:start;
|
|
718
|
+
}
|
|
719
|
+
[data-section="hero"]>h2{ grid-column:1 / -1; grid-row:1; }
|
|
720
|
+
[data-section="hero"]>.hero-graph{ grid-column:1; grid-row:2; align-self:start; }
|
|
721
|
+
[data-section="hero"]>[data-slot="beside"]{ grid-column:2; grid-row:2; min-width:0; }
|
|
722
|
+
|
|
723
|
+
@media (max-width:1100px){
|
|
724
|
+
[data-section="hero"]{ grid-template-columns:minmax(0,1fr); }
|
|
725
|
+
[data-section="hero"]>*{ grid-column:1; grid-row:auto; position:static; }
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.hero-graph{ display:flex; justify-content:center; margin:calc(var(--rb-step) * 2) 0; }
|
|
729
|
+
.hero-graph svg{ width:100%; height:auto; max-width:100%; max-height:min(58vh, 540px); }
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* The graph's frame.
|
|
733
|
+
*
|
|
734
|
+
* A bounded, scrolling box rather than a picture that sets the page's width: a procedure
|
|
735
|
+
* is as wide as it is, and letting it push the column out moves every paragraph on the
|
|
736
|
+
* page to accommodate a diagram. Panning is a pointer gesture, so text selection inside
|
|
737
|
+
* the frame is off — a drag that selects the labels instead of moving the graph reads as
|
|
738
|
+
* the page being broken.
|
|
739
|
+
*/
|
|
740
|
+
[data-section="graph-viewer"]{ margin:calc(var(--rb-step) * 2) 0 calc(var(--rb-step) * 3); }
|
|
741
|
+
[data-section="graph-viewer"] [data-slot="frame"]{
|
|
742
|
+
overflow:auto;
|
|
743
|
+
max-height:70vh;
|
|
744
|
+
border:1px solid var(--rb-border);
|
|
745
|
+
border-radius:var(--rb-radius);
|
|
746
|
+
background:var(--rb-surface);
|
|
747
|
+
cursor:grab;
|
|
748
|
+
touch-action:none;
|
|
749
|
+
user-select:none;
|
|
750
|
+
-webkit-user-select:none;
|
|
751
|
+
}
|
|
752
|
+
[data-section="graph-viewer"] [data-slot="frame"]:active{ cursor:grabbing; }
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* On a phone the graph is a card, not the screen.
|
|
756
|
+
*
|
|
757
|
+
* Seventy per cent of the viewport is most of a phone, and a reader who came for the steps
|
|
758
|
+
* meets a picture they then have to scroll past. What the card has to do here is what a
|
|
759
|
+
* silhouette does on a catalog page: show that the procedure branches and that there is red
|
|
760
|
+
* in it. Reading it is the full-screen view's job, and the control that opens it is the one
|
|
761
|
+
* thing on the card that has to be easy to hit.
|
|
762
|
+
*
|
|
763
|
+
* Full screen keeps the whole viewport, because that is the view that exists to be read.
|
|
764
|
+
*/
|
|
765
|
+
@media (max-width:700px){
|
|
766
|
+
[data-section="graph-viewer"] [data-slot="frame"]{ max-height:34vh; }
|
|
767
|
+
[data-section="graph-viewer"]:fullscreen [data-slot="frame"]{ max-height:none; }
|
|
768
|
+
[data-section="graph-viewer"] [data-slot="controls"]{ flex-wrap:wrap; row-gap:var(--rb-step); }
|
|
769
|
+
[data-section="graph-viewer"] [data-slot="hint"]{ display:none; }
|
|
770
|
+
}
|
|
771
|
+
[data-section="graph-viewer"] [data-slot="canvas"]{ width:max-content; }
|
|
772
|
+
[data-section="graph-viewer"] [data-slot="scaled"]{ width:max-content; }
|
|
773
|
+
[data-section="graph-viewer"] [data-slot="scaled"]>svg{ display:block; max-width:none; }
|
|
774
|
+
[data-section="graph-viewer"] [data-slot="controls"]{
|
|
775
|
+
display:flex;
|
|
776
|
+
align-items:center;
|
|
777
|
+
gap:var(--rb-step);
|
|
778
|
+
margin-bottom:var(--rb-step);
|
|
779
|
+
font-size:var(--rb-meta-size);
|
|
780
|
+
color:var(--rb-text-secondary);
|
|
781
|
+
}
|
|
782
|
+
[data-section="graph-viewer"] [data-slot="controls"] button{
|
|
783
|
+
min-width:2.2em;
|
|
784
|
+
padding:calc(var(--rb-step) * 0.4) calc(var(--rb-step) * 0.8);
|
|
785
|
+
}
|
|
786
|
+
[data-section="graph-viewer"] [data-slot="zoom"]{
|
|
787
|
+
font-family:var(--rb-mono);
|
|
788
|
+
font-variant-numeric:tabular-nums;
|
|
789
|
+
min-width:4.5ch;
|
|
790
|
+
text-align:center;
|
|
791
|
+
}
|
|
792
|
+
[data-section="graph-viewer"] [data-slot="hint"]{ margin-left:auto; }
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Full screen. The browser's own mode where it exists, and a fixed cover where it does
|
|
796
|
+
* not — an iPhone has no element fullscreen, and a control that silently does nothing is
|
|
797
|
+
* worse than one that does the useful part.
|
|
798
|
+
*/
|
|
799
|
+
[data-section="graph-viewer"]:fullscreen{
|
|
800
|
+
background:var(--rb-background);
|
|
801
|
+
padding:calc(var(--rb-step) * 2);
|
|
802
|
+
display:flex;
|
|
803
|
+
flex-direction:column;
|
|
804
|
+
}
|
|
805
|
+
[data-section="graph-viewer"]:fullscreen [data-slot="frame"]{ flex:1; max-height:none; }
|
|
806
|
+
[data-section="graph-viewer"][data-full="true"]:not(:fullscreen){
|
|
807
|
+
position:fixed;
|
|
808
|
+
inset:0;
|
|
809
|
+
z-index:50;
|
|
810
|
+
margin:0;
|
|
811
|
+
padding:calc(var(--rb-step) * 2);
|
|
812
|
+
background:var(--rb-background);
|
|
813
|
+
display:flex;
|
|
814
|
+
flex-direction:column;
|
|
815
|
+
}
|
|
816
|
+
[data-section="graph-viewer"][data-full="true"]:not(:fullscreen) [data-slot="frame"]{
|
|
817
|
+
flex:1;
|
|
818
|
+
max-height:none;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
@media (max-width:600px){
|
|
822
|
+
[data-section="graph-viewer"] [data-slot="frame"]{ max-height:60vh; }
|
|
823
|
+
/* The hint describes a pointer nobody on a phone has. */
|
|
824
|
+
[data-section="graph-viewer"] [data-slot="hint"]{ display:none; }
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* A target a finger can hit.
|
|
829
|
+
*
|
|
830
|
+
* 44px is the smallest square most people can reliably tap, and the controls on this site
|
|
831
|
+
* are laid out for a pointer: a zoom button was 22px tall and a facet chip barely more.
|
|
832
|
+
* Applied where there is no fine pointer rather than at a width, because the question is
|
|
833
|
+
* what is doing the pointing, not how wide the screen is — a touch laptop needs this and
|
|
834
|
+
* a narrow desktop window does not.
|
|
835
|
+
*/
|
|
836
|
+
@media (pointer:coarse){
|
|
837
|
+
button,
|
|
838
|
+
[data-chip="facet"],
|
|
839
|
+
summary,
|
|
840
|
+
header nav a,
|
|
841
|
+
input,
|
|
842
|
+
select{
|
|
843
|
+
min-height:44px;
|
|
844
|
+
}
|
|
845
|
+
button,[data-chip="facet"]{ min-width:44px; justify-content:center; }
|
|
846
|
+
/* A link inside a paragraph is read, not tapped as a control; only lists of links get
|
|
847
|
+
the spacing, or every article would be double-spaced. */
|
|
848
|
+
[data-slot="chips"]{ gap:calc(var(--rb-step) * 1.25); }
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* The catalog: facets beside the results, not above them.
|
|
853
|
+
*
|
|
854
|
+
* A sidebar at a width that fits the longest facet value and no more, and the rest of the
|
|
855
|
+
* shell to the records. Below the breakpoint the columns stack and the facets become a
|
|
856
|
+
* row of chips that wraps — which is the same markup, read the other way.
|
|
857
|
+
*/
|
|
858
|
+
[data-layout="catalog"]{
|
|
859
|
+
display:grid;
|
|
860
|
+
grid-template-columns:minmax(0,15rem) minmax(0,1fr);
|
|
861
|
+
gap:calc(var(--rb-step) * 5);
|
|
862
|
+
align-items:start;
|
|
863
|
+
}
|
|
864
|
+
[data-layout="catalog"] [data-section="facets"] h2{ margin-top:0; }
|
|
865
|
+
[data-layout="catalog"] [data-section="results"] h2{ margin-top:0; }
|
|
866
|
+
[data-facet]{ margin-bottom:calc(var(--rb-step) * 2.5); }
|
|
867
|
+
[data-facet] h3{ margin:0 0 var(--rb-step); color:var(--rb-text-secondary); font-size:var(--rb-meta-size); font-weight:500; }
|
|
868
|
+
|
|
869
|
+
/* A navigation control is not prose, so it carries no list marker. */
|
|
870
|
+
[data-slot="chips"]{ list-style:none; margin:0; padding:0; display:flex; flex-wrap:wrap; gap:calc(var(--rb-step) * 0.75); }
|
|
871
|
+
[data-slot="chips"]>li{ margin:0; max-width:none; }
|
|
872
|
+
[data-chip="facet"]{
|
|
873
|
+
display:inline-flex;
|
|
874
|
+
align-items:baseline;
|
|
875
|
+
gap:calc(var(--rb-step) * 0.6);
|
|
876
|
+
padding:calc(var(--rb-step) * 0.35) calc(var(--rb-step) * 0.9);
|
|
877
|
+
border:1px solid var(--rb-border);
|
|
878
|
+
border-radius:999px;
|
|
879
|
+
background:var(--rb-surface);
|
|
880
|
+
text-decoration:none;
|
|
881
|
+
font-size:var(--rb-meta-size);
|
|
882
|
+
}
|
|
883
|
+
[data-chip="facet"]:hover{ border-color:var(--rb-trust); }
|
|
884
|
+
[data-chip="facet"] [data-slot="count"]{ color:var(--rb-text-secondary); font-variant-numeric:${NUMERIC_FEATURES}; }
|
|
885
|
+
[data-slot="not-offered"],[data-slot="reading"]{ font-size:var(--rb-meta-size); color:var(--rb-text-secondary); }
|
|
886
|
+
/**
|
|
887
|
+
* Cards reflow by how many fit, not by how wide the window was said to be.
|
|
888
|
+
*
|
|
889
|
+
* One column at any width meant a 4K display showed three cards where it has room for
|
|
890
|
+
* eight, and a breakpoint list would answer only for the widths somebody thought of.
|
|
891
|
+
* auto-fill asks the one question that matters — how many cards of a readable width fit —
|
|
892
|
+
* and the inner min() keeps the answer at one when the screen is narrower than a card.
|
|
893
|
+
*/
|
|
894
|
+
/**
|
|
895
|
+
* The card leads with a silhouette, not a diagram.
|
|
896
|
+
*
|
|
897
|
+
* Its job is to say that the procedure branches and that there is red in it, at a glance
|
|
898
|
+
* and before a word is read. Drawn at its own size it came out as specks of different
|
|
899
|
+
* heights, so the cards were ragged and the shape was the one thing you could not see. A
|
|
900
|
+
* band of one height with the picture fitted inside gives every card the same thing to
|
|
901
|
+
* compare, which is what makes a row of them scannable.
|
|
902
|
+
*/
|
|
903
|
+
[data-slot="mini-graph"]{
|
|
904
|
+
display:flex;
|
|
905
|
+
align-items:center;
|
|
906
|
+
justify-content:center;
|
|
907
|
+
height:calc(var(--rb-step) * 14);
|
|
908
|
+
overflow:hidden;
|
|
909
|
+
}
|
|
910
|
+
[data-slot="mini-graph"] svg{ max-width:100%; max-height:100%; width:auto; height:auto; }
|
|
911
|
+
|
|
912
|
+
[data-slot="cards"]{
|
|
913
|
+
display:grid;
|
|
914
|
+
gap:calc(var(--rb-step) * 2);
|
|
915
|
+
grid-template-columns:repeat(auto-fill, minmax(min(100%, 22rem), 1fr));
|
|
916
|
+
align-items:start;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
@media (max-width:820px){
|
|
920
|
+
[data-layout="catalog"]{ grid-template-columns:minmax(0,1fr); gap:calc(var(--rb-step) * 3); }
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/*
|
|
924
|
+
* Two pictures over one coordinate system, side by side (W-17, §18.4).
|
|
925
|
+
*
|
|
926
|
+
* The diff page carried this attribute and the stylesheet had no rule for it, so the two
|
|
927
|
+
* graphs - laid out over one coordinate system precisely so they can be compared - would
|
|
928
|
+
* have stacked a full graph apart. Nobody saw it because the route generated no page: it
|
|
929
|
+
* asked for pairs from a list of one version, and the catalog kept one version. Both of
|
|
930
|
+
* those are fixed, so the rule has to exist before the first republication rather than
|
|
931
|
+
* after somebody opens the first diff.
|
|
932
|
+
*
|
|
933
|
+
* Placement through :where() and a stack below the breakpoint, for the reason the tool
|
|
934
|
+
* layout gives: a comparison is two columns or it is not a comparison, and on a phone it
|
|
935
|
+
* is one column read in order.
|
|
936
|
+
*/
|
|
937
|
+
[data-section="side-by-side"]{
|
|
938
|
+
display:grid;
|
|
939
|
+
grid-template-columns:minmax(0,1fr) minmax(0,1fr);
|
|
940
|
+
gap:calc(var(--rb-step) * 3);
|
|
941
|
+
align-items:start;
|
|
942
|
+
}
|
|
943
|
+
[data-section="side-by-side"]>:where(*){ min-width:0; }
|
|
944
|
+
|
|
945
|
+
@media (max-width:1100px){
|
|
946
|
+
[data-section="side-by-side"]{ grid-template-columns:minmax(0,1fr); }
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/* A pasted document, a command line, a step id: none of them may widen the page. */
|
|
950
|
+
pre{ max-width:100%; }
|
|
951
|
+
[data-slot="text"]{ overflow-x:auto; }
|
|
952
|
+
|
|
953
|
+
input,select,textarea,button{
|
|
954
|
+
font:inherit;
|
|
955
|
+
color:inherit;
|
|
956
|
+
background:var(--rb-surface);
|
|
957
|
+
border:1px solid var(--rb-border);
|
|
958
|
+
border-radius:var(--rb-radius);
|
|
959
|
+
padding:calc(var(--rb-step) * 0.75) var(--rb-step);
|
|
960
|
+
}
|
|
961
|
+
/*
|
|
962
|
+
* A field somebody pastes a document into is not prose.
|
|
963
|
+
*
|
|
964
|
+
* The measure keeps a paragraph readable; a document has structure, and seeing the shape
|
|
965
|
+
* of it is the point of pasting it here. Capped at the measure the import page showed a
|
|
966
|
+
* 700px field with half its column empty beside it and the button hanging off the edge.
|
|
967
|
+
*/
|
|
968
|
+
textarea{ font-family:var(--rb-mono); width:100%; max-width:100%; }
|
|
969
|
+
button{ cursor:pointer; }
|
|
970
|
+
button[aria-pressed="true"]{ background:var(--rb-text); color:var(--rb-background); }
|
|
971
|
+
button[disabled]{ cursor:not-allowed; color:var(--rb-text-secondary); }
|
|
972
|
+
label{ display:block; margin:calc(var(--rb-step) * 2) 0 calc(var(--rb-step) * 0.5); font-weight:500; }
|
|
973
|
+
|
|
974
|
+
form,fieldset{ border:0; margin:0; padding:0; }
|
|
975
|
+
fieldset{ margin:calc(var(--rb-step) * 2) 0; }
|
|
976
|
+
|
|
977
|
+
/* §18.3 rule 1's toggle, and every other run of controls. */
|
|
978
|
+
[role="group"]{ display:flex; gap:var(--rb-step); margin-bottom:calc(var(--rb-step) * 2); }
|
|
979
|
+
|
|
980
|
+
details{ margin:calc(var(--rb-step) * 2) 0; }
|
|
981
|
+
summary{ cursor:pointer; font-weight:500; }
|
|
982
|
+
|
|
983
|
+
@media (prefers-reduced-motion:reduce){
|
|
984
|
+
*,*::before,*::after{ animation-duration:0.01ms !important; animation-iteration-count:1 !important; transition-duration:0.01ms !important; scroll-behavior:auto !important; }
|
|
985
|
+
}
|
|
986
|
+
`;
|
|
987
|
+
}
|