rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

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