rastrillo / fiesta Public

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

Plain git — no account needed to clone.

Download

Download this file

File too large to show in full — download it above (showing a truncated preview below).

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