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