carlos / contra Public

Clone
git clone https://amadan.net/carlos/contra

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