oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1/* rastrillo/ui — design tokens and the component classes the shipped
2 partials use (see ui_test.go's TestAllPartialsAreDefined for the
3 current, authoritative list — deliberately not counted here, so this
4 comment cannot go stale as the set grows).
5
6 rastrillo new writes this file once into your app's static/ directory.
7 From then on it is yours: edit it, replace it, delete rules you do not
8 want. Nothing regenerates it, and rastrillo.Serve never serves it —
9 your app's own static handler does.
10
11 Every class, attribute and custom property is prefixed rst- — except
12 the single .icon rule, which styles the class rastrillo.Icon emits —
13 so dropping this file into an app with existing CSS has exactly one
14 known collision surface. There are no imports, no remote assets and no webfont:
15 this stylesheet fetches nothing, from anywhere, ever. That is a test,
16 not a wish (ui_test.go, TestTokensCSSIsSelfContained) — which is also
17 why this comment cannot spell those constructs out literally.
18
19 TWO SPELLINGS, ON PURPOSE, FOR ONE RELEASE. Every component rule
20 below names its class and its attribute twin: `.rst-btn, [rst-btn]`,
21 `.rst-btn--primary, [rst-btn~="primary"]`, `.rst-callout__body,
22 [rst-callout-body]`. The attribute spelling is the vocabulary this
23 library is moving to (design spec §6-v3); the class spelling is what
24 every app and every shipped partial writes today. Pairing them here
25 first is what makes the move non-breaking in both directions — this
26 file is written into your static/ once and frozen there, while the
27 partials upgrade with the module, so an app that has not refreshed
28 its copy must keep styling either way.
29
30 The grammar, so a rule you add follows it: the KIND is a bare
31 attribute; a VARIANT is a token in that attribute's value, matched
32 with ~= so `rst-btn="primary compact"` composes the way a class list
33 does; a PART is its own flat attribute (`rst-callout-body`), never a
34 value — the value slot means variant on every attribute or the
35 grammar is unreadable; and rst-tone carries the four tones that
36 data-tone carries today. Both members of a pair weigh exactly the
37 same: a class and an attribute selector are both (0,1,0), so pairing
38 changes no cascade anywhere. ui_test.go's
39 TestEveryClassSelectorHasAnAttributeTwin holds all of that, including
40 the specificity, and the browser drive proves the two spellings
41 compute identical styles.
42
43 Utilities keep the class spelling and are not paired:
44 rst-sr-only, rst-mono, rst-m-hide, rst-grow, rst-nm, rst-danger,
45 rst-cell-mut. They are not kinds — they are cross-cutting styling,
46 which is what class is for, and they stay class after the move.
47
48 Colour, the type family and shape are not here: they live in
49 themes/day.css (or whichever theme was scaffolded), which rastrillo new
50 writes beside this file as static/theme.css. This file is structure —
51 layout, spacing, the type scale, and the component classes, every one
52 of them painted with var(--rst-*). Swap the theme file and the app
53 changes palette, corner and depth; nothing here is touched.
54
55 Radius and shadow used to live in the scale block below. They moved to
56 the themes in v2, because a theme that can only change colour cannot
57 change how an app feels: day is soft (8px/6px, wide low-alpha
58 shadows), plain is nearly square (4px/3px, one hairline layer) and
59 signal is milled (4px/2px, short dense shadows). tokens.css still
60 paints with var(--rst-radius*) and var(--rst-shadow-*) everywhere; it
61 just no longer decides what they are.
62
63 Padding values are load-bearing for SC 2.5.8 Target Size (AA, new in
64 WCAG 2.2): pagination chips measure about 30px, row action pills
65 about 27px, and the button sizes about 28px (sm), 34px (default) and
66 44px (lg) on their smaller axis, all clear of the 24px minimum. Do
67 not shrink them without re-measuring; the button block shows the
68 arithmetic.
69
70 Type sizes are rem, not px, so the whole scale tracks a reader who has
71 raised their browser's default font size. Spacing is rem for the same
72 reason. Anything you add here should follow suit.
73 ──────────────────────────────────────────────────────────────────── */
74
75/* ── Scale: type and spacing. Theme-independent. Radius is not here —
76 it is part of the theme's shape axis; see themes/. ────────────── */
77:root {
78 --rst-fs-lg: 1.0625rem; /* 17px — the one step above base */
79 --rst-fs-base: 0.875rem; /* 14px at the browser default */
80 --rst-fs-sm: 0.78125rem; /* 12.5px */
81 --rst-fs-xs: 0.71875rem; /* 11.5px */
82
83 --rst-sp-1: 0.25rem;
84 --rst-sp-2: 0.5rem;
85 --rst-sp-3: 0.75rem;
86 --rst-sp-4: 1rem;
87 --rst-sp-5: 1.5rem;
88 --rst-sp-6: 2.5rem;
89
90 /* The scrollbar's gutter, reserved whether or not there is a
91 scrollbar in it, so a short page and a long one are the same width
92 and clicking between them does not slide the layout sideways.
93
94 A token rather than a class for three reasons: the opt-out is one
95 line in your own stylesheet (--rst-scrollbar-gutter: auto), it
96 layers like every other token, and custom properties are untouched
97 by the markup migration, so this survives it without an edit.
98
99 What it costs, chosen knowingly: a page too short to scroll now
100 reserves the strip as well, so there is a thin empty band at the
101 trailing edge where there was none. That is the price of never
102 shifting. both-edges would double the band to keep centred content
103 exactly centred, and was not taken: this system's pages are already
104 a max-width column inside a wider ground, so the asymmetry does not
105 land where it would be seen.
106
107 Do not over-claim what it fixes. macOS overlay scrollbars take no
108 layout space at all, so on a default Mac there was never anything
109 to shift. This is real on Windows and Linux, on a Mac set to
110 "always show scrollbars", and inside the design gallery's own
111 preview iframes. Chrome 94+, Firefox 97+, Safari 18.2+; an older
112 engine ignores the declaration and lands on the behaviour it has
113 today. */
114 --rst-scrollbar-gutter: stable;
115}
116
117/* ── Base. Three rules, so a freshly scaffolded app looks finished
118 before it has any CSS of its own. Delete them if your app owns
119 its own base styles. ──────────────────────────────────────────── */
120/* The gutter is declared on the root element, which is where the
121 viewport's scrollbar lives. It also fixes a second instance of the
122 same bug that nobody had attributed to the scrollbar: the modal's
123 scroll lock below (body:has(.rst-backdrop) { overflow: hidden })
124 takes the scrollbar away the instant a modal opens, and without a
125 reserved gutter the whole page slides sideways while the reader
126 watches. One declaration, both cases. */
127html {
128 scrollbar-gutter: var(--rst-scrollbar-gutter);
129}
130
131body {
132 margin: 0;
133 background: var(--rst-bg);
134 color: var(--rst-text);
135 font-family: var(--rst-font);
136 font-size: var(--rst-fs-base);
137 line-height: 1.5;
138}
139
140/* The content column every screen sits in. Your page markup emits this;
141 no partial does. 64rem, widened from 52rem after the v2 page went
142 live: 52rem is a reading measure, and this column holds list grids
143 and side-by-side form fields, which a reading measure squeezes. */
144.rst-page, [rst-page] {
145 box-sizing: border-box;
146 margin: 0 auto;
147 max-width: 64rem;
148 padding: var(--rst-sp-5) var(--rst-sp-4) var(--rst-sp-6);
149}
150
151/* rastrillo.Icon emits class="icon" with no width or height, so the icon
152 sizes from here and tracks the text beside it. */
153.icon {
154 block-size: 1em;
155 flex: none;
156 inline-size: 1em;
157}
158
159/* Focus is visible everywhere inside the app column (.rst-page) and
160 inside any component rendered outside one. :where() contributes no
161 specificity, so any rule below can override it. */
162:where(.rst-page, .rst-page-header, .rst-list, .rst-lbar, .rst-search, .rst-empty, .rst-pagination) :focus-visible, :where([rst-page], [rst-page-header], [rst-list], [rst-lbar], [rst-search], [rst-empty], [rst-pagination]) :focus-visible {
163 outline: 2px solid var(--rst-accent);
164 outline-offset: 2px;
165}
166
167
168/* ── Buttons: page-header's primary action, empty-state's CTA. ────── */
169.rst-btn, [rst-btn] {
170 align-items: center;
171 background: var(--rst-surface);
172 border: 1px solid var(--rst-line-strong);
173 border-radius: var(--rst-radius-sm);
174 color: var(--rst-text);
175 cursor: pointer;
176 display: inline-flex;
177 font-family: inherit;
178 font-size: var(--rst-fs-sm);
179 font-weight: 600;
180 gap: var(--rst-sp-2);
181 line-height: 1.4;
182 padding: 0.45rem 0.75rem;
183 text-decoration: none;
184}
185.rst-btn:hover, [rst-btn]:hover {
186 border-color: var(--rst-accent);
187 color: var(--rst-accent);
188}
189.rst-btn--primary, [rst-btn~="primary"] {
190 background: var(--rst-accent);
191 border-color: var(--rst-accent);
192 color: var(--rst-on-accent);
193}
194.rst-btn--primary:hover, [rst-btn~="primary"]:hover {
195 background: var(--rst-accent-strong);
196 border-color: var(--rst-accent-strong);
197 color: var(--rst-on-accent);
198}
199
200/* ghost — a quiet Cancel beside a primary/danger action: no fill, a
201 muted label, and it darkens toward the ordinary text colour on hover
202 rather than picking up the accent (the accent means "this navigates
203 somewhere new"; a ghost Cancel does not). */
204.rst-btn--ghost, [rst-btn~="ghost"] {
205 background: transparent;
206 color: var(--rst-text-muted);
207}
208.rst-btn--ghost:hover, [rst-btn~="ghost"]:hover {
209 border-color: var(--rst-line-strong);
210 color: var(--rst-text);
211}
212
213/* danger — a solid fill for a destructive submit (confirm-form,
214 bulk-bar's actions menu). --rst-tone-negative-fg already clears
215 4.5:1 as a filled background against --rst-on-accent's label colour
216 in both themes, so no new custom properties are needed — computed
217 with the same WCAG relative-luminance formula as each theme file's
218 own contrast table (day's values, shown; every theme's own pair is
219 gated the same way, in both schemes):
220 light --rst-on-accent (#ffffff) on --rst-tone-negative-fg (#b91c1c) 6.47:1
221 dark --rst-on-accent (#0b1220) on --rst-tone-negative-fg (#f79aa0) 8.99:1
222 Hover darkens via filter rather than a themed "negative-strong"
223 token: brightness() scales every channel of the already-themed fill
224 (and, being a post-paint filter, the label too), so one rule works
225 in both themes without a new pair of colours to maintain. Re-checked
226 with the label included in the scaled result, both still clear
227 4.5:1 by a wide margin (day's values again; a post-paint filter
228 result, so these two rows are illustrative rather than gated):
229 light 0.9× fill (#a61919) vs 0.9× label (#e6e6e6) 6.06:1
230 dark 0.9× fill (#de8a90) vs 0.9× label (#0a101c) 7.38:1
231 background/border-color/color are restated on :hover, matching
232 .rst-btn--primary:hover's pattern exactly — .rst-btn:hover (same
233 0,2,0 specificity, an earlier rule) sets border-color/color to the
234 accent, and a filter-only :hover rule wins nothing for properties it
235 never declares, so without this restatement the label silently
236 cascades to accent-on-red (~1.05:1) at the exact moment a user
237 commits to a destructive action. */
238.rst-btn--danger, [rst-btn~="danger"] {
239 background: var(--rst-tone-negative-fg);
240 border-color: var(--rst-tone-negative-fg);
241 color: var(--rst-on-accent);
242}
243.rst-btn--danger:hover, [rst-btn~="danger"]:hover {
244 background: var(--rst-tone-negative-fg);
245 border-color: var(--rst-tone-negative-fg);
246 color: var(--rst-on-accent);
247 filter: brightness(0.9);
248}
249
250/* ── Button sizes ─────────────────────────────────────────────────────
251 Three steps, and they are a size scale rather than a set of variants:
252 size composes with primary/ghost/danger, so `rst-btn="primary lg"` is
253 the ordinary spelling of a form's submit.
254
255 The default step is the one the page-header action and the row pill
256 want — small, dense, a control beside other controls. It was the ONLY
257 step for a long time, and that is how a form's submit ended up
258 wearing it: a 34px, 12.5px-labelled button stretched across a 44rem
259 column, which reads as a skinny bar rather than the thing the whole
260 screen is asking you to press. A CTA is not a toolbar button; give it
261 `lg`.
262
263 Measured on the smaller axis, against SC 2.5.8's 24px floor — padding
264 both sides, plus the label's line box, plus the 1px border top and
265 bottom (the border is why the default step is 34px and not 32px). The
266 header comment carries these too; keep the two in step:
267 sm 0.3rem × 2 + 11.5px × 1.4 + 2 = 9.6 + 16.1 + 2 ≈ 28px
268 default 0.45rem × 2 + 12.5px × 1.4 + 2 = 14.4 + 17.5 + 2 ≈ 34px
269 lg 0.7rem × 2 + 14px × 1.4 + 2 = 22.4 + 19.6 + 2 ≈ 44px
270 sm is the tightest thing in the file that is still a button. It is
271 for a dense row of them, not for making an ordinary button smaller,
272 and there is deliberately nothing below it. */
273.rst-btn--sm, [rst-btn~="sm"] {
274 font-size: var(--rst-fs-xs);
275 padding: 0.3rem 0.55rem;
276}
277.rst-btn--lg, [rst-btn~="lg"] {
278 font-size: var(--rst-fs-base);
279 padding: 0.7rem 1.15rem;
280}
281
282/* block — a button that fills its container, for the one-action form
283 (sign in, sign up, a confirm page) where the submit IS the screen.
284 It centres its own label: .rst-btn sets no justify-content, so a
285 stretched button's text sits at the row's flex-start and reads as a
286 mistake rather than a choice.
287
288 Pair it with lg. A block button at the default size is precisely the
289 skinny bar described above — the width comes from the container and
290 the height does not follow, so the ratio goes wrong in proportion to
291 how wide the column is. Nothing here enforces that pairing, because a
292 narrow container is a legitimate place for a block button at the
293 default size; the wide ones are where it fails. */
294.rst-btn--block, [rst-btn~="block"] {
295 display: flex;
296 justify-content: center;
297 width: 100%;
298}
299
300/* busy — the loading state rastrillo.js gives every submit button on
301 its way out, and the one state of a button that is not a variant:
302 any .rst-btn can wear it. The spinner is a real child element
303 (.rst-spin, further down) rather than a pseudo-element, because the
304 shim adds and removes it; .rst-btn's own `gap` spaces it from the
305 label, so there is no margin here to keep in step with the padding.
306 :disabled arrives a tick later, once the submission is under way —
307 the two are styled together so the button does not change twice.
308 Nothing here animates, so prefers-reduced-motion has nothing to say
309 to it: .rst-spin already stops rotating under that query. */
310.rst-btn[aria-busy="true"], [rst-btn][aria-busy="true"], .rst-btn:disabled, [rst-btn]:disabled {
311 cursor: default;
312}
313/* Dimmed, but not much. A disabled control is "incidental" under WCAG
314 1.4.3 and exempt from the contrast minimum, and this one is disabled
315 for a second — so the dimming is a hint that the button is not
316 listening, not a licence to make the label hard to read while
317 someone is waiting on it. */
318.rst-btn:disabled, [rst-btn]:disabled {
319 opacity: 0.8;
320}
321/* The ring must not be squeezed by a long label, and must not stretch
322 with the button's align-items: center default.
323
324 Placed by shape rather than by a name of its own: the busy spinner is
325 the shim's rst-spin child of an rst-btn, and there is nothing else it
326 could be. rst-btn__spin was the second name for one thing, and the
327 grammar of §6-v3 is explicit that a part is styled structurally where
328 structure says it. The shim now writes one attribute. */
329.rst-btn > .rst-spin, [rst-btn] > [rst-spin] {
330 align-self: center;
331 flex: none;
332}
333
334/* ── page-header ──────────────────────────────────────────────────── */
335.rst-page-header, [rst-page-header] {
336 align-items: baseline;
337 border-bottom: 1px solid var(--rst-header-rule);
338 display: flex;
339 flex-wrap: wrap;
340 gap: var(--rst-sp-3);
341 justify-content: space-between;
342 margin: 0 0 var(--rst-sp-5);
343 padding-bottom: var(--rst-sp-3);
344 position: relative;
345}
346/* The rake line is retired (design doc §6-v2.2). It drew a 2.5rem
347 accent stroke over this rule, flush to the inline start — the
348 library's one flourish, and, built from the same parts in the same
349 order, indistinguishable from a determinate progress bar at 12%. The
350 rule itself carries the theme now: --rst-header-rule is derived from
351 the theme's accent and line, so plain draws grey, day draws a barely
352 tinted hairline and signal a visibly tinted one, and an app that
353 wants its own sets one custom property instead of fighting an
354 ::after it cannot remove.
355
356 The declaration stays, rather than the rule being deleted, because a
357 stylesheet that merely stops mentioning the stroke says nothing to
358 the next reader; this says the stroke is gone on purpose. It also
359 neutralises a vendored theme or app sheet that still carries the old
360 flourish at a lower specificity.
361
362 THE RULE IS DECORATIVE AND CARRIES NO CONTRAST FLOOR. The heading's
363 size, weight and spacing carry the structure; the rule does not. It
364 is deliberately absent from ui/contrast_test.go's pair table, and a
365 later pass must not argue it up to 1.4.11's 3:1 — that floor was
366 never its. See ui/contrast_test.go's colorMixSkip comment.
367
368 position: relative stays: it is the containing block anything an app
369 absolutely positions inside its own header expects to find, and
370 removing it would move those elements rather than tidy the file. */
371.rst-page-header::after, [rst-page-header]::after {
372 content: none;
373}
374.rst-page-header__titles, [rst-page-header-titles] {
375 flex: 1 1 auto;
376 min-inline-size: 0;
377}
378.rst-page-header h1, [rst-page-header] h1 {
379 color: var(--rst-text);
380 font-size: 1.375rem;
381 font-weight: 600;
382 letter-spacing: -0.015em;
383 line-height: 1.25;
384 margin: 0;
385}
386.rst-page-header__sub, [rst-page-header-sub] {
387 color: var(--rst-text-muted);
388 font-size: var(--rst-fs-sm);
389 margin: var(--rst-sp-1) 0 0;
390 max-inline-size: 44rem;
391}
392
393/* ── The list card, its toolbar, and its search field ─────────────── */
394/* No padding here, on purpose: a list card holds rows, and each row pads
395 itself. A form, prose or a strip of links does NOT go in here — it
396 goes in .rst-box, the padded section card. Text touching the border
397 is the sign that content landed in the wrong card. */
398.rst-list, [rst-list] {
399 background: var(--rst-surface);
400 border: 1px solid var(--rst-line);
401 border-radius: var(--rst-radius);
402}
403
404/* Why there is no `overflow: hidden` here, since the obvious way to
405 clip rows to a rounded card is exactly that, and it IS what this rule
406 used to be.
407 A card that clips is a clipping context for everything absolutely
408 positioned inside it — so a bulk bar's Actions menu, or a row kebab's
409 panel, was sliced off at the card's edge the moment it opened. That
410 is any card holding a menu, not one instance of it, so the fix is
411 here rather than at the bulk bar.
412 `overflow: clip` is the same trap: it clips too. `overflow-clip-margin`
413 would push the clip box outward, but it pushes it outward on every
414 side and past the border radius — which is the corner clipping this
415 whole rule exists to do. So the card stops clipping and the rows round
416 THEMSELVES: the first full-bleed child takes the card's start corners,
417 the last takes its end corners, one step in for the 1px border. A
418 card's own rows (.rst-row, .rst-lrow, .rst-lbar, .rst-bulkbar) declare
419 no radius, which is what makes them full-bleed in the first place.
420
421 Both selectors are wrapped in :where() to weigh NOTHING — (0,0,0),
422 not the (0,2,0) a bare `.rst-list > :first-child` would carry. That is
423 the whole mechanism by which a child that owns its own shape keeps it:
424 a bare <form rst-search> or a .rst-empty as the direct first
425 or last child of a card declares its radius at (0,1,0) and therefore
426 wins, and so does any app class doing the same. Source order does NOT
427 decide this and cannot be relied on to — an earlier draft of this
428 comment said it did, and both those children painted lopsided corners
429 (7px over 6px in day) because (0,2,0) beat (0,1,0) whatever the order.
430 :where() rather than a :not(.rst-search, .rst-empty) exception list
431 because the list would need extending for every self-shaped child an
432 app or a later partial invents, and the thing being expressed is not
433 "except these two" but "unless the child says otherwise".
434 TestSelfShapedChildrenKeepTheirCornersInsideACard gates the
435 relationship, by measuring the same child inside a card and outside
436 one and requiring the two to agree.
437
438 KNOWN EDGE, not handled: :last-child is a DOM position, and the old
439 `overflow: hidden` clipped whatever was PAINTED last. So a trailing
440 <input type="hidden"> — which apps write constantly, and which has
441 display: none from the UA — takes the rounding and leaves the real
442 last row square. No template in the framework does this, so nothing
443 regressed; it is unhandled rather than overlooked because the CSS that
444 would handle it has to enumerate what "invisible" means in a selector
445 (:nth-last-child(1 of :not([type=hidden]):not([hidden]))), and the
446 commonest case of all — display: none from the app's own stylesheet —
447 is not expressible in a selector at all. A rule that covered two of
448 the three would read as covering all three. If you are looking at a
449 square corner on a rounded card, look for a hidden last child. */
450:where(.rst-list) > :where(:first-child),
451:where([rst-list]) > :where(:first-child),
452:where(.rst-card) > :where(:first-child),
453:where([rst-card]) > :where(:first-child) {
454 border-start-end-radius: calc(var(--rst-radius) - 1px);
455 border-start-start-radius: calc(var(--rst-radius) - 1px);
456}
457:where(.rst-list) > :where(:last-child),
458:where([rst-list]) > :where(:last-child),
459:where(.rst-card) > :where(:last-child),
460:where([rst-card]) > :where(:last-child) {
461 border-end-end-radius: calc(var(--rst-radius) - 1px);
462 border-end-start-radius: calc(var(--rst-radius) - 1px);
463}
464
465/* list-bar: the toolbar strip. The search field is capped at 20rem so
466 the space to its right stays reserved — that is where the filter and
467 sort controls attach when the dropdown slice lands. */
468.rst-lbar, [rst-lbar] {
469 align-items: center;
470 background: var(--rst-surface-2);
471 border-bottom: 1px solid var(--rst-line);
472 display: flex;
473 flex-wrap: wrap;
474 gap: var(--rst-sp-3);
475 padding: var(--rst-sp-2) var(--rst-sp-3);
476}
477/* list-bar-search wraps its form in <search>, the landmark element, so
478 the strip's flex child is the <search>, not the form. Both are sized
479 here: an app that hand-writes a bare <form rst-search> into
480 its own .rst-lbar keeps the layout it had before the element landed. */
481.rst-lbar > search,
482[rst-lbar] > search,
483.rst-lbar > .rst-search,
484[rst-lbar] > [rst-search] {
485 flex: 0 1 20rem;
486}
487
488/* list-bar-search renders this form, which is the field box itself. Used
489 on its own inside a .rst-list card it just needs margin; inside a
490 .rst-lbar the strip supplies the padding. The <search> around it is a
491 plain block box — it draws nothing, so the form remains the field. */
492.rst-search, [rst-search] {
493 align-items: center;
494 background: var(--rst-surface);
495 border: 1px solid var(--rst-line-strong);
496 border-radius: var(--rst-radius-sm);
497 display: flex;
498 gap: var(--rst-sp-2);
499 margin: 0;
500 padding: 0.3rem 0.55rem;
501}
502.rst-list > search,
503[rst-list] > search,
504.rst-list > .rst-search,
505[rst-list] > [rst-search] {
506 margin: var(--rst-sp-3);
507}
508.rst-search .icon, [rst-search] .icon {
509 color: var(--rst-text-faint);
510}
511.rst-search input[type="search"], [rst-search] input[type="search"] {
512 background: transparent;
513 border: 0;
514 color: var(--rst-text);
515 flex: 1;
516 font-family: inherit;
517 font-size: var(--rst-fs-sm);
518 min-inline-size: 0;
519 padding: 0.2rem 0;
520}
521.rst-search input[type="search"]::placeholder, [rst-search] input[type="search"]::placeholder {
522 color: var(--rst-text-faint);
523}
524/* The field box carries the focus indicator for the whole control, so
525 the input's own default outline would only double it up. */
526.rst-search input[type="search"]:focus-visible, [rst-search] input[type="search"]:focus-visible {
527 outline: none;
528}
529.rst-search:focus-within, [rst-search]:focus-within {
530 border-color: var(--rst-accent);
531 outline: 2px solid var(--rst-accent);
532 outline-offset: 1px;
533}
534/* The browser's own ✕ goes (design spec §6-v2.1b.6). It is
535 ::-webkit-search-cancel-button and it does exactly what it is
536 specified to do — it clears the input's VALUE — but a GET form
537 submits on submit, so the results stand and the address bar still
538 says ?q=. list-bar-search renders a real link in its place, and
539 leaving both would be two affordances, one of which lies.
540 -webkit-appearance is not redundant beside display: none: without it
541 WebKit reserves the button's box before the display ever applies. */
542.rst-search input[type="search"]::-webkit-search-cancel-button, [rst-search] input[type="search"]::-webkit-search-cancel-button {
543 -webkit-appearance: none;
544 appearance: none;
545 display: none;
546}
547/* The link that replaces it. 24px on BOTH axes is WCAG 2.2 SC 2.5.8
548 Target Size (AA), which this library gates in CI — an ✕ tucked inside
549 a field is exactly the shape that fails it, and a 17px chip on this
550 branch already did. Do not shrink it without re-measuring. */
551.rst-search__clear, [rst-search-clear] {
552 align-items: center;
553 border-radius: var(--rst-radius-sm);
554 color: var(--rst-text-faint);
555 display: inline-flex;
556 flex: none;
557 justify-content: center;
558 min-block-size: 24px;
559 min-inline-size: 24px;
560 text-decoration: none;
561}
562.rst-search__clear:hover, [rst-search-clear]:hover {
563 background: var(--rst-accent-soft);
564 color: var(--rst-text);
565}
566
567/* ── list-row-action ──────────────────────────────────────────────── */
568.rst-row, [rst-row] {
569 align-items: center;
570 border-top: 1px solid var(--rst-line);
571 display: flex;
572 gap: var(--rst-sp-3);
573 padding: var(--rst-sp-3);
574 position: relative;
575}
576.rst-row:first-child, [rst-row]:first-child {
577 border-top: 0;
578}
579/* The toolbar strip already draws the line under itself. */
580.rst-lbar + .rst-row, [rst-lbar] + [rst-row] {
581 border-top: 0;
582}
583.rst-row:hover, [rst-row]:hover {
584 background: var(--rst-surface-2);
585}
586.rst-row__lead, [rst-row-lead] {
587 align-items: center;
588 background: var(--rst-tone-neutral-bg);
589 block-size: 1.9rem;
590 border-radius: 50%;
591 color: var(--rst-tone-neutral-fg);
592 display: flex;
593 flex: none;
594 font-size: var(--rst-fs-xs);
595 font-weight: 600;
596 inline-size: 1.9rem;
597 justify-content: center;
598}
599.rst-row__lead[data-lead="positive"], [rst-row-lead][data-lead="positive"] {
600 background: var(--rst-tone-positive-bg);
601 color: var(--rst-tone-positive-fg);
602}
603.rst-row__lead[data-lead="warning"], [rst-row-lead][data-lead="warning"] {
604 background: var(--rst-tone-warning-bg);
605 color: var(--rst-tone-warning-fg);
606}
607.rst-row__lead[data-lead="negative"], [rst-row-lead][data-lead="negative"] {
608 background: var(--rst-tone-negative-bg);
609 color: var(--rst-tone-negative-fg);
610}
611.rst-row__lead[data-lead="accent"], [rst-row-lead][data-lead="accent"] {
612 background: var(--rst-accent-soft);
613 color: var(--rst-accent);
614}
615.rst-row__main, [rst-row-main] {
616 flex: 1;
617 min-inline-size: 0;
618}
619/* The name link's overlay makes the whole row clickable without nesting
620 one anchor inside another. The action pill sits above it. */
621.rst-row__main > a, [rst-row-main] > a {
622 color: var(--rst-text);
623 font-weight: 600;
624 text-decoration: none;
625}
626.rst-row__main > a::after, [rst-row-main] > a::after {
627 content: "";
628 inset: 0;
629 position: absolute;
630}
631.rst-row__main > a:hover, [rst-row-main] > a:hover {
632 text-decoration: underline;
633}
634.rst-row__sub, [rst-row-sub] {
635 color: var(--rst-text-muted);
636 display: block;
637 font-size: var(--rst-fs-sm);
638 margin-block-start: 0.1rem;
639}
640.rst-row__action, [rst-row-action] {
641 background: var(--rst-surface);
642 border: 1px solid var(--rst-line-strong);
643 border-radius: var(--rst-radius-pill);
644 color: var(--rst-text-muted);
645 flex: none;
646 font-size: var(--rst-fs-sm);
647 padding: 0.2rem 0.7rem;
648 position: relative;
649 text-decoration: none;
650 z-index: 1;
651}
652.rst-row__action:hover, [rst-row-action]:hover {
653 border-color: var(--rst-accent);
654 color: var(--rst-accent);
655}
656
657/* A status pill inside a row sits above the name link's overlay, like
658 the action pill does. */
659.rst-row .rst-status, [rst-row] [rst-status] {
660 flex: none;
661 position: relative;
662 z-index: 1;
663}
664
665/* ── status-pill. The dot is decoration; the label is the state. ──── */
666.rst-status, [rst-status] {
667 align-items: center;
668 background: var(--rst-tone-neutral-bg);
669 border-radius: var(--rst-radius-pill);
670 color: var(--rst-tone-neutral-fg);
671 display: inline-flex;
672 font-size: var(--rst-fs-xs);
673 font-weight: 600;
674 gap: 0.35rem;
675 line-height: 1.6;
676 padding: 0.15rem 0.55rem;
677}
678.rst-status::before, [rst-status]::before {
679 background: currentColor;
680 block-size: 0.375rem;
681 border-radius: 50%;
682 content: "";
683 flex: none;
684 inline-size: 0.375rem;
685}
686.rst-status[data-tone="positive"], [rst-status][rst-tone~="positive"] {
687 background: var(--rst-tone-positive-bg);
688 color: var(--rst-tone-positive-fg);
689}
690.rst-status[data-tone="warning"], [rst-status][rst-tone~="warning"] {
691 background: var(--rst-tone-warning-bg);
692 color: var(--rst-tone-warning-fg);
693}
694.rst-status[data-tone="negative"], [rst-status][rst-tone~="negative"] {
695 background: var(--rst-tone-negative-bg);
696 color: var(--rst-tone-negative-fg);
697}
698
699/* ── empty-state ──────────────────────────────────────────────────── */
700.rst-empty, [rst-empty] {
701 background: var(--rst-surface);
702 border: 1px dashed var(--rst-line-strong);
703 border-radius: var(--rst-radius);
704 padding: var(--rst-sp-6) var(--rst-sp-4);
705 text-align: center;
706}
707/* Class-based, so it styles the partial's <h2> without inheriting any
708 <h2> default: an empty state's heading is a real heading in the
709 document outline, not a paragraph dressed up as one. */
710.rst-empty__title, [rst-empty-title] {
711 color: var(--rst-text);
712 font-size: var(--rst-fs-lg);
713 font-weight: 600;
714 margin: 0 0 var(--rst-sp-2);
715}
716.rst-empty__body, [rst-empty-body] {
717 color: var(--rst-text-muted);
718 margin: 0 auto;
719 max-inline-size: 34rem;
720}
721.rst-empty__cta, [rst-empty-cta] {
722 margin: var(--rst-sp-4) 0 0;
723}
724
725/* ── pagination. The current page is a filled chip AND aria-current AND
726 bolder — three signals, so it never rests on colour alone. ────── */
727.rst-pagination, [rst-pagination] {
728 align-items: center;
729 display: flex;
730 flex-wrap: wrap;
731 font-variant-numeric: tabular-nums;
732 gap: var(--rst-sp-1);
733 margin: var(--rst-sp-4) 0;
734}
735.rst-pagination a,
736[rst-pagination] a,
737.rst-pagination span,
738[rst-pagination] span {
739 border: 1px solid var(--rst-line);
740 border-radius: var(--rst-radius-sm);
741 color: var(--rst-text-muted);
742 display: inline-block;
743 font-size: var(--rst-fs-sm);
744 min-inline-size: 2rem;
745 padding: 0.3rem 0.6rem;
746 text-align: center;
747 text-decoration: none;
748}
749.rst-pagination a:hover, [rst-pagination] a:hover {
750 background: var(--rst-accent-soft);
751 border-color: var(--rst-accent);
752 color: var(--rst-text);
753}
754.rst-pagination [aria-current="page"], [rst-pagination] [aria-current="page"] {
755 background: var(--rst-accent);
756 border-color: var(--rst-accent);
757 color: var(--rst-on-accent);
758 font-weight: 600;
759}
760.rst-pagination .rst-pagination__disabled, [rst-pagination] [rst-pagination-disabled] {
761 border-style: dashed;
762 color: var(--rst-text-faint);
763}
764.rst-pagination .rst-pagination__gap, [rst-pagination] [rst-pagination-gap] {
765 border-color: transparent;
766 min-inline-size: 0;
767 padding-inline: var(--rst-sp-1);
768}
769
770/* badge — uppercase bordered chip; tones reuse the pill tone pairs. */
771.rst-badge, [rst-badge] {
772 border: 1px solid var(--rst-line);
773 border-radius: var(--rst-radius-sm);
774 color: var(--rst-text-muted);
775 display: inline-block;
776 font-size: 0.6875rem;
777 font-variant-numeric: tabular-nums;
778 font-weight: 700;
779 letter-spacing: 0.04em;
780 padding: 0.05rem 0.4rem;
781 text-transform: uppercase;
782 white-space: nowrap;
783}
784.rst-badge--positive, [rst-badge~="positive"] { background: var(--rst-tone-positive-bg); border-color: var(--rst-tone-positive-fg); color: var(--rst-tone-positive-fg); }
785.rst-badge--warning, [rst-badge~="warning"] { background: var(--rst-tone-warning-bg); border-color: var(--rst-tone-warning-fg); color: var(--rst-tone-warning-fg); }
786.rst-badge--negative, [rst-badge~="negative"] { background: var(--rst-tone-negative-bg); border-color: var(--rst-tone-negative-fg); color: var(--rst-tone-negative-fg); }
787.rst-badge--neutral, [rst-badge~="neutral"] { background: var(--rst-tone-neutral-bg); border-color: var(--rst-tone-neutral-fg); color: var(--rst-tone-neutral-fg); }
788
789/* meter — 4px capacity bar; the number rides beside it as text.
790
791 The bar is a native <meter> (spec §6-v2.4), and styling one is the
792 one place in this file where the same declaration has to be written
793 three times: once on the element, once for the WebKit/Blink
794 pseudo-elements, once for the Firefox one. They cannot share a rule.
795 A selector list containing a pseudo-element an engine does not know
796 is invalid AS A WHOLE in that engine, so ::-webkit-meter-bar and
797 ::-moz-meter-bar in one comma-separated rule would style nothing
798 anywhere. That is why the repetition below is not tidied up.
799
800 The two engines also disagree about which pseudo-element is which.
801 In WebKit/Blink ::-webkit-meter-bar is the TRACK and
802 ::-webkit-meter-optimum-value is the FILL; in Firefox the element
803 itself is the track and ::-moz-meter-bar is the FILL. Read the three
804 blocks with that in mind or they look contradictory.
805
806 HONESTLY FLAGGED: the -moz- rule is unverified. This project's
807 browser drive is Chromium, so the WebKit/Blink path below is measured
808 and the Firefox path is written from the spec and reasoned about. It
809 was shipped that way knowingly (Paul's call). If a Firefox drive ever
810 lands, this is the first rule to point it at.
811
812 The old markup was a <span> with an <i> in it sized by
813 --rst-meter-fill. That custom property is gone; an app that set it is
814 setting nothing now, which is why it is called out in the changelog
815 rather than only here. */
816.rst-meter, [rst-meter] { align-items: center; color: var(--rst-text-muted); display: inline-flex; font-size: var(--rst-fs-xs); gap: var(--rst-sp-2); }
817.rst-meter__bar, [rst-meter-bar] { appearance: none; background: var(--rst-accent-soft); block-size: 4px; border: 0; border-radius: 2px; flex: 1; min-inline-size: 34px; }
818.rst-meter__bar::-webkit-meter-inner-element, [rst-meter-bar]::-webkit-meter-inner-element { block-size: 100%; }
819.rst-meter__bar::-webkit-meter-bar, [rst-meter-bar]::-webkit-meter-bar { background: var(--rst-accent-soft); block-size: 100%; border: 0; border-radius: 2px; }
820.rst-meter__bar::-webkit-meter-optimum-value, [rst-meter-bar]::-webkit-meter-optimum-value { background: var(--rst-accent); border-radius: 2px; }
821.rst-meter__bar::-moz-meter-bar, [rst-meter-bar]::-moz-meter-bar { background: var(--rst-accent); border-radius: 2px; }
822.rst-meter__num, [rst-meter-num] { font-variant-numeric: tabular-nums; white-space: nowrap; }
823
824/* stats — the instrument strip a dashboard or a report opens with: a
825 lead reading at headline size, companion readings beside it, divided
826 by hairlines inside one card.
827
828 One band, one cell, one lead variant. The headline stat and the
829 counts next to it are the same component at two sizes (spec
830 §6-v2.4), so there is no second kind to keep in step with this one.
831
832 flex-wrap and a flex-basis rather than a grid with a column count.
833 Any number of cells has to work — three on one dashboard, five on
834 the next — and a grid would need the caller to say how many. The
835 basis is what decides when the strip becomes two rows, and the lead
836 cell's is larger so it keeps the line to itself when the others
837 wrap under it.
838
839 The hairline is border-inline-start on every cell but the first,
840 which is what makes the strip read as ONE instrument rather than as
841 a row of cards. Logical, so it lands on the right in Arabic. A
842 wrapped cell keeps its line, and that is the one visual compromise
843 here: at two rows the first cell of the second row draws a leading
844 hairline against the card's edge. It is a hairline, and the
845 alternative — :nth-child arithmetic against a column count nobody
846 declared — is the grid this deliberately is not. */
847.rst-stats, [rst-stats] {
848 background: var(--rst-surface);
849 border: 1px solid var(--rst-line);
850 border-radius: var(--rst-radius);
851 display: flex;
852 flex-wrap: wrap;
853}
854.rst-stat, [rst-stat] {
855 border-inline-start: 1px solid var(--rst-line);
856 display: flex;
857 flex: 1 1 9rem;
858 flex-direction: column;
859 gap: 0.15rem;
860 min-inline-size: 0;
861 padding: var(--rst-sp-4);
862}
863.rst-stat:first-child, [rst-stat]:first-child {
864 border-inline-start: 0;
865}
866.rst-stat--lead, [rst-stat~="lead"] {
867 flex-basis: 14rem;
868}
869/* The eyebrow. Uppercase and tracked, the same grammar every label in
870 this system uses above a value, so a band of cells reads as one row
871 of labels rather than as four headings. */
872.rst-stat__label, [rst-stat-label] {
873 color: var(--rst-text-muted);
874 font-size: var(--rst-fs-xs);
875 font-weight: 650;
876 letter-spacing: 0.06em;
877 text-transform: uppercase;
878}
879/* tabular-nums so a column of readings lines up on its digits, and so
880 a number that changes on a poll does not jitter its neighbours. */
881.rst-stat__num, [rst-stat-num] {
882 font-size: 1.5rem;
883 font-variant-numeric: tabular-nums;
884 font-weight: 650;
885 line-height: 1.1;
886}
887.rst-stat--lead .rst-stat__num, [rst-stat~="lead"] [rst-stat-num] {
888 font-size: 2.1rem;
889}
890/* The delta is TEXT in a colour, not a colour. Its sign is written by
891 the caller and is the signal a reader who cannot tell the two hues
892 apart is reading; the colour is the second one (WCAG 2.2 AA 1.4.1).
893 Both hues are held at 4.5:1 against this card by the contrast gate
894 in the same commit that added them — they are text, so the floor
895 applies whether or not the colour carries meaning of its own. */
896.rst-stat__delta, [rst-stat-delta] {
897 font-size: var(--rst-fs-sm);
898 font-variant-numeric: tabular-nums;
899 font-weight: 600;
900}
901.rst-stat__delta[data-tone="positive"], [rst-stat-delta][rst-tone~="positive"] {
902 color: var(--rst-tone-positive-fg);
903}
904.rst-stat__delta[data-tone="negative"], [rst-stat-delta][rst-tone~="negative"] {
905 color: var(--rst-tone-negative-fg);
906}
907.rst-stat__delta[data-tone="neutral"], [rst-stat-delta][rst-tone~="neutral"] {
908 color: var(--rst-text-muted);
909}
910.rst-stat__note, [rst-stat-note] {
911 color: var(--rst-text-faint);
912 font-size: var(--rst-fs-xs);
913}
914
915/* person — avatar + name + email; avatar is decoration, text is identity. */
916.rst-person, [rst-person] { align-items: center; color: inherit; display: inline-flex; gap: 0.7rem; min-width: 0; text-decoration: none; }
917.rst-person__av, [rst-person-av] { align-items: center; background: var(--rst-accent); border-radius: 50%; color: var(--rst-on-accent); display: flex; flex: none; font-size: 0.6875rem; font-weight: 600; height: 28px; justify-content: center; width: 28px; }
918.rst-person__av--empty, [rst-person-av~="empty"] { background: transparent; border: 1px dashed var(--rst-line-strong); color: transparent; }
919.rst-person__meta, [rst-person-meta] { min-width: 0; }
920.rst-person__name, [rst-person-name] { display: block; font-weight: 550; line-height: 1.25; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
921.rst-person__email, [rst-person-email] { color: var(--rst-text-muted); display: block; font-size: var(--rst-fs-xs); line-height: 1.25; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
922.rst-person:hover .rst-person__name, [rst-person]:hover [rst-person-name] { color: var(--rst-accent); }
923.rst-person--lg .rst-person__av, [rst-person~="lg"] [rst-person-av] { font-size: 1.0625rem; height: 46px; width: 46px; }
924
925/* callout — the one alert vocabulary; icon sized to the first text line. */
926.rst-callout, [rst-callout] { align-items: flex-start; background: var(--rst-surface-2); border: 1px solid var(--rst-line-strong); border-radius: var(--rst-radius); display: flex; gap: 0.6rem; margin: var(--rst-sp-4) 0; padding: 0.85rem 1rem; }
927.rst-callout__ic, [rst-callout-ic] { align-items: center; color: var(--rst-accent); display: inline-flex; flex: none; height: 1.5rem; justify-content: center; width: 1.25rem; }
928.rst-callout__ic svg, [rst-callout-ic] svg, .rst-callout__ic .icon, [rst-callout-ic] .icon { display: block; height: 1.25rem; width: 1.25rem; }
929.rst-callout__body, [rst-callout-body] { line-height: 1.5; min-width: 0; }
930.rst-callout__body > strong, [rst-callout-body] > strong { display: block; font-weight: 600; }
931.rst-callout__body > p, [rst-callout-body] > p { color: var(--rst-text-muted); margin: 0.15rem 0 0; }
932.rst-callout__body > p:only-child, [rst-callout-body] > p:only-child { color: inherit; margin: 0; }
933.rst-callout__body > ul, [rst-callout-body] > ul { color: var(--rst-text-muted); margin: 0.15rem 0 0; padding-inline-start: 1.1rem; }
934.rst-callout[data-tone="positive"], [rst-callout][rst-tone~="positive"] { background: var(--rst-tone-positive-bg); border-color: var(--rst-tone-positive-fg); }
935.rst-callout[data-tone="positive"] > .rst-callout__ic, [rst-callout][rst-tone~="positive"] > [rst-callout-ic] { color: var(--rst-tone-positive-fg); }
936.rst-callout[data-tone="warning"], [rst-callout][rst-tone~="warning"] { background: var(--rst-tone-warning-bg); border-color: var(--rst-tone-warning-fg); }
937.rst-callout[data-tone="warning"] > .rst-callout__ic, [rst-callout][rst-tone~="warning"] > [rst-callout-ic] { color: var(--rst-tone-warning-fg); }
938.rst-callout[data-tone="negative"], [rst-callout][rst-tone~="negative"] { background: var(--rst-tone-negative-bg); border-color: var(--rst-tone-negative-fg); }
939.rst-callout[data-tone="negative"] > .rst-callout__ic, [rst-callout][rst-tone~="negative"] > [rst-callout-ic] { color: var(--rst-tone-negative-fg); }
940
941/* detail-list — a record's labelled facts as a definition list; the
942 partial emits .rst-detail, .rst-mono marking machine-ish values. */
943.rst-detail, [rst-detail] { display: grid; gap: var(--rst-sp-2) var(--rst-sp-5); grid-template-columns: max-content 1fr; margin: var(--rst-sp-4) 0; }
944.rst-detail dt, [rst-detail] dt { color: var(--rst-text-muted); font-size: var(--rst-fs-sm); font-weight: 600; }
945.rst-detail dd, [rst-detail] dd { margin: 0; }
946.rst-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: var(--rst-fs-sm); }
947
948/* ── Narrow screens ───────────────────────────────────────────────── */
949@media (max-width: 34rem) {
950 .rst-lbar > search,
951 [rst-lbar] > search,
952 .rst-lbar > .rst-search,
953 [rst-lbar] > [rst-search] {
954 flex: 1 1 100%;
955 }
956 .rst-row, [rst-row] {
957 align-items: flex-start;
958 flex-wrap: wrap;
959 }
960 .rst-page-header h1, [rst-page-header] h1 {
961 font-size: 1.25rem;
962 }
963}
964
965/* ── The structural idioms: box, list grid, row-menu, dropdown, ftok ── */
966
967/* box — the section card. Its heading (rst-box-head) is a SIBLING
968 before the box, never inside it: a screen is a stack of
969 section-header + card. A box-head action is a compact real button,
970 never a pill. */
971.rst-box, [rst-box] { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: var(--rst-radius); margin: var(--rst-sp-4) 0; padding: 1.1rem 1.25rem; }
972.rst-box > :first-child, [rst-box] > :first-child { margin-top: 0; }
973.rst-box > :last-child, [rst-box] > :last-child { margin-bottom: 0; }
974.rst-box-head, [rst-box-head] { align-items: baseline; display: flex; gap: var(--rst-sp-4); justify-content: space-between; margin: 1.5rem 0 0.5rem; }
975.rst-box-head:first-child, [rst-box-head]:first-child { margin-top: 0; }
976.rst-box-head + .rst-box, [rst-box-head] + [rst-box] { margin-top: 0; }
977.rst-box-head h2, [rst-box-head] h2 { font-size: var(--rst-fs-base); margin: 0; }
978.rst-box-foot, [rst-box-foot] { border-top: 1px solid var(--rst-line); color: var(--rst-text-muted); margin-top: var(--rst-sp-4); padding-top: 0.75rem; }
979.rst-box > .rst-detail, [rst-box] > [rst-detail] { margin: 0; }
980
981/* The list grid — the real data-table vocabulary. The card sets the
982 columns once (--rst-cols, inline, trailing 32px for the kebab);
983 rows only choose cells. Hover fill only on rows that contain the
984 identity link — a display-only row must never look clickable.
985 Like .rst-list, .rst-card has no padding: rows only. Anything that
986 is not a row belongs in .rst-box. */
987.rst-card, [rst-card] { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: var(--rst-radius); }
988.rst-lrow, [rst-lrow] { align-items: center; border-bottom: 1px solid var(--rst-line); display: grid; gap: 0.85rem; grid-template-columns: var(--rst-cols, 1fr); padding: 0.68rem 1rem; position: relative; }
989.rst-lrow:last-child, [rst-lrow]:last-child { border-bottom: 0; }
990.rst-lrow--head, [rst-lrow~="head"] { color: var(--rst-text-faint); font-size: 0.71875rem; font-weight: 550; letter-spacing: 0.05em; padding: 0.55rem 1rem; text-transform: uppercase; }
991.rst-lrow:not(.rst-lrow--head):has(> .rst-nm, > .rst-person):hover, [rst-lrow]:not([rst-lrow~="head"]):has(> .rst-nm, > [rst-person]):hover { background: var(--rst-accent-soft); }
992.rst-nm { border-radius: var(--rst-radius-sm); color: inherit; font-weight: 550; min-width: 0; text-align: start; text-decoration: none; }
993a.rst-nm:hover { color: var(--rst-accent); }
994.rst-nm small { color: var(--rst-text-muted); display: block; font-size: var(--rst-fs-xs); font-weight: 400; }
995.rst-cell-mut { color: var(--rst-text-muted); font-size: var(--rst-fs-xs); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
996.rst-no-match, [rst-no-match] { color: var(--rst-text-muted); font-size: var(--rst-fs-sm); padding: 1.6rem 1rem; text-align: center; }
997.rst-no-match a, [rst-no-match] a { color: var(--rst-accent); }
998.rst-count-line, [rst-count-line] { color: var(--rst-text-faint); font-size: var(--rst-fs-xs); margin: 0.7rem 0 0; }
999@media (max-width: 800px) {
1000 .rst-m-hide { display: none; }
1001 .rst-lrow, [rst-lrow], .rst-lrow--head, [rst-lrow~="head"] { grid-template-columns: minmax(0, 1fr) auto 32px; }
1002}
1003
1004/* row-menu — the per-row kebab: native details/summary, no JS. The
1005 destructive item sits last, class rst-danger, label ending "…". */
1006.rst-row-menu, [rst-row-menu] { justify-self: end; position: relative; }
1007.rst-row-menu > summary, [rst-row-menu] > summary { align-items: center; border-radius: var(--rst-radius-sm); color: var(--rst-text-faint); cursor: pointer; display: flex; height: 26px; justify-content: center; list-style: none; width: 26px; }
1008.rst-row-menu > summary::-webkit-details-marker, [rst-row-menu] > summary::-webkit-details-marker { display: none; }
1009.rst-row-menu > summary:hover, [rst-row-menu] > summary:hover, .rst-row-menu[open] > summary, [rst-row-menu][open] > summary { background: var(--rst-accent-soft); color: var(--rst-text); }
1010.rst-row-menu__panel, [rst-row-menu-panel] { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: 9px; box-shadow: var(--rst-shadow-pop); inset-inline-end: 0; min-width: 176px; padding: 0.25rem; position: absolute; top: calc(100% + 4px); z-index: 40; }
1011.rst-row-menu__panel a, [rst-row-menu-panel] a, .rst-row-menu__panel button, [rst-row-menu-panel] button { background: none; border: 0; border-radius: var(--rst-radius-sm); color: var(--rst-text); cursor: pointer; display: block; font: inherit; font-size: var(--rst-fs-sm); font-weight: 450; margin: 0; padding: 0.4rem 0.65rem; text-align: start; text-decoration: none; width: 100%; }
1012.rst-row-menu__panel a:hover, [rst-row-menu-panel] a:hover, .rst-row-menu__panel button:hover, [rst-row-menu-panel] button:hover { background: var(--rst-accent-soft); }
1013/* The separator every menu surface draws. Scoped to the row menu alone
1014 until the live page showed the topbar account menu wearing the UA's
1015 thick inset 3D <hr> instead: that panel is a .rst-dropdown__menu, and
1016 nothing named it. The three panels are one surface with three entry
1017 points, so one rule names all three — .rst-locale redundantly, since
1018 its panel carries .rst-dropdown__menu too, but a locale menu built
1019 without that class is still a menu and still needs the hairline. */
1020.rst-row-menu__panel hr,
1021[rst-row-menu-panel] hr,
1022.rst-dropdown__menu hr,
1023[rst-dropdown-menu] hr,
1024.rst-locale hr,
1025[rst-locale] hr { border: 0; border-top: 1px solid var(--rst-line); margin: 0.25rem 0; }
1026.rst-danger { color: var(--rst-tone-negative-fg); }
1027.rst-row-menu__panel .rst-danger:hover, [rst-row-menu-panel] .rst-danger:hover { background: var(--rst-tone-negative-bg); }
1028
1029/* dropdown — the details/summary menu vocabulary (header overflow,
1030 list-bar Filter/Sort). Exclusivity between siblings is the native
1031 details name attribute — zero JS. */
1032.rst-dropdown, [rst-dropdown] { position: relative; }
1033.rst-dropdown > summary, [rst-dropdown] > summary { align-items: center; color: var(--rst-text-muted); cursor: pointer; display: flex; gap: 0.3rem; list-style: none; padding: 0.4rem 0.6rem; white-space: nowrap; }
1034.rst-dropdown > summary::-webkit-details-marker, [rst-dropdown] > summary::-webkit-details-marker { display: none; }
1035.rst-dropdown > summary:hover, [rst-dropdown] > summary:hover, .rst-dropdown[open] > summary, [rst-dropdown][open] > summary { color: var(--rst-text); }
1036.rst-dropdown__menu, [rst-dropdown-menu] { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: 9px; box-shadow: var(--rst-shadow-pop); inset-inline-end: 0; margin-top: 4px; min-width: 176px; padding: 0.25rem; position: absolute; top: 100%; z-index: 30; }
1037.rst-dropdown__menu a, [rst-dropdown-menu] a { border-radius: var(--rst-radius-sm); color: inherit; display: block; font-size: var(--rst-fs-sm); padding: 0.4rem 0.65rem; text-decoration: none; }
1038.rst-dropdown__menu a:hover, [rst-dropdown-menu] a:hover, .rst-dropdown__menu a[aria-current], [rst-dropdown-menu] a[aria-current] { background: var(--rst-accent-soft); }
1039/* locale-menu — the language switcher is a dropdown whose items are
1040 one-field POST forms (spec §2.4), so the button inside each form
1041 takes the menu-link styling. Autonyms keep their own script, so no
1042 text-transform here, ever. */
1043.rst-locale form, [rst-locale] form { margin: 0; }
1044.rst-locale button, [rst-locale] button { background: none; border: 0; border-radius: var(--rst-radius-sm); color: var(--rst-text); cursor: pointer; display: block; font: inherit; font-size: var(--rst-fs-sm); padding: 0.4rem 0.65rem; text-align: start; width: 100%; }
1045.rst-locale button:hover, [rst-locale] button:hover { background: var(--rst-accent-soft); }
1046.rst-locale button[aria-current], [rst-locale] button[aria-current] { color: var(--rst-accent); font-weight: 600; }
1047/* Task 2's menu only ever held links; bulk-bar's actions menu (Task 4)
1048 is real submit buttons on the surrounding form, so the same panel
1049 needs button styling too — mirrors .rst-row-menu__panel's combined
1050 a/button rule above. */
1051.rst-dropdown__menu button, [rst-dropdown-menu] button { background: none; border: 0; border-radius: var(--rst-radius-sm); color: inherit; cursor: pointer; display: block; font: inherit; font-size: var(--rst-fs-sm); margin: 0; padding: 0.4rem 0.65rem; text-align: start; width: 100%; }
1052.rst-dropdown__menu button:hover, [rst-dropdown-menu] button:hover { background: var(--rst-accent-soft); }
1053/* The same content-box bug the inputs had, in the three menu panels:
1054 a full-width button inside a 0.25rem-padded panel measured 100% of
1055 the content box and then added its own 0.65rem of side padding, so
1056 every hover highlight bled ~20.8px past the panel's rounded edge.
1057 One rule for all three panels, because it is one cause. */
1058.rst-row-menu__panel a, [rst-row-menu-panel] a, .rst-row-menu__panel button, [rst-row-menu-panel] button, .rst-locale button, [rst-locale] button, .rst-dropdown__menu button, [rst-dropdown-menu] button { box-sizing: border-box; }
1059.rst-dropdown__menu .rst-danger:hover, [rst-dropdown-menu] .rst-danger:hover { background: var(--rst-tone-negative-bg); }
1060.rst-dropdown__menu .rst-menu-group > summary, [rst-dropdown-menu] [rst-menu-group] > summary { color: var(--rst-text-muted); cursor: pointer; font-size: var(--rst-fs-sm); list-style: none; padding: 0.4rem 0.65rem; }
1061.rst-dropdown__menu .rst-menu-group > summary::-webkit-details-marker, [rst-dropdown-menu] [rst-menu-group] > summary::-webkit-details-marker { display: none; }
1062.rst-dropdown__menu .rst-menu-group > div, [rst-dropdown-menu] [rst-menu-group] > div { padding-inline-start: 0.6rem; }
1063/* Menus that fit the viewport (design spec §6-v2.1b, 1 and 2). Two
1064 bugs, one surface — and one rule for all four panels, because
1065 .rst-combo__list and .rst-dtp__list have carried the cap and the
1066 scroll since they were written and these two never did.
1067
1068 THE CAP AND THE SCROLL come first because they work in every engine.
1069 The twelve-locale language menu measures 388px; on a short viewport
1070 its last entries were simply unreachable — no cap, no scroll, nothing
1071 to drag. The cap is the space a menu may take: 20rem where the window
1072 is tall, and the window itself less the room the anchor and the
1073 page's own chrome need where it is not. overscroll-behavior keeps a
1074 flick that bottoms the menu out from carrying on into the page behind
1075 it.
1076
1077 THE 8rem FLOOR IS NOT DECORATION, and it went in after the first
1078 version of this rule shipped without one. dvh is the viewport, and
1079 inside an iframe the viewport is THE FRAME — so in the design
1080 gallery's own 100px preview frames `100dvh - 6rem` computed to 4px
1081 and a three-item menu rendered as a 12px sliver; below a 96px frame
1082 it computed to zero and the menu was invisible. A fix aimed at menu
1083 entries nobody could reach had shipped a menu with no entries at all.
1084 The floor is the honest trade: in a frame too short for the menu it
1085 overflows, and an overflowing menu can be scrolled to, where an
1086 invisible one cannot be anything. Any app embedding a screen in a
1087 short iframe had the same bug, so this is not a gallery fix.
1088
1089 THE FLIP is CSS anchor positioning. A menu with no room below opens
1090 upward; one against the trailing edge opens the other way inline.
1091 Zero script.
1092
1093 It is Chromium-only today. That is accepted, and the reason is
1094 recorded here rather than left looking like an oversight: an engine
1095 without it lands on the fixed position above, which is exactly what
1096 every engine does today, so nothing regresses — and Firefox and
1097 Safari gain the behaviour with no release from us. The alternative
1098 was script, which would put positioning behind JavaScript and make
1099 the scriptless path worse than the one we already ship. That trade is
1100 the wrong way round for this library.
1101
1102 anchor-scope is load-bearing, not decoration. anchor-name is a
1103 document-wide name, so without a scope every menu on the page would
1104 resolve --rst-menu to the LAST element carrying it and all of them
1105 would pile onto one anchor. Scoping the name to each disclosure's own
1106 subtree is what lets one name serve every menu in the library.
1107
1108 Inside the @supports the fixed insets have to go. position-area
1109 supplies the containing block, so a leftover top: 100% would then be
1110 measured against THAT box and push the panel a whole anchor-height
1111 further down again. The margin stays: position-try-fallbacks flips
1112 margins along with insets, so one margin-block-start is the 4px gap
1113 in both directions.
1114
1115 The cap is deliberately NOT a percentage of the position-area. A
1116 percentage would be the exact space available, which sounds better
1117 and is worse: an element clamped to fit never overflows, and overflow
1118 is precisely what triggers position-try-fallbacks, so the menu would
1119 scroll in place at the foot of the window instead of flipping up into
1120 the room above it.
1121
1122 position-visibility covers the fixed positioning above. A fixed panel
1123 is deliberately outside every scrolling ancestor's clip — that is how
1124 a menu opened inside a card escapes it — but the anchor is NOT, so a
1125 rail's nav scrolled until the menu's own button is gone could leave
1126 the menu painted over unrelated content with nothing under it.
1127 anchors-visible ties the panel's visibility to its anchor's: when the
1128 button goes, the menu goes.
1129
1130 It is a no-op in Chromium today, and that is written down rather than
1131 left to be rediscovered: Chromium's INITIAL value for the property is
1132 already anchors-visible, on every element. The CSS spec's initial
1133 value is `always`. So this declaration is not what produces today's
1134 behaviour — it is what keeps it if an engine ever aligns with the
1135 spec, which would otherwise orphan every menu here with no warning at
1136 all. ui's TestAMenuDoesNotOutliveTheAnchorScrolledAwayFromUnderIt
1137 watches for exactly that, and says the same at more length. */
1138.rst-dropdown__menu,
1139[rst-dropdown-menu],
1140.rst-row-menu__panel,
1141[rst-row-menu-panel] {
1142 max-block-size: min(20rem, max(calc(100dvh - 6rem), 8rem));
1143 overflow-y: auto;
1144 overscroll-behavior: contain;
1145}
1146@supports (position-area: block-end) and (position-try-fallbacks: flip-block) {
1147 .rst-dropdown,
1148 [rst-dropdown],
1149 .rst-row-menu,
1150 [rst-row-menu] {
1151 anchor-name: --rst-menu;
1152 anchor-scope: --rst-menu;
1153 }
1154 .rst-dropdown__menu,
1155 [rst-dropdown-menu],
1156 .rst-row-menu__panel,
1157 [rst-row-menu-panel] {
1158 inset: auto;
1159 margin-block-start: 4px;
1160 position: fixed;
1161 position-anchor: --rst-menu;
1162 position-area: block-end span-inline-start;
1163 position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
1164 position-visibility: anchors-visible;
1165 }
1166}
1167
1168.rst-caret, [rst-caret] { align-items: center; color: var(--rst-text-faint); display: inline-flex; font-size: 0.8em; transition: transform 0.15s; }
1169.rst-caret svg, [rst-caret] svg, .rst-caret .icon, [rst-caret] .icon { display: block; height: 1em; width: 1em; }
1170details[open] > summary > .rst-caret, details[open] > summary > [rst-caret] { transform: rotate(180deg); }
1171
1172/* ftok — an applied filter as a removable chip; the × is a plain link
1173 to the unfiltered URL, so removing a filter is just navigation.
1174 The × is 24px square because WCAG 2.2 AA 2.5.8 says a target is, and
1175 it was 17px until the accessibility gate measured it. The chip lost
1176 its block padding in the same breath to pay for most of that: the
1177 chip measures 26.0px tall now against 22.4px before, and would have
1178 been 29.2px if the padding had stayed. */
1179.rst-ftok, [rst-ftok] { align-items: center; background: var(--rst-surface); border: 1px solid var(--rst-line-strong); border-radius: 7px; display: inline-flex; font-size: var(--rst-fs-xs); gap: 0.45rem; padding-block: 0; padding-inline: 0.6rem 0.2rem; }
1180.rst-ftok .rst-ftok__k, [rst-ftok] [rst-ftok-k] { color: var(--rst-text-muted); }
1181.rst-ftok a, [rst-ftok] a { align-items: center; border-radius: 4px; color: var(--rst-text-faint); display: inline-flex; height: 24px; justify-content: center; text-decoration: none; width: 24px; }
1182.rst-ftok a:hover, [rst-ftok] a:hover { background: var(--rst-tone-negative-bg); color: var(--rst-tone-negative-fg); }
1183
1184/* Reduced motion: the caret's rotate transition is the only new
1185 animation this batch adds. Everything else here (hover fills, colour
1186 changes) is not a "large motion" trigger under 2.3.3, so it does not
1187 need a rule here too. */
1188@media (prefers-reduced-motion: reduce) {
1189 .rst-caret, [rst-caret] { transition: none; }
1190}
1191
1192/* ── The form family: field, switch, choice cards, seg-tabs, layout ── */
1193
1194/* fields — bare on the page, one column; a card in a form is reserved
1195 for collections, toggle blocks, and choice cards. */
1196.rst-field, [rst-field] { margin: var(--rst-sp-4) 0; }
1197.rst-field__label, [rst-field-label] { color: var(--rst-text-muted); display: block; font-size: var(--rst-fs-sm); margin: 0 0 0.2rem; }
1198.rst-field__hint, [rst-field-hint] { color: var(--rst-text-faint); font-weight: 400; }
1199/* box-sizing is set here rather than in a `*` reset, because this file
1200 promises exactly one collision surface with an app's own CSS: the
1201 rst- prefix. Without it width:100% is the CONTENT box, so an input
1202 overflowed its own wrapper by its padding and borders — 21px for a
1203 plain field, 47px once .rst-dtp__input reserves room for the picker
1204 button. That overflow was three of this batch's visible bugs at once:
1205 an input running under the field beside it in a row, an input running
1206 out of its card, and a picker button anchored to the wrapper landing
1207 47px short of the input's visible end. */
1208.rst-input, [rst-input] { background: var(--rst-surface); border: 1px solid var(--rst-line-strong); border-radius: var(--rst-radius-sm); box-sizing: border-box; color: inherit; font: inherit; padding: 0.45rem 0.6rem; width: 100%; }
1209.rst-input:focus-visible, [rst-input]:focus-visible { outline: 2px solid var(--rst-accent); outline-offset: 2px; }
1210/* short is compact, not tiny: 8rem is a border-box width, so it renders
1211 within a pixel or two of the old 6.5rem content box plus its padding,
1212 and it is wide enough for the postcodes and codes it is for. The
1213 max keeps it from overflowing a column narrower than itself. */
1214.rst-input--short, [rst-input~="short"] { inline-size: 8rem; max-inline-size: 100%; }
1215/* primary is the form's headline field — the title or name the record is
1216 known by — and the size is the whole signal, because the label above it
1217 stays the same quiet line every other field gets. font-size has to be
1218 restated rather than inherited: .rst-input sets `font: inherit`, so the
1219 shorthand would otherwise reset it back to the body's 14px. 550 is the
1220 weight .rst-nm and .rst-person__name already give a name; 600 reads as a
1221 heading, which this is not. The padding grows with the type so the box
1222 keeps its proportions instead of looking like a squeezed plain input.
1223 17px is also where iOS Safari stops zooming a focused input, which the
1224 14px base does not clear — a real bonus, not the reason. */
1225.rst-input--primary, [rst-input~="primary"] { font-size: var(--rst-fs-lg); font-weight: 550; padding: 0.6rem 0.7rem; }
1226textarea.rst-input, textarea[rst-input] { min-height: 5rem; resize: vertical; }
1227/* field-text/field-textarea (the interim pair the manifest generator
1228 emits) mark a required label with * and put the textarea on its own
1229 class; styled here so both field families render finished. */
1230.rst-field__required, [rst-field-required] { color: var(--rst-tone-negative-fg); }
1231.rst-textarea, [rst-textarea] { background: var(--rst-surface); border: 1px solid var(--rst-line-strong); border-radius: var(--rst-radius-sm); box-sizing: border-box; color: inherit; font: inherit; line-height: 1.6; min-height: 5rem; padding: 0.45rem 0.6rem; resize: vertical; width: 100%; }
1232.rst-textarea:focus-visible, [rst-textarea]:focus-visible { outline: 2px solid var(--rst-accent); outline-offset: 2px; }
1233/* The three message lines under a control. display:block is load-bearing
1234 rather than cosmetic: the field partial emits them as <p>, and the
1235 field-text/date/time/select family as <small>, and an inline <small>
1236 silently drops the top margin — so the same message sat at two
1237 different distances from its control depending on which partial drew
1238 it. The hint's class is shared with the inline parenthetical inside a
1239 label in the field partial, so only the <small> form is blocked out. */
1240.rst-field__help, [rst-field-help] { color: var(--rst-text-muted); display: block; font-size: var(--rst-fs-xs); margin: 0.45rem 0 0; }
1241.rst-field__error, [rst-field-error] { color: var(--rst-tone-negative-fg); display: block; font-size: var(--rst-fs-xs); margin: 0.45rem 0 0; }
1242small.rst-field__hint, small[rst-field-hint] { display: block; font-size: var(--rst-fs-xs); margin: 0.45rem 0 0; }
1243
1244/* switch — the one toggle mechanism: real checkbox, visible track. The
1245 knob's left: 2px is deliberately physical, not inset-inline-start: it
1246 is the origin of a physical translateX, so the pair has to travel the
1247 same axis in either writing mode or the knob leaves the track. What
1248 that gives up: the knob therefore always travels left-to-right, so a
1249 mirrored switch would need a direction-scoped rule of its own. */
1250.rst-switch, [rst-switch] { align-items: center; cursor: pointer; display: inline-flex; gap: 0.6rem; position: relative; }
1251.rst-switch input, [rst-switch] input { height: 1px; opacity: 0; position: absolute; width: 1px; }
1252.rst-switch__track, [rst-switch-track] { background: var(--rst-line-strong); border-radius: 10px; flex: none; height: 19px; position: relative; transition: background 0.15s; width: 34px; }
1253.rst-switch__track::after, [rst-switch-track]::after { background: var(--rst-surface); border-radius: 50%; box-shadow: var(--rst-shadow-knob); content: ""; height: 15px; left: 2px; position: absolute; top: 2px; transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1); width: 15px; }
1254.rst-switch:has(input:checked) .rst-switch__track, [rst-switch]:has(input:checked) [rst-switch-track] { background: var(--rst-accent); }
1255.rst-switch:has(input:checked) .rst-switch__track::after, [rst-switch]:has(input:checked) [rst-switch-track]::after { transform: translateX(15px); }
1256.rst-switch:has(input:focus-visible) .rst-switch__track, [rst-switch]:has(input:focus-visible) [rst-switch-track] { outline: 2px solid var(--rst-accent); outline-offset: 2px; }
1257.rst-switch:has(input:disabled), [rst-switch]:has(input:disabled) { cursor: default; opacity: 0.55; }
1258
1259/* choice cards — whole-card target, native input visible. */
1260.rst-choice, [rst-choice] { border: 0; margin: var(--rst-sp-4) 0; padding: 0; }
1261.rst-choice > legend, [rst-choice] > legend { font-size: var(--rst-fs-sm); font-weight: 600; margin: 0 0 0.45rem; padding: 0; }
1262.rst-choice__cards, [rst-choice-cards] { display: grid; gap: 0.5rem; }
1263.rst-choice__cards label, [rst-choice-cards] label { align-items: flex-start; border: 1px solid var(--rst-line); border-radius: var(--rst-radius); cursor: pointer; display: flex; gap: 0.7rem; margin: 0; padding: 0.75rem 0.95rem; }
1264.rst-choice__cards label:hover, [rst-choice-cards] label:hover { border-color: var(--rst-accent); }
1265.rst-choice__cards input, [rst-choice-cards] input { accent-color: var(--rst-accent); flex: none; margin-top: 0.18rem; }
1266.rst-choice__title, [rst-choice-title] { display: block; font-size: var(--rst-fs-sm); font-weight: 550; line-height: 1.35; }
1267.rst-choice__desc, [rst-choice-desc] { color: var(--rst-text-muted); display: block; font-size: var(--rst-fs-xs); line-height: 1.4; }
1268.rst-choice__cards label:has(input:checked), [rst-choice-cards] label:has(input:checked) { background: var(--rst-accent-soft); border-color: var(--rst-accent); }
1269.rst-choice__cards label:has(input:focus-visible), [rst-choice-cards] label:has(input:focus-visible) { outline: 2px solid var(--rst-accent); outline-offset: 2px; }
1270
1271/* seg-tabs — filled pill inside a soft track; current is aria-current.
1272 It wraps: width: max-content is what a segmented control wants and
1273 max-inline-size: 100% is what WCAG 2.2 AA 1.4.10 requires, because
1274 four tabs are 345px wide and a reflow viewport is 320. Two rows beats
1275 a sideways scrollbar on the whole page. */
1276.rst-seg-tabs, [rst-seg-tabs] { background: var(--rst-accent-soft); border-radius: 8px; display: flex; flex-wrap: wrap; gap: 2px; max-inline-size: 100%; padding: 2px; width: max-content; }
1277.rst-seg-tabs a, [rst-seg-tabs] a { border-radius: var(--rst-radius-sm); color: var(--rst-text-muted); font-size: var(--rst-fs-sm); font-weight: 550; padding: 5px 16px; text-decoration: none; }
1278.rst-seg-tabs a:hover, [rst-seg-tabs] a:hover { color: var(--rst-text); }
1279.rst-seg-tabs a[aria-current], [rst-seg-tabs] a[aria-current] { background: var(--rst-surface); box-shadow: var(--rst-shadow-lift); color: var(--rst-text); font-weight: 600; }
1280
1281/* form layout — the interview rhythm and the save bar. */
1282.rst-form-flow > .rst-field + .rst-field, [rst-form-flow] > [rst-field] + [rst-field] { margin-top: var(--rst-sp-5); }
1283/* field-row aligns by the CONTROL row, not by the bottom of the box.
1284 align-items:end was the bug: a field carrying an error is taller than
1285 its sibling, so bottom-aligning it lifted its input clear of the row
1286 and dropped its error line across the field beside it.
1287
1288 Judged against grid-with-subgrid, which does align label/control/
1289 message tracks across siblings exactly: subgrid needs the row to be a
1290 grid, and a grid cannot express what this row is for. grid-auto-flow:
1291 column never wraps, so a narrow screen overflows instead of stacking;
1292 and grid-auto-columns is one value for every column, so rst-grow — a
1293 wide field beside a short one — cannot be said at all. Flex keeps
1294 both. What align-items:start gives up is a row whose fields have
1295 label boxes of different heights; the ::before below buys back the
1296 common case of that (a field with no label at all), and a label long
1297 enough to wrap is a field that wants its own row.
1298
1299 The message then flows under its own column and shifts no sibling,
1300 which is the second half of the fix — it needs the border-box on
1301 .rst-input above to hold, or the control overflows the column the
1302 message is measured against. */
1303.rst-field-row, [rst-field-row] { align-items: start; display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 0.75rem 0; }
1304.rst-field-row .rst-field, [rst-field-row] [rst-field] { margin: 0; }
1305.rst-field-row > .rst-field, [rst-field-row] > [rst-field] { flex: 0 1 auto; min-inline-size: 8rem; }
1306.rst-field-row > .rst-grow, [rst-field-row] > .rst-grow { flex: 1 1 12rem; }
1307/* A field in a row with no label reserves the label's line anyway, so
1308 its control still lines up with a labelled sibling's. It measures
1309 that line rather than restating it: the same font-size .rst-field__label
1310 declares, one lh of the line-height both of them inherit, and the
1311 label's own bottom margin.
1312
1313 The two block-size declarations are one value with a fallback, not a
1314 duplicate: an engine that has :has() but not the lh unit drops the
1315 second line and keeps the calc. That window is real — :has() shipped
1316 in Chrome 105 and Safari 15.4, lh in Chrome 109 and Safari 16.4 — so
1317 Chrome 105-108 and Safari 15.4-16.3 render this rule and would
1318 otherwise reserve a zero-height line. They get the body's 1.5
1319 line-height as a number; everything newer measures the line itself
1320 and the copy stops mattering. Firefox never sees the calc: lh landed
1321 there in 120 and :has() in 121, so any Firefox that matches this
1322 selector already has the unit. */
1323.rst-field-row > .rst-field:not(:has(> .rst-field__label))::before, [rst-field-row] > [rst-field]:not(:has(> [rst-field-label]))::before { block-size: calc(var(--rst-fs-sm) * 1.5); block-size: 1lh; content: ""; display: block; font-size: var(--rst-fs-sm); margin-block-end: 0.2rem; }
1324/* A row is sized by its controls, never by its messages: contain keeps a
1325 long error out of the field's max-content width, so the sentence wraps
1326 under its own control instead of stretching the column it belongs to
1327 and squeezing the field beside it. */
1328.rst-field-row > .rst-field > :is(.rst-field__help, .rst-field__error, .rst-field__hint), [rst-field-row] > [rst-field] > :is([rst-field-help], [rst-field-error], [rst-field-hint]) { contain: inline-size; }
1329/* field-daterange — the fieldset reset the grouped start/end pair needs,
1330 with the legend sized like a field label. Its own selector rather than
1331 a share of .rst-choice: the two groups look alike today, and a card
1332 tweak there must not silently reshape a date range. */
1333.rst-field-range, [rst-field-range] { border: 0; margin: var(--rst-sp-4) 0; padding: 0; }
1334.rst-field-range > legend, [rst-field-range] > legend { font-size: var(--rst-fs-sm); font-weight: 600; margin: 0 0 0.45rem; padding: 0; }
1335.rst-field-range > .rst-field-row, [rst-field-range] > [rst-field-row] { margin: 0; }
1336.rst-form-bar, [rst-form-bar] { align-items: center; background: var(--rst-bg); border-top: 1px solid var(--rst-line); bottom: 0; display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1.75rem; padding: 0.8rem 0; position: sticky; z-index: 5; }
1337.rst-form-bar .rst-form-bar__note, [rst-form-bar] [rst-form-bar-note] { color: var(--rst-text-muted); font-size: var(--rst-fs-xs); margin-inline-end: auto; }
1338.rst-form-actions, [rst-form-actions] { display: flex; gap: 0.5rem; justify-content: flex-end; }
1339/* form/form-foot — the container the form-foot partial's callers wrap
1340 fields in, and the closing action row form-foot itself emits. */
1341/* One spacing mechanism, not two. The form is a flex column, so its
1342 children's own stacking margins never collapse — they were added to
1343 the gap, and they were also added at the two ends, where nothing is
1344 being separated from anything. Inside a card holding a single field
1345 that was the whole of the dead space under the control: 16px of
1346 field margin on top of the card's own padding, at both ends. The gap
1347 is now the rhythm (the same var(--rst-sp-5) .rst-form-flow uses
1348 between its fields) and the blocks contribute no margin of their
1349 own.
1350
1351 Stated as a rule over every child rather than as a list of the
1352 classes we happen to ship: a list is a promise this file cannot keep,
1353 because it cannot name .rst-callout's successor, and it cannot name
1354 an app's own div at all — and a child the list forgot lands at gap
1355 plus its own two margins, which is the bug this rule exists to
1356 prevent. So: the form owns the block rhythm, full stop, and the two
1357 deliberate exceptions are exempted here where they can be read.
1358 .rst-form-foot and .rst-form-bar both set a block-start margin that
1359 is not rhythm — it is the extra air that separates a closing action
1360 row from the last question above it. */
1361.rst-form, [rst-form] { display: flex; flex-direction: column; gap: var(--rst-sp-5); max-inline-size: 44rem; }
1362.rst-form > *:not(.rst-form-foot, .rst-form-bar), [rst-form] > *:not([rst-form-foot], [rst-form-bar]) { margin-block: 0; }
1363/* The closing action row the form-foot partial emits. It was
1364 rst-form__foot until the markup flip (spec §6-v3): BEM's __ flattens
1365 to a hyphen, so it and the sticky save bar above wanted the same
1366 attribute, and a name the partial emits is the one that should own
1367 the name the partial is called. The save bar, which no partial emits
1368 and every caller writes by hand, is rst-form-bar now. */
1369.rst-form-foot, [rst-form-foot] { display: flex; gap: var(--rst-sp-3); margin-block-start: var(--rst-sp-3); }
1370
1371@media (prefers-reduced-motion: reduce) {
1372 .rst-switch__track, [rst-switch-track], .rst-switch__track::after, [rst-switch-track]::after { transition: none; }
1373}
1374
1375/* ── The routes family: confirm, toggle-block, modal shells, bulk-bar,
1376 help ────────────────────────────────────────────────────────── */
1377
1378/* toggle-block — a bordered card whose head is a switch; the body
1379 reveals via :has(), zero JS. The switch is authoritative: the server
1380 treats off as off, whatever the revealed (still-POSTed) fields say. */
1381.rst-tblock, [rst-tblock] { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: var(--rst-radius); margin: 0.7rem 0; overflow: hidden; }
1382.rst-tblock > .rst-tblock__head, [rst-tblock] > [rst-tblock-head] { align-items: flex-start; cursor: pointer; display: flex; gap: 0.85rem; margin: 0; padding: 0.9rem 1.05rem; }
1383.rst-tblock > .rst-tblock__head:hover, [rst-tblock] > [rst-tblock-head]:hover { background: var(--rst-accent-soft); }
1384.rst-tblock .rst-tblock__title, [rst-tblock] [rst-tblock-title] { display: block; font-size: var(--rst-fs-sm); font-weight: 550; }
1385.rst-tblock .rst-tblock__desc, [rst-tblock] [rst-tblock-desc] { color: var(--rst-text-muted); display: block; font-size: var(--rst-fs-xs); }
1386.rst-tblock .rst-tblock__head input, [rst-tblock] [rst-tblock-head] input { height: 1px; opacity: 0; position: absolute; width: 1px; }
1387.rst-tblock:has(.rst-tblock__head input:checked) .rst-switch__track, [rst-tblock]:has([rst-tblock-head] input:checked) [rst-switch-track] { background: var(--rst-accent); }
1388.rst-tblock:has(.rst-tblock__head input:checked) .rst-switch__track::after, [rst-tblock]:has([rst-tblock-head] input:checked) [rst-switch-track]::after { transform: translateX(15px); }
1389.rst-tblock:has(.rst-tblock__head input:focus-visible) .rst-switch__track, [rst-tblock]:has([rst-tblock-head] input:focus-visible) [rst-switch-track] { outline: 2px solid var(--rst-accent); outline-offset: 2px; }
1390.rst-tblock > .rst-tblock__body, [rst-tblock] > [rst-tblock-body] { display: none; }
1391.rst-tblock:has(.rst-tblock__head input:checked) > .rst-tblock__body, [rst-tblock]:has([rst-tblock-head] input:checked) > [rst-tblock-body] { background: var(--rst-surface-2); border-radius: 0 0 var(--rst-radius) var(--rst-radius); border-top: 1px solid var(--rst-line); display: block; padding: 0.9rem 1.05rem 1.05rem; }
1392
1393/* modal route — its own URL: the response renders the page you'll
1394 return to inside an inert backdrop, then the overlay. Closing is a
1395 plain link to that page. */
1396.rst-backdrop, [rst-backdrop] { display: contents; }
1397/* The scroll lock. It used to shift the whole page sideways as it
1398 landed, because taking the scrollbar away takes its width away too;
1399 --rst-scrollbar-gutter at the top of this file reserves that width
1400 whether or not a scrollbar is in it, so the page now stays put. */
1401body:has(.rst-backdrop), body:has([rst-backdrop]) { overflow: hidden; }
1402.rst-modal-overlay, [rst-modal-overlay] { align-items: center; background: var(--rst-overlay); display: flex; inset: 0; justify-content: center; padding: 2rem 1rem; position: fixed; z-index: 10; }
1403.rst-modal-panel, [rst-modal-panel] { background: var(--rst-bg); border: 1px solid var(--rst-line); border-radius: 14px; box-shadow: var(--rst-shadow-pop); display: flex; max-height: min(85vh, 640px); max-width: 860px; overflow: hidden; width: 100%; }
1404/* The panel is a <dialog open>: the rendered-open, NON-modal dialog is
1405 exactly what a modal-as-a-URL is — the server sends the page with the
1406 dialog already open, and nothing calls showModal(). So ::backdrop is
1407 never painted (it only exists for the top layer) and .rst-modal-overlay
1408 stays the scrim, as it was. What the element does bring is a UA block
1409 this idiom has to undo: absolute positioning with auto inline insets,
1410 auto margins, 1em of padding and the Canvas colour pair. Reset them
1411 and the panel lays out as the ordinary flex item it already was. */
1412dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inherit; inset: auto; margin: 0; padding: 0; position: static; }
1413.rst-modal-panel > nav, [rst-modal-panel] > nav { border-inline-end: 1px solid var(--rst-line); display: flex; flex: none; flex-direction: column; gap: 0.15rem; padding: 1.1rem 0.8rem; width: 180px; }
1414.rst-modal-panel > nav a, [rst-modal-panel] > nav a { border-radius: 8px; color: inherit; padding: 0.4rem 0.65rem; text-decoration: none; }
1415.rst-modal-panel > nav a:hover, [rst-modal-panel] > nav a:hover { background: var(--rst-accent-soft); }
1416.rst-modal-panel > nav a[aria-current], [rst-modal-panel] > nav a[aria-current] { background: var(--rst-accent-soft); color: var(--rst-accent); font-weight: 600; }
1417.rst-modal-panel > section, [rst-modal-panel] > section { flex: 1; min-width: 0; overflow-y: auto; padding: 1.5rem 2rem 2rem; position: relative; }
1418.rst-modal-close, [rst-modal-close] { color: var(--rst-text-muted); font-size: 1.5rem; inset-inline-end: 1.1rem; line-height: 1; position: absolute; text-decoration: none; top: 1rem; }
1419.rst-modal-close:hover, [rst-modal-close]:hover { color: inherit; }
1420@media (max-width: 800px) {
1421 .rst-modal-overlay, [rst-modal-overlay] { padding: 0.75rem; }
1422 .rst-modal-panel, [rst-modal-panel] { flex-direction: column; max-height: 92vh; }
1423 .rst-modal-panel > nav, [rst-modal-panel] > nav { border-bottom: 1px solid var(--rst-line); border-inline-end: 0; flex-direction: row; width: auto; }
1424}
1425
1426/* help — a bordered ? icon-link to a section's help article: plain <a>,
1427 new tab, CSS tooltip. The tooltip is NOT the accessible name — the
1428 link must carry a full aria-label of its own. */
1429.rst-help, [rst-help] { align-items: center; border: 1px solid var(--rst-line-strong); border-radius: 7px; color: var(--rst-text-muted); display: inline-flex; height: 28px; justify-content: center; position: relative; width: 28px; }
1430.rst-help:hover, [rst-help]:hover { border-color: var(--rst-text-faint); color: var(--rst-text); }
1431.rst-help svg, [rst-help] svg, .rst-help .icon, [rst-help] .icon { height: 15px; width: 15px; }
1432.rst-tip::after, [rst-tip]::after { background: var(--rst-text); border-radius: var(--rst-radius-sm); color: var(--rst-bg); content: attr(data-tip); font-size: 0.71875rem; font-weight: 500; inset-inline-end: 0; opacity: 0; padding: 3px 8px; pointer-events: none; position: absolute; top: calc(100% + 6px); transition: opacity 0.12s; white-space: nowrap; z-index: 35; }
1433.rst-tip:hover::after, [rst-tip]:hover::after, .rst-tip:focus-visible::after, [rst-tip]:focus-visible::after { opacity: 1; }
1434
1435/* bulk-bar + selbox — select mode as server-rendered state. */
1436.rst-bulkbar, [rst-bulkbar] { align-items: center; background: var(--rst-accent-soft); border-bottom: 1px solid var(--rst-line); display: flex; gap: 0.7rem; min-height: 46px; padding-block: 0; padding-inline: 0.6rem 0.85rem; }
1437.rst-bulkbar__close, [rst-bulkbar-close] { align-items: center; border-radius: var(--rst-radius-sm); color: var(--rst-text-muted); display: inline-flex; height: 26px; justify-content: center; text-decoration: none; width: 26px; }
1438.rst-bulkbar__close:hover, [rst-bulkbar-close]:hover { background: var(--rst-accent-soft); color: var(--rst-text); }
1439.rst-bulkbar__close svg, [rst-bulkbar-close] svg, .rst-bulkbar__close .icon, [rst-bulkbar-close] .icon { height: 15px; width: 15px; }
1440.rst-bulkbar__count, [rst-bulkbar-count] { font-size: var(--rst-fs-sm); font-weight: 600; }
1441.rst-bulkbar__escalate, [rst-bulkbar-escalate] { color: var(--rst-accent); font-size: var(--rst-fs-xs); font-weight: 550; text-decoration: none; }
1442.rst-bulkbar__escalate:hover, [rst-bulkbar-escalate]:hover { text-decoration: underline; }
1443.rst-bulkbar .rst-dropdown, [rst-bulkbar] [rst-dropdown] { margin-inline-start: auto; }
1444.rst-bulkbar .rst-dropdown > summary, [rst-bulkbar] [rst-dropdown] > summary { background: var(--rst-surface); border: 1px solid var(--rst-line-strong); border-radius: 7px; font-size: var(--rst-fs-xs); font-weight: 550; padding: 0.22rem 0.6rem; }
1445.rst-selbox, [rst-selbox] { align-items: center; display: flex; margin: 0; }
1446.rst-selbox input, [rst-selbox] input { accent-color: var(--rst-accent); cursor: pointer; height: 16px; margin: 0; width: 16px; }
1447.rst-selbox input:focus-visible, [rst-selbox] input:focus-visible { outline: 2px solid var(--rst-accent); outline-offset: 2px; }
1448
1449.rst-back-nav, [rst-back-nav] { margin: 0 0 var(--rst-sp-3); }
1450.rst-back-nav a, [rst-back-nav] a { color: var(--rst-text-muted); text-decoration: none; }
1451.rst-back-nav a:hover, [rst-back-nav] a:hover { color: var(--rst-accent); }
1452.rst-notice, [rst-notice] { background: var(--rst-tone-positive-bg); border-radius: var(--rst-radius-sm); color: var(--rst-tone-positive-fg); font-size: var(--rst-fs-sm); padding: 0.5rem 0.75rem; }
1453.rst-form-error, [rst-form-error] { color: var(--rst-tone-negative-fg); font-size: var(--rst-fs-sm); }
1454
1455@media (prefers-reduced-motion: reduce) {
1456 .rst-tip::after, [rst-tip]::after { transition: none; }
1457}
1458
1459/* rst-spin — the working indicator a status page or busy button wears.
1460 Pure CSS; reduced-motion users get a steady dimmed ring instead of
1461 rotation. */
1462.rst-spin, [rst-spin] {
1463 display: inline-block;
1464 width: 1em;
1465 height: 1em;
1466 border: 2px solid currentColor;
1467 /* Physical on purpose: the gap in a ring that rotates a full turn
1468 every 0.8s has no start or end side to mirror. */
1469 border-right-color: transparent;
1470 border-radius: 50%;
1471 vertical-align: -0.15em;
1472 animation: rst-spin 0.8s linear infinite;
1473}
1474@keyframes rst-spin { to { transform: rotate(1turn); } }
1475
1476/* A running job's determinate bar: a native <progress>, styled the same
1477 way and for the same reasons as [rst-meter-bar] — see that block for
1478 why the three engines need three rules that cannot be merged, and for
1479 the same honest flag about the -moz- one being unverified here.
1480 <progress>'s pseudo-elements are its own: ::-webkit-progress-bar is
1481 the track, ::-webkit-progress-value the fill, ::-moz-progress-bar the
1482 fill. It is a block on its own line under the sentence, because it
1483 measures the job the sentence is about. */
1484.rst-job__bar, [rst-job-bar] { appearance: none; background: var(--rst-accent-soft); block-size: 4px; border: 0; border-radius: 2px; display: block; inline-size: 100%; margin-block-start: var(--rst-sp-2); }
1485.rst-job__bar::-webkit-progress-bar, [rst-job-bar]::-webkit-progress-bar { background: var(--rst-accent-soft); border-radius: 2px; }
1486.rst-job__bar::-webkit-progress-value, [rst-job-bar]::-webkit-progress-value { background: var(--rst-accent); border-radius: 2px; }
1487.rst-job__bar::-moz-progress-bar, [rst-job-bar]::-moz-progress-bar { background: var(--rst-accent); border-radius: 2px; }
1488@media (prefers-reduced-motion: reduce) {
1489 .rst-spin, [rst-spin] { animation: none; opacity: 0.5; }
1490}
1491
1492/* field-select's combobox, built by rastrillo.js past ten options. The
1493 popup deliberately borrows .rst-dropdown__menu's shape so the two
1494 overlays read as one system. Nothing here styles the native <select>:
1495 it keeps .rst-input and is simply moved out of sight by .rst-sr-only. */
1496.rst-combo, [rst-combo] { position: relative; }
1497.rst-combo__list, [rst-combo-list] { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: 9px; box-shadow: var(--rst-shadow-pop); inset-inline: 0; list-style: none; margin-top: 4px; max-block-size: 15rem; overflow-y: auto; padding: 0.25rem; position: absolute; top: 100%; z-index: 30; }
1498.rst-combo__option, [rst-combo-option] { border-radius: var(--rst-radius-sm); color: inherit; cursor: pointer; font-size: var(--rst-fs-sm); padding: 0.4rem 0.65rem; }
1499.rst-combo__option:hover, [rst-combo-option]:hover, .rst-combo__option.is-active, [rst-combo-option].is-active { background: var(--rst-accent-soft); }
1500/* The committed option is marked in the accessibility tree by
1501 aria-selected; this is the matching visual, never the only signal. */
1502.rst-combo__option[aria-selected="true"], [rst-combo-option][aria-selected="true"] { font-weight: 600; }
1503/* A grouped <select> keeps its groups: each <optgroup> becomes an ARIA
1504 group holding its own list of rows, under a heading that is furniture
1505 only — the group's aria-label is what a screen reader announces.
1506 Same faint uppercase recipe as .rst-lrow--head, for the same reason:
1507 it names a run of rows without competing with them. */
1508.rst-combo__rows, [rst-combo-rows] { list-style: none; margin: 0; padding: 0; }
1509.rst-select__group, [rst-select-group] { color: var(--rst-text-faint); font-size: var(--rst-fs-xs); font-weight: 550; letter-spacing: 0.05em; padding-block: 0.4rem 0.2rem; padding-inline: 0.65rem; text-transform: uppercase; }
1510
1511/* The date fields' combobox, built by datetime.js on any input carrying
1512 data-rst-date or data-rst-time. Same shape as .rst-combo above, plus
1513 a picker button anchored in the field and two kinds of row: the
1514 reading of what was typed (with its Set affordance) and the quick
1515 picks under it. The native input is not styled here either — it keeps
1516 its value and its name and is simply moved out of sight by
1517 .rst-sr-only, because it is what the form submits. */
1518.rst-dtp, [rst-dtp] { position: relative; }
1519.rst-dtp__input, [rst-dtp-input] { padding-inline-end: 2.25rem; }
1520/* The button is anchored to .rst-dtp, which is exactly as wide and as
1521 tall as the input it wraps once the input is a border box — so
1522 inset-inline-end puts it at the control's inline end in either
1523 writing mode, and inset-block:0 with auto block margins centres it on
1524 the control at whatever height the type scale gives it. The old
1525 inset-block-start:0.3rem was that centring measured once by hand
1526 against one font size. .rst-dtp__input's padding-inline-end is the
1527 room this reserves inside the control, so text never runs under it,
1528 with a value or without one. */
1529.rst-dtp__pick, [rst-dtp-pick] { align-items: center; background: none; border: 0; border-radius: var(--rst-radius-sm); block-size: 1.75rem; color: var(--rst-muted); cursor: pointer; display: flex; inline-size: 1.75rem; inset-block: 0; inset-inline-end: 0.35rem; justify-content: center; margin-block: auto; padding: 0; position: absolute; }
1530.rst-dtp__pick:hover, [rst-dtp-pick]:hover { color: var(--rst-text); }
1531.rst-dtp__pick svg, [rst-dtp-pick] svg { block-size: 1rem; inline-size: 1rem; }
1532.rst-dtp__list, [rst-dtp-list] { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: 9px; box-shadow: var(--rst-shadow-pop); inset-inline: 0; list-style: none; margin-block-start: 4px; max-block-size: 17rem; overflow-y: auto; padding: 0.25rem; position: absolute; top: 100%; z-index: 40; }
1533.rst-dtp__row, [rst-dtp-row] { align-items: baseline; border-radius: var(--rst-radius-sm); color: inherit; cursor: pointer; display: flex; font-size: var(--rst-fs-sm); gap: 0.5rem; justify-content: space-between; padding: 0.4rem 0.65rem; }
1534.rst-dtp__row:hover, [rst-dtp-row]:hover, .rst-dtp__row.is-active, [rst-dtp-row].is-active { background: var(--rst-accent-soft); }
1535.rst-dtp__row--set .rst-dtp__label, [rst-dtp-row~="set"] [rst-dtp-label] { font-weight: 600; }
1536.rst-dtp__quick .rst-dtp__set, [rst-dtp-quick] [rst-dtp-set] { color: var(--rst-muted); }
1537/* The committed value is marked in the accessibility tree by
1538 aria-selected; this is the matching visual, never the only signal. */
1539.rst-dtp__row[aria-selected="true"] .rst-dtp__label, [rst-dtp-row][aria-selected="true"] [rst-dtp-label] { font-weight: 600; }
1540.rst-dtp__set, [rst-dtp-set] { flex: none; font-size: var(--rst-fs-xs); color: var(--rst-muted); }
1541.rst-dtp__hint, [rst-dtp-hint] { color: var(--rst-muted); font-size: var(--rst-fs-xs); padding: 0.35rem 0.65rem 0.15rem; }
1542
1543/* The popup opens above the field where there is no room below it —
1544 datetime.js measures and sets .is-above, because a panel hanging off
1545 the bottom of the window is a panel whose last week nobody can click.
1546 One rule for both popups: only ever one of them is open. */
1547.rst-dtp.is-above .rst-dtp__list, [rst-dtp].is-above [rst-dtp-list], .rst-dtp.is-above .rst-cal, [rst-dtp].is-above [rst-cal] { inset-block-end: 100%; margin-block-end: 4px; margin-block-start: 0; top: auto; }
1548/* A completion offered rather than chosen — the other months a
1549 half-typed word could still have meant. Lighter than the reading
1550 above it, because it is a second-best by construction. */
1551.rst-dtp__row--alt .rst-dtp__label, [rst-dtp-row~="alt"] [rst-dtp-label] { color: var(--rst-text-muted); }
1552
1553/* ── The calendar overlay (ui/calendar.js): the month grid a date
1554 field's button opens. A real <table> with real column headers,
1555 because that is what a calendar is, with role="grid" over the top
1556 and a roving tabindex so exactly one day sits in the tab order.
1557
1558 Sized in rem so the grid keeps its shape when a page is zoomed:
1559 seven columns of text, not seven columns of pixels. A fixed inline
1560 size rather than the field's, because a calendar is a calendar
1561 whatever it hangs under — a month stretched across a wide input is
1562 a grid nobody can aim at.
1563
1564 Everything here is logical (inline/block, never left/right), so a
1565 right-to-left page mirrors the panel and the grid together. The
1566 chevrons are the one thing that needs saying twice, below.
1567 ───────────────────────────────────────────────────────────────── */
1568.rst-cal, [rst-cal] { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: 9px; box-shadow: var(--rst-shadow-pop); inline-size: 18rem; inset-inline-start: 0; margin-block-start: 4px; padding: var(--rst-sp-2); position: absolute; top: 100%; z-index: 40; }
1569.rst-cal__head, [rst-cal-head] { align-items: center; display: flex; gap: var(--rst-sp-1); justify-content: space-between; padding-block-end: var(--rst-sp-2); }
1570.rst-cal__title, [rst-cal-title] { flex: 1; font-size: var(--rst-fs-sm); font-weight: 600; text-align: center; }
1571.rst-cal__nav, [rst-cal-nav] { align-items: center; background: none; block-size: 1.75rem; border: 0; border-radius: var(--rst-radius-sm); color: var(--rst-muted); cursor: pointer; display: flex; inline-size: 1.75rem; justify-content: center; padding: 0; }
1572.rst-cal__nav:hover:not(:disabled), [rst-cal-nav]:hover:not(:disabled) { background: var(--rst-accent-soft); color: var(--rst-text); }
1573/* A month with nothing pickable in it — past the field's min, or past
1574 its max. Dimmed and inert rather than removed, so the header keeps
1575 its shape and the title stays where the eye left it. */
1576.rst-cal__nav:disabled, [rst-cal-nav]:disabled { cursor: default; opacity: 0.35; }
1577.rst-cal__nav svg, [rst-cal-nav] svg { block-size: 1.1rem; inline-size: 1.1rem; }
1578/* One chevron, drawn once and turned round for the other direction, so
1579 "back" always points at the start of the line — which is the other
1580 end of the box in a right-to-left page, and handled there by the
1581 panel's own direction rather than a second rule. */
1582[rst-cal-nav~="next"] svg, .rst-cal__nav--next svg { transform: scaleX(-1); }
1583.rst-cal__grid, [rst-cal-grid] { border-collapse: collapse; inline-size: 100%; table-layout: fixed; }
1584.rst-cal__grid th, [rst-cal-grid] th { color: var(--rst-muted); font-size: var(--rst-fs-xs); font-weight: 500; padding-block: var(--rst-sp-1); text-align: center; }
1585.rst-cal__day, [rst-cal-day] { border-radius: var(--rst-radius-sm); cursor: pointer; font-size: var(--rst-fs-sm); font-variant-numeric: tabular-nums; line-height: 2.15; text-align: center; }
1586.rst-cal__day:hover, [rst-cal-day]:hover { background: var(--rst-accent-soft); }
1587/* The days either side of the month. Still pickable — reaching for the
1588 1st from the row above it is how people use a calendar — but plainly
1589 not part of the month on screen. */
1590.rst-cal__day.is-outside, [rst-cal-day].is-outside { color: var(--rst-text-faint); }
1591/* Today is a ring rather than a fill, so it can still be read when the
1592 selection is sitting on top of it. */
1593.rst-cal__day.is-today, [rst-cal-day].is-today { box-shadow: inset 0 0 0 1px var(--rst-line-strong); font-weight: 600; }
1594/* What the text in the box currently reads as: an outline that follows
1595 the words as they are typed. Deliberately quieter than the selection
1596 — it is a preview, and nothing has been chosen yet. */
1597.rst-cal__day.is-preview, [rst-cal-day].is-preview { box-shadow: inset 0 0 0 2px var(--rst-accent); }
1598/* The committed value. aria-selected says so in the accessibility tree;
1599 this is the matching visual, never the only signal. */
1600.rst-cal__day.is-selected, [rst-cal-day].is-selected { background: var(--rst-accent); color: var(--rst-on-accent); font-weight: 600; }
1601.rst-cal__day.is-selected.is-outside, [rst-cal-day].is-selected.is-outside { color: var(--rst-on-accent); }
1602/* Outside the field's own min/max. aria-disabled carries it to a screen
1603 reader and calendar.js refuses the pick; this says the same to the
1604 eye. */
1605.rst-cal__day.is-disabled, [rst-cal-day].is-disabled { color: var(--rst-text-faint); cursor: default; text-decoration: line-through; }
1606.rst-cal__day.is-disabled:hover, [rst-cal-day].is-disabled:hover { background: none; }
1607.rst-cal__day:focus-visible, [rst-cal-day]:focus-visible { outline: 2px solid var(--rst-accent); outline-offset: -2px; }
1608
1609/* ── Shells: the page frames an app's layout template wraps .rst-page
1610 in — topbar and sidebar. Written in logical properties throughout,
1611 so a right-to-left locale gets a mirrored shell for free: the rail
1612 moves to the inline end, the account menu with it, and nothing
1613 here needs a second rule. ─────────────────────────────────────── */
1614
1615/* skip link — first in the DOM of every shell, out of sight until it
1616 takes focus, then a real chip at the block/inline start. A keyboard
1617 user must be able to jump the nav; this is the whole mechanism. */
1618.rst-skip, [rst-skip] { block-size: 1px; clip-path: inset(50%); inline-size: 1px; overflow: hidden; position: absolute; white-space: nowrap; }
1619.rst-skip:focus-visible, [rst-skip]:focus-visible { background: var(--rst-surface); block-size: auto; border: 1px solid var(--rst-accent); border-radius: var(--rst-radius-sm); clip-path: none; color: var(--rst-text); inline-size: auto; inset-block-start: var(--rst-sp-2); inset-inline-start: var(--rst-sp-2); overflow: visible; padding: 0.4rem 0.7rem; text-decoration: none; z-index: 60; }
1620
1621/* topbar shell — brand, nav and an account menu on one bar, the page
1622 column beneath it, a footer under that. The account dropdown is
1623 pushed to the inline end by auto margin, not by order or float. */
1624.rst-shell-topbar .rst-shell__bar, [rst-shell-topbar] [rst-shell-bar] { align-items: center; background: var(--rst-surface); border-block-end: 1px solid var(--rst-line); display: flex; flex-wrap: wrap; gap: var(--rst-sp-4); padding-block: 0.5rem; padding-inline: var(--rst-sp-4); }
1625.rst-shell__brand, [rst-shell-brand] { color: inherit; font-weight: 650; text-decoration: none; }
1626.rst-shell__nav, [rst-shell-nav] { display: flex; gap: var(--rst-sp-4); }
1627.rst-shell__nav a, [rst-shell-nav] a { border-block-end: 2px solid transparent; color: var(--rst-text-muted); font-size: var(--rst-fs-sm); padding-block: 0.3rem; text-decoration: none; }
1628.rst-shell__nav a:hover, [rst-shell-nav] a:hover { color: var(--rst-text); }
1629.rst-shell__nav a[aria-current], [rst-shell-nav] a[aria-current] { border-block-end-color: var(--rst-accent); color: var(--rst-text); font-weight: 550; }
1630.rst-shell__account, [rst-shell-account] { margin-inline-start: auto; }
1631/* an account menu whose block was never overridden has an empty menu; hide the whole control rather than show a dead summary. */
1632.rst-shell__account:has(> .rst-dropdown__menu:empty), [rst-shell-account]:has(> [rst-dropdown-menu]:empty) { display: none; }
1633.rst-shell__foot, [rst-shell-foot] { border-block-start: 1px solid var(--rst-line); color: var(--rst-text-muted); font-size: var(--rst-fs-xs); padding-block: var(--rst-sp-4); padding-inline: var(--rst-sp-4); text-align: center; }
1634/* the topbar shell emits a foot even when the block is un-overridden; an empty one is a stray rule and padding, so draw nothing. */
1635.rst-shell__foot:empty, [rst-shell-foot]:empty { display: none; }
1636
1637/* The topbar's narrow layout. Below 800px — the same breakpoint the
1638 sidebar collapses at, deliberately, because two shells with two
1639 breakpoints is two systems — the bar's tail (nav, account, locale)
1640 goes behind one disclosure carrying the `menu` icon. Above it the
1641 summary is display: none and the tail is display: contents, so nav,
1642 account and locale are direct flex items of the bar again and the
1643 inline-end layout is byte-for-byte the one that shipped before this
1644 existed. No script: a <details>, a media query and a pair of display
1645 rules.
1646
1647 Why the tail is a SIBLING of the disclosure rather than its content,
1648 which is the shape .rst-shell__chrome already uses: a closed
1649 <details> hides its own content, and no CSS reliably un-hides it
1650 across engines, so the wide layout could not be recovered from
1651 inside one. It also keeps the account menu out of the disclosure
1652 entirely, which matters — see the name below.
1653
1654 The name is rst-shell-menu and NOT rst-menus. <details name>
1655 exclusivity is document-wide, not sibling-scoped, so a disclosure
1656 sharing the menus' group would be closed by the very account menu it
1657 reveals: the user clicks Account and the navigation they opened it
1658 from disappears. Same rule this file's rst-menu-group already
1659 documents ("a nested group MUST name a different one"), met here for
1660 the first time in the framework's own shells. */
1661.rst-shell-topbar .rst-shell__menu, [rst-shell-topbar] [rst-shell-menu] { margin-inline-start: auto; }
1662.rst-shell-topbar .rst-shell__menu > summary, [rst-shell-topbar] [rst-shell-menu] > summary { align-items: center; border: 1px solid var(--rst-line); border-radius: var(--rst-radius-sm); cursor: pointer; display: flex; font-size: var(--rst-fs-sm); font-weight: 550; gap: 0.35rem; list-style: none; min-block-size: 24px; padding-block: 0.3rem; padding-inline: 0.55rem; }
1663.rst-shell-topbar .rst-shell__menu > summary::-webkit-details-marker, [rst-shell-topbar] [rst-shell-menu] > summary::-webkit-details-marker { display: none; }
1664.rst-shell-topbar .rst-shell__tail, [rst-shell-topbar] [rst-shell-tail] { display: none; }
1665/* The account's auto margin has to be undone here, and stretch does not
1666 do it: in a column flex box the inline axis is the CROSS axis, and an
1667 item with an auto cross-axis margin is exempt from stretching, so
1668 margin-inline-start: auto survived and shoved Account to the trailing
1669 edge of the collapsed stack — the same trailing-edge shove that made
1670 the wrapped bar look wrong in the first place. The wide query below
1671 puts it back. */
1672.rst-shell-topbar .rst-shell__menu[open] + .rst-shell__tail, [rst-shell-topbar] [rst-shell-menu][open] + [rst-shell-tail] { display: flex; flex-basis: 100%; flex-direction: column; gap: var(--rst-sp-3); }
1673.rst-shell-topbar .rst-shell__tail > .rst-shell__account, [rst-shell-topbar] [rst-shell-tail] > [rst-shell-account] { margin-inline-start: 0; }
1674/* flex-start so a nav link's aria-current underline is as wide as its
1675 own label rather than as wide as the bar, where it reads as a rule
1676 across the menu instead of as a marker on one item. */
1677.rst-shell-topbar .rst-shell__tail > .rst-shell__nav, [rst-shell-topbar] [rst-shell-tail] > [rst-shell-nav] { align-items: flex-start; flex-direction: column; gap: var(--rst-sp-2); }
1678/* The nav links pick up the inline padding the two menu summaries below
1679 them already carry, so the collapsed stack reads as one column of
1680 labels rather than three components that happen to be above each
1681 other. Same reason the rail's links carry 0.55rem. */
1682.rst-shell-topbar .rst-shell__tail > .rst-shell__nav a, [rst-shell-topbar] [rst-shell-tail] > [rst-shell-nav] a { padding-inline: 0.6rem; }
1683@media (min-width: 800px) {
1684 .rst-shell-topbar .rst-shell__menu, [rst-shell-topbar] [rst-shell-menu] { display: none; }
1685 /* Both selectors, and both at least as specific as the narrow rules
1686 they undo: a media query adds no specificity, so the open
1687 disclosure's 0-4-0 rule above would otherwise keep the tail a
1688 column for anyone who opened the menu and then widened the
1689 window. */
1690 .rst-shell-topbar .rst-shell__tail,
1691 [rst-shell-topbar] [rst-shell-tail],
1692 .rst-shell-topbar .rst-shell__menu[open] + .rst-shell__tail,
1693 [rst-shell-topbar] [rst-shell-menu][open] + [rst-shell-tail] { display: contents; }
1694 .rst-shell-topbar .rst-shell__tail > .rst-shell__account, [rst-shell-topbar] [rst-shell-tail] > [rst-shell-account] { margin-inline-start: auto; }
1695 .rst-shell-topbar .rst-shell__tail > .rst-shell__nav, [rst-shell-topbar] [rst-shell-tail] > [rst-shell-nav] { align-items: normal; flex-direction: row; gap: var(--rst-sp-4); }
1696 .rst-shell-topbar .rst-shell__tail > .rst-shell__nav a, [rst-shell-topbar] [rst-shell-tail] > [rst-shell-nav] a { padding-inline: 0; }
1697}
1698
1699/* sidebar shell — a rail beside the page. Below 800px the rail is
1700 hidden behind a <details> chrome strip and, opened, becomes an
1701 full-width column above the content: the open state is the
1702 native element's own, so the shell stays zero-JS like every idiom
1703 above it. The main column carries min-inline-size: 0 so a wide child
1704 (a list grid, a <pre>) scrolls itself instead of stretching the grid
1705 track. */
1706.rst-shell__main, [rst-shell-main] { min-inline-size: 0; }
1707/* box-sizing on the rail, per this file's convention (see .rst-input
1708 above): set on the component that needs it, never in a `*` reset.
1709 The rail is the one box in the stylesheet whose height is the
1710 VIEWPORT'S — block-size: 100dvh in the query below — and it also
1711 carries padding, so as a content box its border box was 100dvh + 32px.
1712 Sticky at inset-block-start: 0, the last 32px of it hung under the
1713 window and clipped whatever sat at its foot; overflow-y: auto could
1714 not help, because the content fitted the content box and no scrollbar
1715 was ever owed. Anything sized against the viewport has to be measured
1716 the way the viewport measures it. */
1717.rst-shell-sidebar > .rst-shell__rail, [rst-shell-sidebar] > [rst-shell-rail] { background: var(--rst-surface); border-block-end: 1px solid var(--rst-line); box-sizing: border-box; display: none; flex-direction: column; gap: var(--rst-sp-3); padding: var(--rst-sp-4); }
1718.rst-shell-sidebar > .rst-shell__chrome > summary, [rst-shell-sidebar] > [rst-shell-chrome] > summary { align-items: center; border-block-end: 1px solid var(--rst-line); cursor: pointer; display: flex; font-size: var(--rst-fs-sm); font-weight: 550; gap: 0.3rem; list-style: none; padding-block: 0.55rem; padding-inline: var(--rst-sp-4); }
1719.rst-shell-sidebar > .rst-shell__chrome > summary::-webkit-details-marker, [rst-shell-sidebar] > [rst-shell-chrome] > summary::-webkit-details-marker { display: none; }
1720.rst-shell-sidebar > .rst-shell__chrome[open] + .rst-shell__rail, [rst-shell-sidebar] > [rst-shell-chrome][open] + [rst-shell-rail] { display: flex; }
1721.rst-shell__group, [rst-shell-group] { color: var(--rst-text-faint); font-size: var(--rst-fs-xs); font-weight: 550; letter-spacing: 0.05em; margin-block: var(--rst-sp-3) 0.2rem; text-transform: uppercase; }
1722.rst-shell-sidebar .rst-shell__nav, [rst-shell-sidebar] [rst-shell-nav] { flex-direction: column; gap: 2px; }
1723.rst-shell-sidebar .rst-shell__nav a, [rst-shell-sidebar] [rst-shell-nav] a { border-block-end: 0; border-radius: var(--rst-radius-sm); display: block; padding-block: 0.35rem; padding-inline: 0.55rem; }
1724.rst-shell-sidebar .rst-shell__nav a:hover, [rst-shell-sidebar] [rst-shell-nav] a:hover { background: var(--rst-accent-soft); color: var(--rst-text); }
1725.rst-shell-sidebar .rst-shell__nav a[aria-current], [rst-shell-sidebar] [rst-shell-nav] a[aria-current] { background: var(--rst-accent-soft); color: var(--rst-accent); font-weight: 600; }
1726/* The rail's foot — the language switcher and the person, in the corner
1727 an app puts them in everywhere else. margin-block-start: auto is the
1728 whole mechanism: the rail is a column flex box, so the auto margin
1729 eats whatever height is left between the nav and the bottom.
1730
1731 Below 800px the disclosed rail is STILL a flex column — the media
1732 query below only adds block-size: 100dvh, it does not change the
1733 display — so the reason the foot stays put down there is not that the
1734 layout changed but that the column is content-height and the auto
1735 margin has no free space to distribute. That is a dependency, so name
1736 it: give the disclosed rail a min-block-size or a height and the foot
1737 floats back down to the bottom of it, silently. The 390px leg of
1738 TestTheSidebarRailPutsThePersonAtItsFootAndTheLanguageMenuOpensUpward
1739 asserts the rail's own height for exactly that reason. */
1740.rst-shell__rail-foot, [rst-shell-rail-foot] { display: flex; flex-direction: column; gap: var(--rst-sp-3); margin-block-start: auto; }
1741/* An un-overridden shell renders the wrapper with nothing in it; draw
1742 no gap for it, the same way .rst-shell__foot handles its own. */
1743.rst-shell__rail-foot:empty, [rst-shell-rail-foot]:empty { display: none; }
1744@media (min-width: 800px) {
1745 .rst-shell-sidebar, [rst-shell-sidebar] { display: grid; grid-template-columns: 15rem 1fr; }
1746 .rst-shell-sidebar > .rst-shell__chrome, [rst-shell-sidebar] > [rst-shell-chrome] { display: none; }
1747 .rst-shell-sidebar > .rst-shell__rail, [rst-shell-sidebar] > [rst-shell-rail] { block-size: 100dvh; border-block-end: 0; border-inline-end: 1px solid var(--rst-line); display: flex; inset-block-start: 0; overflow-y: auto; position: sticky; }
1748 /* A menu at the foot of a full-height rail is a DROPUP: the same
1749 <details> panel, opened block-start-ward instead of block-end-ward,
1750 because below it there is only the window's edge. No script — this
1751 is one pair of inset values. Only inside this query: below 800px
1752 the rail is a content-height column with the whole page under it,
1753 so the same menu opens downward there like every other menu in the
1754 system. */
1755 .rst-shell__rail-foot .rst-dropdown__menu, [rst-shell-rail-foot] [rst-dropdown-menu] { inset-block-end: calc(100% + 4px); inset-block-start: auto; margin-block-start: 0; }
1756 /* Where anchor positioning is available the hand-written inset pair
1757 has to be handed back, and not because it stopped being right: the
1758 insets above are measured against .rst-dropdown's own box, and
1759 position-area replaces that box with the region beside the anchor,
1760 so calc(100% + 4px) would be 100% of the free space under the foot
1761 — the panel launched up the rail. The flip does this case anyway
1762 and does it by measuring: there is no room below the rail's foot,
1763 so flip-block turns the menu upward on its own, and it turns back
1764 down again in a layout where there IS room, which a fixed pair of
1765 insets cannot. Specificity is why this is written here rather than
1766 left to the generic rule: .rst-shell__rail-foot .rst-dropdown__menu
1767 is (0,2,0) and would otherwise outrank it. */
1768 @supports (position-area: block-end) and (position-try-fallbacks: flip-block) {
1769 .rst-shell__rail-foot .rst-dropdown__menu, [rst-shell-rail-foot] [rst-dropdown-menu] { inset: auto; margin-block-start: 4px; }
1770 }
1771}
1772
1773
1774/* console shell — a brand-and-account bar across the top AND a
1775 navigation rail down the side. topbar without the rail is topbar;
1776 sidebar without the bar is sidebar; this is the admin shape both of
1777 them were half of, and the one an app used to hand-write.
1778
1779 TWO CHROMES, ONE CONTROL. Below 800px the bar's tail and the rail
1780 both have to go away, and two disclosures would be two things to
1781 learn. So one <details rst-shell-menu> gates both: its own next
1782 sibling with +, and the rail from the shell root with :has(). One
1783 [open], two reveals, one summary on screen at 390px.
1784
1785 Both reveals are written as HIDE-WHEN-CLOSED rather than
1786 show-when-open. In an engine without :has() the rail's hide rule
1787 never matches and the rail renders as a column of links under the
1788 bar — longer page, navigation intact. Spelled the other way round,
1789 the same missing selector is a phone that cannot navigate. Choose
1790 the failure. */
1791.rst-shell-console .rst-shell__bar, [rst-shell-console] [rst-shell-bar] { align-items: center; background: var(--rst-surface); border-block-end: 1px solid var(--rst-line); display: flex; flex-wrap: wrap; gap: var(--rst-sp-4); padding-block: 0.5rem; padding-inline: var(--rst-sp-4); }
1792.rst-shell-console .rst-shell__menu, [rst-shell-console] [rst-shell-menu] { margin-inline-start: auto; }
1793.rst-shell-console .rst-shell__menu > summary, [rst-shell-console] [rst-shell-menu] > summary { align-items: center; border: 1px solid var(--rst-line); border-radius: var(--rst-radius-sm); cursor: pointer; display: flex; font-size: var(--rst-fs-sm); font-weight: 550; gap: 0.35rem; list-style: none; min-block-size: 24px; padding-block: 0.3rem; padding-inline: 0.55rem; }
1794.rst-shell-console .rst-shell__menu > summary::-webkit-details-marker, [rst-shell-console] [rst-shell-menu] > summary::-webkit-details-marker { display: none; }
1795/* The rail, narrow: a full-width column under the bar, disclosed. It
1796 holds navigation and nothing else — the language switcher and the
1797 person are on the bar — so there is no rail-foot here and nothing
1798 pinned to a bottom edge. box-sizing all the same, because the wide
1799 query below sizes something in this subtree against the viewport and
1800 a viewport promise has to be kept by the border box. */
1801.rst-shell-console > .rst-shell__rail, [rst-shell-console] > [rst-shell-rail] { background: var(--rst-surface); border-block-end: 1px solid var(--rst-line); box-sizing: border-box; display: flex; flex-direction: column; gap: var(--rst-sp-3); padding: var(--rst-sp-4); }
1802.rst-shell-console .rst-shell__nav, [rst-shell-console] [rst-shell-nav] { flex-direction: column; gap: 2px; }
1803.rst-shell-console .rst-shell__nav a, [rst-shell-console] [rst-shell-nav] a { border-block-end: 0; border-radius: var(--rst-radius-sm); display: block; padding-block: 0.35rem; padding-inline: 0.55rem; }
1804.rst-shell-console .rst-shell__nav a:hover, [rst-shell-console] [rst-shell-nav] a:hover { background: var(--rst-accent-soft); color: var(--rst-text); }
1805.rst-shell-console .rst-shell__nav a[aria-current], [rst-shell-console] [rst-shell-nav] a[aria-current] { background: var(--rst-accent-soft); color: var(--rst-accent); font-weight: 600; }
1806/* The two hide rules. Same [open], two combinators. */
1807.rst-shell-console .rst-shell__menu:not([open]) + .rst-shell__tail, [rst-shell-console] [rst-shell-menu]:not([open]) + [rst-shell-tail] { display: none; }
1808.rst-shell-console:has(.rst-shell__menu:not([open])) > .rst-shell__rail, [rst-shell-console]:has([rst-shell-menu]:not([open])) > [rst-shell-rail] { display: none; }
1809/* Disclosed, the tail is a column across the full width of the wrapped
1810 bar. The account's margin-inline-start: auto has to be undone by
1811 hand and stretch does not do it: in a column flex box the inline
1812 axis is the CROSS axis, and an item with an auto cross-axis margin
1813 is exempt from stretching — the same trap the topbar documents. */
1814.rst-shell-console .rst-shell__menu[open] + .rst-shell__tail, [rst-shell-console] [rst-shell-menu][open] + [rst-shell-tail] { display: flex; flex-basis: 100%; flex-direction: column; gap: var(--rst-sp-3); }
1815.rst-shell-console .rst-shell__tail > .rst-shell__account, [rst-shell-console] [rst-shell-tail] > [rst-shell-account] { margin-inline-start: 0; }
1816@media (min-width: 800px) {
1817 /* The frame: bar across the top, rail beside the page, footer under
1818 the page and not under the rail. Named areas rather than DOM
1819 order, because the DOM order is the reading order and must not be
1820 spent on layout — and areas map to the block and inline axes, so
1821 an RTL locale gets the rail on the other side with no second rule.
1822 min-block-size, never block-size: the rail reaches the foot of a
1823 short page, and a long one still scrolls the document. */
1824 .rst-shell-console, [rst-shell-console] { display: grid; grid-template-areas: "bar bar" "rail main" "rail foot"; grid-template-columns: 15rem 1fr; grid-template-rows: auto 1fr auto; min-block-size: 100dvh; }
1825 .rst-shell-console > .rst-shell__bar, [rst-shell-console] > [rst-shell-bar] { grid-area: bar; }
1826 .rst-shell-console > .rst-shell__main, [rst-shell-console] > [rst-shell-main] { grid-area: main; }
1827 .rst-shell-console > .rst-shell__foot, [rst-shell-console] > [rst-shell-foot] { grid-area: foot; }
1828 .rst-shell-console .rst-shell__menu, [rst-shell-console] [rst-shell-menu] { display: none; }
1829 /* Every undo below repeats the narrow rule's own selector, because a
1830 media query adds no specificity: without the :has() spelling here
1831 a reader who opened the menu and then widened the window would
1832 keep a hidden rail. */
1833 .rst-shell-console .rst-shell__tail,
1834 [rst-shell-console] [rst-shell-tail],
1835 .rst-shell-console .rst-shell__menu:not([open]) + .rst-shell__tail,
1836 [rst-shell-console] [rst-shell-menu]:not([open]) + [rst-shell-tail],
1837 .rst-shell-console .rst-shell__menu[open] + .rst-shell__tail,
1838 [rst-shell-console] [rst-shell-menu][open] + [rst-shell-tail] { display: contents; }
1839 .rst-shell-console .rst-shell__tail > .rst-shell__account, [rst-shell-console] [rst-shell-tail] > [rst-shell-account] { margin-inline-start: auto; }
1840 .rst-shell-console > .rst-shell__rail, [rst-shell-console] > [rst-shell-rail] { border-block-end: 0; border-inline-end: 1px solid var(--rst-line); display: flex; grid-area: rail; }
1841 /* The undo is a rule of ITS OWN, and this is the whole reason: a
1842 selector list is invalid as a whole if any selector in it is
1843 invalid, so an engine that cannot parse :has() drops every
1844 declaration in the list — including grid-area: rail, which has
1845 nothing to do with :has() and which this shell's wide layout is
1846 built on. Co-listed, the four selectors above cost the legacy
1847 engine its declared frame and left the rail in the right cell only
1848 because grid auto-placement happened to put it there. Split, the
1849 plain pair always applies and only the fight over `display` — the
1850 one declaration the narrow hide rule contests — rides on :has().
1851 That is the failure direction this shell chose, actually chosen
1852 rather than nearly chosen. */
1853 .rst-shell-console:has(.rst-shell__menu:not([open])) > .rst-shell__rail,
1854 [rst-shell-console]:has([rst-shell-menu]:not([open])) > [rst-shell-rail] { display: flex; }
1855 /* THE VIEWPORT. The rail itself is a grid item stretched to the row,
1856 which can be taller than the window — that is what makes the
1857 background reach the foot of a long page. So the thing that has to
1858 fit the window is the nav, and it is the nav that is sized against
1859 it: sticky, capped at the viewport MINUS the two gaps it is inset
1860 by, measured as a border box, scrolling itself when an app's
1861 navigation is longer than that.
1862 This is the shipped regression, avoided by construction rather
1863 than by care: block-size: 100dvh on a content box that also
1864 carried padding gave a border box 32px taller than the window, and
1865 the gate that missed it asked where the person sat inside the rail
1866 — a question that stayed true while the rail hung off the screen.
1867 TestTheConsoleRailFitsTheViewport asks the other question, and
1868 carries a control page that must answer it the other way. */
1869 .rst-shell-console > .rst-shell__rail > .rst-shell__nav, [rst-shell-console] > [rst-shell-rail] > [rst-shell-nav] { box-sizing: border-box; inset-block-start: var(--rst-sp-4); max-block-size: calc(100dvh - 2 * var(--rst-sp-4)); overflow-y: auto; position: sticky; }
1870}
1871
1872/* error page — one centred column, whatever shell it lands in: the
1873 status as a quiet label above the sentence, one or two ways out, and
1874 the reference last, where an operator asked to quote it will find it.
1875 Logical properties throughout, so an RTL locale mirrors it with no
1876 second rule. */
1877.rst-error, [rst-error] { margin-inline: auto; max-inline-size: 28rem; padding-block: 4rem var(--rst-sp-6); padding-inline: var(--rst-sp-4); text-align: center; }
1878.rst-error__status, [rst-error-status] { color: var(--rst-text-faint); font-size: var(--rst-fs-xs); font-weight: 600; letter-spacing: 0.08em; margin: 0; }
1879.rst-error__title, [rst-error-title] { color: var(--rst-text); font-size: 1.375rem; font-weight: 600; letter-spacing: -0.015em; line-height: 1.25; margin-block: var(--rst-sp-2) 0; }
1880.rst-error__body, [rst-error-body] { color: var(--rst-text-muted); margin-block: var(--rst-sp-3) 0; }
1881.rst-error__cta, [rst-error-cta] { display: flex; flex-wrap: wrap; gap: var(--rst-sp-2); justify-content: center; margin-block: var(--rst-sp-5) 0; }
1882.rst-error__ref, [rst-error-ref] { color: var(--rst-text-faint); margin-block: 2rem 0; }
1883
1884/* ---------------------------------------------------------------------
1885 Touch: the two zooms nobody asked for.
1886
1887 Mobile Safari zooms the whole page when it focuses a form control
1888 whose text is under 16px, and this file's base type is 14px on
1889 purpose. So every text-entry control comes back up to 16px where the
1890 pointer is coarse, and stays 14px under a mouse. max() rather than a
1891 flat 1rem, so a control deliberately set larger keeps its size; rem
1892 rather than px, so a reader who raised the browser default keeps the
1893 increase. This is the fix. The other one — user-scalable=no in the
1894 viewport tag — buys the same quiet by taking pinch-zoom away from
1895 everyone, which is a WCAG 1.4.4 failure and not on offer here.
1896
1897 Double-tap is the second zoom: tap a button or a row twice in quick
1898 succession and the second tap lands as a zoom gesture rather than a
1899 click. touch-action: manipulation drops that gesture on interactive
1900 elements only, so panning and pinch-zoom are untouched, and so is
1901 double-tapping prose or an image, which is a real way to read a
1902 narrow column. :where() weighs nothing, so any rule can override it.
1903 --------------------------------------------------------------------- */
1904
1905@media (pointer: coarse) {
1906 input, select, textarea,
1907 .rst-input, [rst-input],
1908 .rst-textarea, [rst-textarea],
1909 .rst-search input[type="search"], [rst-search] input[type="search"] {
1910 font-size: max(1rem, 1em);
1911 }
1912}
1913
1914:where(a[href], button, input, select, textarea, label, summary, [role="button"], [role="tab"], [role="option"]) {
1915 touch-action: manipulation;
1916}
1917
1918/* ---------------------------------------------------------------------
1919 Utilities, last on purpose.
1920
1921 .rst-sr-only competes with component rules of the same specificity —
1922 both are single class selectors — so the cascade falls to source
1923 order. Declared earlier it LOSES: .rst-input's width:100% beat its
1924 inline-size:1px. Keep this block at the end of the file so the utility
1925 wins wherever it is applied; a test asserts it stays here.
1926
1927 What that does NOT buy: a replaced element such as <select> has an
1928 intrinsic minimum size, so inline-size:1px does not shrink it to 1px
1929 whoever wins — measured at ~21px in Chromium. clip-path is what
1930 actually hides these, and position:absolute is what keeps them out of
1931 flow. Code that wants a genuinely minimal box (select.js does) should
1932 drop the component class rather than rely on this.
1933 --------------------------------------------------------------------- */
1934
1935/* Visually hidden, but in the DOM and the tab order — and it un-hides on
1936 keyboard focus, because a focused control a sighted keyboard user
1937 cannot see is a trap. */
1938.rst-sr-only {
1939 clip-path: inset(50%);
1940 block-size: 1px;
1941 inline-size: 1px;
1942 overflow: hidden;
1943 position: absolute;
1944 white-space: nowrap;
1945}
1946.rst-sr-only:focus-visible {
1947 background: var(--rst-accent);
1948 border: 0;
1949 border-radius: var(--rst-radius-sm);
1950 block-size: auto;
1951 clip-path: none;
1952 color: var(--rst-on-accent);
1953 cursor: pointer;
1954 font: inherit;
1955 font-size: var(--rst-fs-sm);
1956 inline-size: auto;
1957 overflow: visible;
1958 padding: 0.3rem 0.6rem;
1959 position: static;
1960 white-space: normal;
1961}
1962