oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

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