| index 10f6da5..6886854 100644 |
| --- a/internal/sheets/static/tokens.css |
| +++ b/internal/sheets/static/tokens.css |
| @@ -87,6 +87,20 @@ |
| --rst-sp-5: 1.5rem; |
| --rst-sp-6: 2.5rem; |
| |
| + /* The tap floor on small or touch screens: 44px at the default root, |
| + and it tracks a reader who raised their default size. Declared here |
| + so an app's own CSS can use it; only rules inside the touch query |
| + below read it, so desktop density never sees it. */ |
| + --rst-tap: 2.75rem; |
| + |
| + /* The list grid's kebab column. A variable rather than the literal |
| + 32px the samples used to write, so the column can grow to a 44px |
| + kebab on touch screens (the touch block redefines it). An app that |
| + keeps a literal 32px still works: the kebab is justify-self: end, |
| + so it overflows 12px toward the inline start, into the grid's |
| + 0.85rem gap, without reaching the cell beside it. */ |
| + --rst-col-menu: 32px; |
| + |
| /* The scrollbar's gutter, reserved whether or not there is a |
| scrollbar in it, so a short page and a long one are the same width |
| and clicking between them does not slide the layout sideways. |
| @@ -114,6 +128,34 @@ |
| --rst-scrollbar-gutter: stable; |
| } |
| |
| +/* ── Small or touch screens: the type scale moves up one step ─────── |
| + One query decides "a phone, a tablet, or a window too narrow to read |
| + at desktop density", and every touch-only rule in this file sits in |
| + it. pointer: coarse is the PRIMARY pointer, so a touchscreen laptop |
| + driven by its trackpad keeps desktop density; any-pointer would |
| + switch a mouse user's whole interface because a touchscreen exists. |
| + max-width: 40rem catches a narrow desktop window and the design |
| + gallery's 390px Mobile tab, a fine pointer showing what a phone |
| + shows (a rem in a media query is the browser's initial size, not the |
| + root's). The shells' 800px is a different axis: between 640 and |
| + 800px under a mouse is a split window, which gets the narrow LAYOUT |
| + and keeps mouse DENSITY. |
| + |
| + The four tokens move and nothing else: the root size, the rem |
| + spacing and every px geometry stay, and controls grow because their |
| + text grows and because the touch block at the end of this file gives |
| + them a minimum box. Display sizes (page titles, stat numbers, avatar |
| + initials) stay literal on purpose, so a 22px title sits over 16px |
| + body text. ─────────────────────────────────────────────────────── */ |
| +@media (pointer: coarse), (max-width: 40rem) { |
| + :root { |
| + --rst-fs-lg: 1.1875rem; /* 19px */ |
| + --rst-fs-base: 1rem; /* 16px */ |
| + --rst-fs-sm: 0.875rem; /* 14px */ |
| + --rst-fs-xs: 0.8125rem; /* 13px */ |
| + } |
| +} |
| + |
| /* ── Base. Three rules, so a freshly scaffolded app looks finished |
| before it has any CSS of its own. Delete them if your app owns |
| its own base styles. ──────────────────────────────────────────── */ |
| @@ -166,11 +208,16 @@ body { |
| |
| |
| /* ── Buttons: page-header's primary action, empty-state's CTA. ────── */ |
| +/* border-box because rst-btn goes on <a> as often as on <button>, and |
| + only <button> is border-box by the browser's own stylesheet: a block |
| + link-button's width: 100% came out its padding and border wider than |
| + its column, and poked past the sign-in card on a phone. */ |
| .rst-btn, [rst-btn] { |
| align-items: center; |
| background: var(--rst-surface); |
| border: 1px solid var(--rst-line-strong); |
| border-radius: var(--rst-radius-sm); |
| + box-sizing: border-box; |
| color: var(--rst-text); |
| cursor: pointer; |
| display: inline-flex; |
| @@ -297,6 +344,12 @@ body { |
| width: 100%; |
| } |
| |
| +/* The UA's [hidden] { display: none } loses to the display this file |
| + gives every button, so a button marked hidden would show anyway. The |
| + sign-in screen's passkey door ships hidden and is revealed only where |
| + a ceremony can work; without this it would show everywhere. */ |
| +.rst-btn[hidden], [rst-btn][hidden] { display: none; } |
| + |
| /* busy — the loading state rastrillo.js gives every submit button on |
| its way out, and the one state of a button that is not a variant: |
| any .rst-btn can wear it. The spinner is a real child element |
| @@ -330,6 +383,48 @@ body { |
| align-self: center; |
| flex: none; |
| } |
| +/* With no busy label there are no words to show while it works, so the |
| + spinner takes the label's place instead of joining it: joining widens |
| + the button mid-click and shoves whatever sits beside it. The shim |
| + marks a button that HAS a busy label with data-idle-label, which is |
| + what the :not() keys on — those words were asked for, so they stay. |
| + |
| + :has(> spinner) because aria-busy is ordinary ARIA an app may set on |
| + its own fetch button with no shim and no spinner; keyed on aria-busy |
| + alone, that button would lose its label and show nothing at all. |
| + |
| + The label is unpainted, never removed: aria-busy plus the untouched |
| + text is what a screen reader announces. -webkit-text-fill-color |
| + rather than color, because the ring is drawn in currentColor — a |
| + transparent color would take the spinner with it. Element children |
| + (an icon) are not reached by the text fill and take opacity 0, which |
| + unlike visibility keeps them in the accessibility tree. |
| + |
| + The media query stands the swap down for two readers a lone ring |
| + fails. Under reduced motion the ring is still and dimmed, and on its |
| + own reads as a stray "C". Under forced colours the engine repaints |
| + the text fill, so the label paints anyway and a centred ring would |
| + land on top of it. Both keep the older spinner-beside-label look. */ |
| +@media (prefers-reduced-motion: no-preference) and (forced-colors: none) { |
| + .rst-btn[aria-busy="true"]:not([data-idle-label]):has(> .rst-spin), [rst-btn][aria-busy="true"]:not([data-idle-label]):has(> [rst-spin]) { |
| + -webkit-text-fill-color: transparent; |
| + } |
| + .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]) { |
| + opacity: 0; |
| + } |
| + .rst-btn[aria-busy="true"]:not([data-idle-label]) > .rst-spin, [rst-btn][aria-busy="true"]:not([data-idle-label]) > [rst-spin] { |
| + inset: 0; |
| + margin: auto; |
| + position: absolute; |
| + } |
| + /* The spinner's containing block. Zero specificity, so an app's own |
| + position: fixed or sticky on a floating Save still wins — either |
| + of those contains the spinner just as well, and losing it would |
| + drop the button back into the page flow mid-click. */ |
| + :where(.rst-btn[aria-busy="true"]:not([data-idle-label])), :where([rst-btn][aria-busy="true"]:not([data-idle-label])) { |
| + position: relative; |
| + } |
| +} |
| |
| /* ── page-header ──────────────────────────────────────────────────── */ |
| .rst-page-header, [rst-page-header] { |
| @@ -518,6 +613,15 @@ body { |
| min-inline-size: 0; |
| padding: 0.2rem 0; |
| } |
| +/* The zoom floor for the search input. Mobile Safari zooms a page that |
| + focuses a text control under 16px, and this input is set to |
| + --rst-fs-sm, which is 14px even after the touch step: this is the one |
| + floor that fires inside the rastrillo idioms. It sits directly after |
| + the rule above because at equal weight the later rule wins; see the |
| + input floor below for the rest of the reasoning. */ |
| +@media (pointer: coarse), (max-width: 40rem) { |
| + .rst-search input[type="search"], [rst-search] input[type="search"] { font-size: max(1rem, 1em); } |
| +} |
| .rst-search input[type="search"]::placeholder, [rst-search] input[type="search"]::placeholder { |
| color: var(--rst-text-faint); |
| } |
| @@ -580,7 +684,9 @@ body { |
| .rst-lbar + .rst-row, [rst-lbar] + [rst-row] { |
| border-top: 0; |
| } |
| -.rst-row:hover, [rst-row]:hover { |
| +/* Only a row that is a link looks like one: the fill follows the |
| + primary link, so a hand-written row with nothing to open stays flat. */ |
| +.rst-row:has(> .rst-row__main > a):hover, [rst-row]:has(> [rst-row-main] > a):hover { |
| background: var(--rst-surface-2); |
| } |
| .rst-row__lead, [rst-row-lead] { |
| @@ -616,8 +722,13 @@ body { |
| flex: 1; |
| min-inline-size: 0; |
| } |
| -/* The name link's overlay makes the whole row clickable without nesting |
| - one anchor inside another. The action pill sits above it. */ |
| +/* A row with a destination has ONE primary link, and its ::after covers |
| + the row, so the row is the target on every screen: the |
| + cursor, the status-bar URL, middle-click and "open in new tab" all |
| + work across the whole width. Everything else in the row that can be |
| + operated is lifted above the overlay by the :where() rule under the |
| + list grid. The row is not a link, it contains one, so nested anchors |
| + stay impossible. */ |
| .rst-row__main > a, [rst-row-main] > a { |
| color: var(--rst-text); |
| font-weight: 600; |
| @@ -628,6 +739,13 @@ body { |
| inset: 0; |
| position: absolute; |
| } |
| +/* Focus draws the ring on the overlay, inset 2px so a card's corner |
| + cannot cut it, so a keyboard user sees the whole row outlined. The |
| + link also underlines: a check that reads the focused element and its |
| + ancestors (the gallery's keyboard walk) never sees a pseudo-element, |
| + and forced-colors mode keeps text decoration. */ |
| +.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; } |
| +.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; } |
| .rst-row__main > a:hover, [rst-row-main] > a:hover { |
| text-decoration: underline; |
| } |
| @@ -654,12 +772,11 @@ body { |
| color: var(--rst-accent); |
| } |
| |
| -/* A status pill inside a row sits above the name link's overlay, like |
| - the action pill does. */ |
| +/* A status pill inside a row is part of the row's target, not a |
| + control: a dead spot inside a row you can tap is a mis-tap on a |
| + phone. It is not lifted, so a tap on it opens the row. */ |
| .rst-row .rst-status, [rst-row] [rst-status] { |
| flex: none; |
| - position: relative; |
| - z-index: 1; |
| } |
| |
| /* ── status-pill. The dot is decoration; the label is the state. ──── */ |
| @@ -979,16 +1096,23 @@ body { |
| .rst-box > .rst-detail, [rst-box] > [rst-detail] { margin: 0; } |
| |
| /* The list grid — the real data-table vocabulary. The card sets the |
| - columns once (--rst-cols, inline, trailing 32px for the kebab); |
| - rows only choose cells. Hover fill only on rows that contain the |
| - identity link — a display-only row must never look clickable. |
| + columns once (--rst-cols, from a class in the app's stylesheet — |
| + the default CSP blocks inline styles — trailing |
| + var(--rst-col-menu) for the kebab); rows only choose cells. Hover |
| + fill only on rows that contain the identity link — a display-only |
| + row must never look clickable. |
| Like .rst-list, .rst-card has no padding: rows only. Anything that |
| is not a row belongs in .rst-box. */ |
| .rst-card, [rst-card] { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: var(--rst-radius); } |
| .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; } |
| .rst-lrow:last-child, [rst-lrow]:last-child { border-bottom: 0; } |
| -.rst-lrow--head, [rst-lrow~="head"] { color: var(--rst-text-faint); font-size: 0.71875rem; font-weight: 550; letter-spacing: 0.05em; padding: 0.55rem 1rem; text-transform: uppercase; } |
| -.rst-lrow:not(.rst-lrow--head):has(> .rst-nm, > .rst-person):hover, [rst-lrow]:not([rst-lrow~="head"]):has(> .rst-nm, > [rst-person]):hover { background: var(--rst-accent-soft); } |
| +.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; } |
| +.rst-lrow:not(.rst-lrow--head):has(> a.rst-nm, > a.rst-person):hover, [rst-lrow]:not([rst-lrow~="head"]):has(> a.rst-nm, > a[rst-person]):hover { background: var(--rst-accent-soft); } |
| +/* The list grid's primary link is its identity cell: the rst-nm link, |
| + or a person link when a person is who the row is. Its ::after covers |
| + the row, as list-row-action's does. One identity cell per row: with |
| + both, the later overlay wins and the earlier link is unreachable. */ |
| +.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; } |
| .rst-nm { border-radius: var(--rst-radius-sm); color: inherit; font-weight: 550; min-width: 0; text-align: start; text-decoration: none; } |
| a.rst-nm:hover { color: var(--rst-accent); } |
| .rst-nm small { color: var(--rst-text-muted); display: block; font-size: var(--rst-fs-xs); font-weight: 400; } |
| @@ -996,9 +1120,35 @@ a.rst-nm:hover { color: var(--rst-accent); } |
| .rst-no-match, [rst-no-match] { color: var(--rst-text-muted); font-size: var(--rst-fs-sm); padding: 1.6rem 1rem; text-align: center; } |
| .rst-no-match a, [rst-no-match] a { color: var(--rst-accent); } |
| .rst-count-line, [rst-count-line] { color: var(--rst-text-faint); font-size: var(--rst-fs-xs); margin: 0.7rem 0 0; } |
| +/* Controls above the overlay. A positioned overlay paints above every |
| + element that is not positioned, whatever the DOM order, so without |
| + this a checkbox before the name link would be covered and a tap on it |
| + would open the item. |
| + |
| + It lifts the CONTROLS, never their containers: lifting a row menu's |
| + <details> would make it a stacking context and trap its open panel |
| + under the next row's lifted controls. The :not() is attached to the |
| + :is() with no space, so it filters the controls themselves; with a |
| + space it is a descendant combinator and lifts things inside them. |
| + The primary link is excluded, or its ::after would measure against |
| + the link instead of the row. |
| + |
| + The whole selector is inside :where(), so it weighs nothing: its job |
| + is to give an otherwise static control a position and a z-index, and |
| + never to replace a position a component chose. The switch's input, |
| + the date field's pick button and select.js's hidden native select are |
| + all absolutely placed, and at zero weight their own rules win |
| + position while z-index still applies; at any weight they would be |
| + pulled back into flow, out of the boxes they were drawn for. |
| + |
| + Each lifted control is a stacking context at z-index 1, so a popup |
| + it draws itself (a tooltip's [rst-tip]::after) paints under the next |
| + row's lifted controls. Do not put such a control in a row without |
| + raising it. */ |
| +:where(:is(.rst-row, .rst-lrow) :is(a[href], button, summary, label, input, select, textarea):not(.rst-row__main > a, .rst-lrow > a.rst-nm, .rst-lrow > a.rst-person)), :where(:is([rst-row], [rst-lrow]) :is(a[href], button, summary, label, input, select, textarea):not([rst-row-main] > a, [rst-lrow] > a.rst-nm, [rst-lrow] > a[rst-person])) { position: relative; z-index: 1; } |
| @media (max-width: 800px) { |
| .rst-m-hide { display: none; } |
| - .rst-lrow, [rst-lrow], .rst-lrow--head, [rst-lrow~="head"] { grid-template-columns: minmax(0, 1fr) auto 32px; } |
| + .rst-lrow, [rst-lrow], .rst-lrow--head, [rst-lrow~="head"] { grid-template-columns: minmax(0, 1fr) auto var(--rst-col-menu); } |
| } |
| |
| /* row-menu — the per-row kebab: native details/summary, no JS. The |
| @@ -1025,6 +1175,15 @@ a.rst-nm:hover { color: var(--rst-accent); } |
| [rst-locale] hr { border: 0; border-top: 1px solid var(--rst-line); margin: 0.25rem 0; } |
| .rst-danger { color: var(--rst-tone-negative-fg); } |
| .rst-row-menu__panel .rst-danger:hover, [rst-row-menu-panel] .rst-danger:hover { background: var(--rst-tone-negative-bg); } |
| +/* A POST item is a one-button form: no margin, so it lines up with a |
| + link item. busy.js puts a spinner in front of a submit button's label, |
| + and a menu item is not an [rst-btn], so the spinner sits BESIDE the |
| + label; this reserves its slot at the inline end whether or not the |
| + item is busy, so a long translated label can never widen the panel or |
| + wrap mid-submit. The cost is 1.5rem of padding on POST items. */ |
| +.rst-row-menu__panel form, [rst-row-menu-panel] form { margin: 0; } |
| +.rst-row-menu__panel button, [rst-row-menu-panel] button { padding-inline-end: calc(0.65rem + 1rem + var(--rst-sp-2)); position: relative; } |
| +.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; } |
| |
| /* dropdown — the details/summary menu vocabulary (header overflow, |
| list-bar Filter/Sort). Exclusivity between siblings is the native |
| @@ -1206,6 +1365,19 @@ details[open] > summary > .rst-caret, details[open] > summary > [rst-caret] { tr |
| out of its card, and a picker button anchored to the wrapper landing |
| 47px short of the input's visible end. */ |
| .rst-input, [rst-input] { background: var(--rst-surface); border: 1px solid var(--rst-line-strong); border-radius: var(--rst-radius-sm); box-sizing: border-box; color: inherit; font: inherit; padding: 0.45rem 0.6rem; width: 100%; } |
| +/* The zoom floor for the input, and why it is HERE. `font: inherit` in |
| + the rule above resets font-size, so a floor placed before it would be |
| + reset with it; the primary rule below sizes the field on purpose, so |
| + the floor must come before that or it wins the tie and pulls a 19px |
| + field down to 16. max(1rem, 1em) lifts a field whose parent is small |
| + (a bulk bar, a menu panel) to 16px; 1em in font-size is the PARENT's |
| + size, which is exactly why it cannot also keep a large field large — |
| + the bug the old single floor had. A :where() spelling was rejected: at |
| + zero weight it loses to `font: inherit` above and switches itself off. |
| + ui/sizing_test.go holds all three floors to their positions. */ |
| +@media (pointer: coarse), (max-width: 40rem) { |
| + .rst-input, [rst-input] { font-size: max(1rem, 1em); } |
| +} |
| .rst-input:focus-visible, [rst-input]:focus-visible { outline: 2px solid var(--rst-accent); outline-offset: 2px; } |
| /* short is compact, not tiny: 8rem is a border-box width, so it renders |
| within a pixel or two of the old 6.5rem content box plus its padding, |
| @@ -1229,6 +1401,12 @@ textarea.rst-input, textarea[rst-input] { min-height: 5rem; resize: vertical; } |
| class; styled here so both field families render finished. */ |
| .rst-field__required, [rst-field-required] { color: var(--rst-tone-negative-fg); } |
| .rst-textarea, [rst-textarea] { background: var(--rst-surface); border: 1px solid var(--rst-line-strong); border-radius: var(--rst-radius-sm); box-sizing: border-box; color: inherit; font: inherit; line-height: 1.6; min-height: 5rem; padding: 0.45rem 0.6rem; resize: vertical; width: 100%; } |
| +/* The textarea's zoom floor, directly after the rule whose `font: |
| + inherit` would reset it: a floor above that line is dead, undone by |
| + the reset. Reasoning as for the input floor above. */ |
| +@media (pointer: coarse), (max-width: 40rem) { |
| + .rst-textarea, [rst-textarea] { font-size: max(1rem, 1em); } |
| +} |
| .rst-textarea:focus-visible, [rst-textarea]:focus-visible { outline: 2px solid var(--rst-accent); outline-offset: 2px; } |
| /* The three message lines under a control. display:block is load-bearing |
| rather than cosmetic: the field partial emits them as <p>, and the |
| @@ -1429,7 +1607,7 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| .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; } |
| .rst-help:hover, [rst-help]:hover { border-color: var(--rst-text-faint); color: var(--rst-text); } |
| .rst-help svg, [rst-help] svg, .rst-help .icon, [rst-help] .icon { height: 15px; width: 15px; } |
| -.rst-tip::after, [rst-tip]::after { background: var(--rst-text); border-radius: var(--rst-radius-sm); color: var(--rst-bg); content: attr(data-tip); font-size: 0.71875rem; font-weight: 500; inset-inline-end: 0; opacity: 0; padding: 3px 8px; pointer-events: none; position: absolute; top: calc(100% + 6px); transition: opacity 0.12s; white-space: nowrap; z-index: 35; } |
| +.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; } |
| .rst-tip:hover::after, [rst-tip]:hover::after, .rst-tip:focus-visible::after, [rst-tip]:focus-visible::after { opacity: 1; } |
| |
| /* bulk-bar + selbox — select mode as server-rendered state. */ |
| @@ -1442,7 +1620,15 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| .rst-bulkbar__escalate:hover, [rst-bulkbar-escalate]:hover { text-decoration: underline; } |
| .rst-bulkbar .rst-dropdown, [rst-bulkbar] [rst-dropdown] { margin-inline-start: auto; } |
| .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; } |
| -.rst-selbox, [rst-selbox] { align-items: center; display: flex; margin: 0; } |
| +/* The row checkbox's target is its LABEL, padded so the unchanged 16px |
| + box sits in a 24×24 target (WCAG 2.5.8) on every screen. Inside a row |
| + whose whole width is a link, WCAG's spacing exception cannot rescue a |
| + 16px target, since the row's link covers the space around it, so |
| + the padding applies on a desktop too, not only on touch. inline-flex |
| + and justify-self: start keep the label its own size in a grid cell |
| + and in a line, where display: flex made it as wide as either. A |
| + larger invisible hit area was rejected: it would overlap the row's own overlay and take clicks meant for it. */ |
| +.rst-selbox, [rst-selbox] { align-items: center; display: inline-flex; justify-self: start; margin: 0; padding: 4px; } |
| .rst-selbox input, [rst-selbox] input { accent-color: var(--rst-accent); cursor: pointer; height: 16px; margin: 0; width: 16px; } |
| .rst-selbox input:focus-visible, [rst-selbox] input:focus-visible { outline: 2px solid var(--rst-accent); outline-offset: 2px; } |
| |
| @@ -1507,6 +1693,27 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| it names a run of rows without competing with them. */ |
| .rst-combo__rows, [rst-combo-rows] { list-style: none; margin: 0; padding: 0; } |
| .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; } |
| +/* The rest of the combobox's furniture. A divider the page drew between |
| + two options (an <hr>, pinned rows above the rest) and the row saying |
| + nothing matches are never stops: select.js leaves both out of the |
| + keyboard order, and the divider hides while a search re-ranks. */ |
| +.rst-combo__sep, [rst-combo-sep] { border-top: 1px solid var(--rst-line); list-style: none; margin-block: 0.25rem; } |
| +.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; } |
| +/* An option with a description reads as one line: the name, then the |
| + description quieter — the brackets the native option carries are for a |
| + reader without JavaScript, and colour does that job here. */ |
| +.rst-combo__desc, [rst-combo-desc] { color: var(--rst-text-faint); margin-inline-start: 0.5em; } |
| +/* A decorative glyph (a country's flag) before a row, and before the |
| + closed box once that row is picked. aria-hidden everywhere; the text |
| + still says which option it is. The box makes room only while there is |
| + a glyph to show. */ |
| +.rst-combo__option > .rst-combo__lead, [rst-combo-option] > [rst-combo-lead] { margin-inline-end: 0.5em; } |
| +.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; } |
| +.rst-combo > .rst-combo__lead[hidden], [rst-combo] > [rst-combo-lead][hidden] { display: none; } |
| +.rst-combo.rst-combo__has-lead > .rst-input, [rst-combo][rst-combo-has-lead] > [rst-input] { padding-inline-start: 2.2rem; } |
| +/* The list opens upward when there is no room below it (select.js |
| + measures the visual viewport, which a phone's keyboard shrinks). */ |
| +.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; } |
| |
| /* The date fields' combobox, built by datetime.js on any input carrying |
| data-rst-date or data-rst-time. Same shape as .rst-combo above, plus |
| @@ -1526,19 +1733,19 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| against one font size. .rst-dtp__input's padding-inline-end is the |
| room this reserves inside the control, so text never runs under it, |
| with a value or without one. */ |
| -.rst-dtp__pick, [rst-dtp-pick] { align-items: center; background: none; border: 0; border-radius: var(--rst-radius-sm); block-size: 1.75rem; color: var(--rst-muted); cursor: pointer; display: flex; inline-size: 1.75rem; inset-block: 0; inset-inline-end: 0.35rem; justify-content: center; margin-block: auto; padding: 0; position: absolute; } |
| +.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; } |
| .rst-dtp__pick:hover, [rst-dtp-pick]:hover { color: var(--rst-text); } |
| .rst-dtp__pick svg, [rst-dtp-pick] svg { block-size: 1rem; inline-size: 1rem; } |
| .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; } |
| .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; } |
| .rst-dtp__row:hover, [rst-dtp-row]:hover, .rst-dtp__row.is-active, [rst-dtp-row].is-active { background: var(--rst-accent-soft); } |
| .rst-dtp__row--set .rst-dtp__label, [rst-dtp-row~="set"] [rst-dtp-label] { font-weight: 600; } |
| -.rst-dtp__quick .rst-dtp__set, [rst-dtp-quick] [rst-dtp-set] { color: var(--rst-muted); } |
| +.rst-dtp__quick .rst-dtp__set, [rst-dtp-quick] [rst-dtp-set] { color: var(--rst-text-muted); } |
| /* The committed value is marked in the accessibility tree by |
| aria-selected; this is the matching visual, never the only signal. */ |
| .rst-dtp__row[aria-selected="true"] .rst-dtp__label, [rst-dtp-row][aria-selected="true"] [rst-dtp-label] { font-weight: 600; } |
| -.rst-dtp__set, [rst-dtp-set] { flex: none; font-size: var(--rst-fs-xs); color: var(--rst-muted); } |
| -.rst-dtp__hint, [rst-dtp-hint] { color: var(--rst-muted); font-size: var(--rst-fs-xs); padding: 0.35rem 0.65rem 0.15rem; } |
| +.rst-dtp__set, [rst-dtp-set] { flex: none; font-size: var(--rst-fs-xs); color: var(--rst-text-muted); } |
| +.rst-dtp__hint, [rst-dtp-hint] { color: var(--rst-text-muted); font-size: var(--rst-fs-xs); padding: 0.35rem 0.65rem 0.15rem; } |
| |
| /* The popup opens above the field where there is no room below it — |
| datetime.js measures and sets .is-above, because a panel hanging off |
| @@ -1568,7 +1775,7 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| .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; } |
| .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); } |
| .rst-cal__title, [rst-cal-title] { flex: 1; font-size: var(--rst-fs-sm); font-weight: 600; text-align: center; } |
| -.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-muted); cursor: pointer; display: flex; inline-size: 1.75rem; justify-content: center; padding: 0; } |
| +.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; } |
| .rst-cal__nav:hover:not(:disabled), [rst-cal-nav]:hover:not(:disabled) { background: var(--rst-accent-soft); color: var(--rst-text); } |
| /* A month with nothing pickable in it — past the field's min, or past |
| its max. Dimmed and inert rather than removed, so the header keeps |
| @@ -1581,7 +1788,7 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| panel's own direction rather than a second rule. */ |
| [rst-cal-nav~="next"] svg, .rst-cal__nav--next svg { transform: scaleX(-1); } |
| .rst-cal__grid, [rst-cal-grid] { border-collapse: collapse; inline-size: 100%; table-layout: fixed; } |
| -.rst-cal__grid th, [rst-cal-grid] th { color: var(--rst-muted); font-size: var(--rst-fs-xs); font-weight: 500; padding-block: var(--rst-sp-1); text-align: center; } |
| +.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; } |
| .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; } |
| .rst-cal__day:hover, [rst-cal-day]:hover { background: var(--rst-accent-soft); } |
| /* The days either side of the month. Still pickable — reaching for the |
| @@ -1696,13 +1903,19 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| .rst-shell-topbar .rst-shell__tail > .rst-shell__nav a, [rst-shell-topbar] [rst-shell-tail] > [rst-shell-nav] a { padding-inline: 0; } |
| } |
| |
| -/* sidebar shell — a rail beside the page. Below 800px the rail is |
| - hidden behind a <details> chrome strip and, opened, becomes an |
| - full-width column above the content: the open state is the |
| - native element's own, so the shell stays zero-JS like every idiom |
| - above it. The main column carries min-inline-size: 0 so a wide child |
| - (a list grid, a <pre>) scrolls itself instead of stretching the grid |
| - track. */ |
| +/* sidebar shell — a rail beside the page. Below 800px the rail is not |
| + shown on a content page and IS the page on the index (the index/back |
| + block after the console's). The main column carries min-inline-size: |
| + 0 so a wide child (a list grid, a <pre>) scrolls itself instead of |
| + stretching the grid track. |
| + |
| + LEGACY, kept for layouts written before the phone index: the |
| + <details rst-shell-chrome> drawer, which opened the rail into the |
| + page flow. Its rules are the ones naming rst-shell-chrome, below and |
| + in the wide block. None of the index/back rules match an old |
| + layout's root (it has no view), so an app that re-vendors tokens.css |
| + but keeps its old layout still has its drawer. Delete these rules |
| + together once no layout and no gallery page writes rst-shell-chrome. */ |
| .rst-shell__main, [rst-shell-main] { min-inline-size: 0; } |
| /* box-sizing on the rail, per this file's convention (see .rst-input |
| above): set on the component that needs it, never in a `*` reset. |
| @@ -1715,8 +1928,11 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| was ever owed. Anything sized against the viewport has to be measured |
| the way the viewport measures it. */ |
| .rst-shell-sidebar > .rst-shell__rail, [rst-shell-sidebar] > [rst-shell-rail] { background: var(--rst-surface); border-block-end: 1px solid var(--rst-line); box-sizing: border-box; display: none; flex-direction: column; gap: var(--rst-sp-3); padding: var(--rst-sp-4); } |
| +/* LEGACY: the old drawer, see above. */ |
| .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); } |
| +/* LEGACY: the old drawer, see above. */ |
| .rst-shell-sidebar > .rst-shell__chrome > summary::-webkit-details-marker, [rst-shell-sidebar] > [rst-shell-chrome] > summary::-webkit-details-marker { display: none; } |
| +/* LEGACY: the old drawer, see above. */ |
| .rst-shell-sidebar > .rst-shell__chrome[open] + .rst-shell__rail, [rst-shell-sidebar] > [rst-shell-chrome][open] + [rst-shell-rail] { display: flex; } |
| .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; } |
| .rst-shell-sidebar .rst-shell__nav, [rst-shell-sidebar] [rst-shell-nav] { flex-direction: column; gap: 2px; } |
| @@ -1728,21 +1944,20 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| whole mechanism: the rail is a column flex box, so the auto margin |
| eats whatever height is left between the nav and the bottom. |
| |
| - Below 800px the disclosed rail is STILL a flex column — the media |
| - query below only adds block-size: 100dvh, it does not change the |
| - display — so the reason the foot stays put down there is not that the |
| - layout changed but that the column is content-height and the auto |
| - margin has no free space to distribute. That is a dependency, so name |
| - it: give the disclosed rail a min-block-size or a height and the foot |
| - floats back down to the bottom of it, silently. The 390px leg of |
| + Below 800px the rail on the phone index is STILL a flex column, and |
| + it has a min-block-size of the whole screen there, so this auto |
| + margin would float the foot to the bottom of the screen, where the |
| + language menu would open off it. The index rules replace it with a |
| + fixed gap; the 390px leg of |
| TestTheSidebarRailPutsThePersonAtItsFootAndTheLanguageMenuOpensUpward |
| - asserts the rail's own height for exactly that reason. */ |
| + measures that gap for exactly that reason. */ |
| .rst-shell__rail-foot, [rst-shell-rail-foot] { display: flex; flex-direction: column; gap: var(--rst-sp-3); margin-block-start: auto; } |
| /* An un-overridden shell renders the wrapper with nothing in it; draw |
| no gap for it, the same way .rst-shell__foot handles its own. */ |
| .rst-shell__rail-foot:empty, [rst-shell-rail-foot]:empty { display: none; } |
| @media (min-width: 800px) { |
| .rst-shell-sidebar, [rst-shell-sidebar] { display: grid; grid-template-columns: 15rem 1fr; } |
| + /* LEGACY: the old drawer, see above. */ |
| .rst-shell-sidebar > .rst-shell__chrome, [rst-shell-sidebar] > [rst-shell-chrome] { display: none; } |
| .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; } |
| /* A menu at the foot of a full-height rail is a DROPUP: the same |
| @@ -1776,18 +1991,18 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| sidebar without the bar is sidebar; this is the admin shape both of |
| them were half of, and the one an app used to hand-write. |
| |
| - TWO CHROMES, ONE CONTROL. Below 800px the bar's tail and the rail |
| - both have to go away, and two disclosures would be two things to |
| - learn. So one <details rst-shell-menu> gates both: its own next |
| - sibling with +, and the rail from the shell root with :has(). One |
| - [open], two reveals, one summary on screen at 390px. |
| - |
| - Both reveals are written as HIDE-WHEN-CLOSED rather than |
| - show-when-open. In an engine without :has() the rail's hide rule |
| - never matches and the rail renders as a column of links under the |
| - bar — longer page, navigation intact. Spelled the other way round, |
| - the same missing selector is a phone that cannot navigate. Choose |
| - the failure. */ |
| + TWO CHROMES, ONE PATTERN EACH. Below 800px the bar's tail (the account |
| + and language menus) folds behind the one <details rst-shell-menu> and |
| + opens as a card over the page (the card block after this one); the |
| + rail is an index page, like the sidebar's (the index/back block after |
| + that). The :has() pair below is LEGACY: the rail gated on the Menu's |
| + [open], kept for console layouts written before the phone index, |
| + whose root has no view. |
| + |
| + The legacy gate is written HIDE-WHEN-CLOSED rather than |
| + show-when-open: in an engine without :has() its hide rule never |
| + matches and the rail renders as a column of links under the bar, |
| + longer page, navigation intact. */ |
| .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); } |
| .rst-shell-console .rst-shell__menu, [rst-shell-console] [rst-shell-menu] { margin-inline-start: auto; } |
| .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; } |
| @@ -1803,9 +2018,12 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| .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; } |
| .rst-shell-console .rst-shell__nav a:hover, [rst-shell-console] [rst-shell-nav] a:hover { background: var(--rst-accent-soft); color: var(--rst-text); } |
| .rst-shell-console .rst-shell__nav a[aria-current], [rst-shell-console] [rst-shell-nav] a[aria-current] { background: var(--rst-accent-soft); color: var(--rst-accent); font-weight: 600; } |
| -/* The two hide rules. Same [open], two combinators. */ |
| .rst-shell-console .rst-shell__menu:not([open]) + .rst-shell__tail, [rst-shell-console] [rst-shell-menu]:not([open]) + [rst-shell-tail] { display: none; } |
| -.rst-shell-console:has(.rst-shell__menu:not([open])) > .rst-shell__rail, [rst-shell-console]:has([rst-shell-menu]:not([open])) > [rst-shell-rail] { display: none; } |
| +/* LEGACY: the rail gated on the Menu's [open], for console layouts |
| + written before the phone index. A root with a view is new markup, |
| + where the rail follows the view and no :has() decides it; the :not() |
| + keeps this rule out of that fight entirely. */ |
| +.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; } |
| /* Disclosed, the tail is a column across the full width of the wrapped |
| bar. The account's margin-inline-start: auto has to be undone by |
| hand and stretch does not do it: in a column flex box the inline |
| @@ -1850,8 +2068,8 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| one declaration the narrow hide rule contests — rides on :has(). |
| That is the failure direction this shell chose, actually chosen |
| rather than nearly chosen. */ |
| - .rst-shell-console:has(.rst-shell__menu:not([open])) > .rst-shell__rail, |
| - [rst-shell-console]:has([rst-shell-menu]:not([open])) > [rst-shell-rail] { display: flex; } |
| + .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: flex; } |
| /* THE VIEWPORT. The rail itself is a grid item stretched to the row, |
| which can be taller than the window — that is what makes the |
| background reach the foot of a long page. So the thing that has to |
| @@ -1869,6 +2087,261 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| .rst-shell-console > .rst-shell__rail > .rst-shell__nav, [rst-shell-console] > [rst-shell-rail] > [rst-shell-nav] { box-sizing: border-box; inset-block-start: var(--rst-sp-4); max-block-size: calc(100dvh - 2 * var(--rst-sp-4)); overflow-y: auto; position: sticky; } |
| } |
| |
| +/* The topbar's narrow menu is a floating card: it overlays the page and |
| + never pushes it down. The open tail is placed against the bar, at its |
| + inline end (logical insets, so an RTL page anchors it left), scrolling |
| + itself if a long nav does not fit. The account and language menus |
| + inside it expand in place: a popover inside a popover has nowhere good |
| + to go on a phone. It is the menu panels' surface (9px, their border |
| + and shadow), so every menu reads as one system. The current item is a |
| + fill and weight, never a border or an inset bar. |
| + |
| + EVERY rule here is scoped below 800px rather than written narrow and |
| + undone at 800: the wide tail's display: contents removes the tail's |
| + box and nothing else, so the account panel would stay static, the nav |
| + would lose its underline, and the layer below would still swallow |
| + clicks on a desktop someone widened with the menu open. These rules |
| + come after both shells' narrow rules above and win at equal weight. |
| + |
| + CLOSING ON AN OUTSIDE TAP, with and without scripts: while open, the |
| + summary grows an invisible ::before over the viewport, under the card |
| + and over the page. A tap anywhere outside the card lands on it, which |
| + is a click on the summary, which closes the <details> natively, and |
| + the tap never reaches the page. It is position: fixed inside the bar, |
| + so it breaks if an app gives the bar or an ancestor a transform, |
| + filter or backdrop-filter (each makes a containing block for fixed |
| + descendants). While the card is open the bar's z-index 45 is above |
| + the in-page panels (dropdown 30, row menu 40), so a menu left open |
| + without scripts cannot paint over the card, and below the skip link |
| + (60). Only while open: lifted for good, the bar painted over a modal's |
| + scrim (z-index 10), undimmed and still tappable on a phone. Closed, it |
| + is positioned at z-index auto, which the scrim covers. The card is |
| + sized from the bar (100%), not the viewport, which counts a classic |
| + scrollbar's gutter as room and overhung the start edge at 320px. |
| + rastrillo.js adds Escape. |
| + |
| + The console's bar folds the same way; its nav is in the rail, so its |
| + card holds the account and language menus only. Only a console whose |
| + root names its view: an old console layout gates its rail on the |
| + same [open], and the tap-to-close layer would cover that rail, so a |
| + tap on any rail link would close the navigation instead of following |
| + it. An old layout keeps its push-down tail. */ |
| +@media (max-width: 799.98px) { |
| + .rst-shell-topbar .rst-shell__bar, [rst-shell-topbar] [rst-shell-bar], .rst-shell-console--page .rst-shell__bar, .rst-shell-console--index .rst-shell__bar, [rst-shell-console~="page"] [rst-shell-bar], [rst-shell-console~="index"] [rst-shell-bar] { position: relative; } |
| + .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; } |
| + .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); } |
| + .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; } |
| + .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; } |
| + .rst-shell-topbar .rst-shell__tail > .rst-shell__nav, [rst-shell-topbar] [rst-shell-tail] > [rst-shell-nav] { align-items: stretch; gap: 0; } |
| + .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; } |
| + .rst-shell-topbar .rst-shell__tail > .rst-shell__nav a[aria-current], [rst-shell-topbar] [rst-shell-tail] > [rst-shell-nav] a[aria-current] { background: var(--rst-accent-soft); box-shadow: none; color: var(--rst-accent); font-weight: 600; } |
| + /* Topbar only: the rule separates the account from the nav above it, |
| + and in the console's card the account is first, under nothing. */ |
| + .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; } |
| + .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; } |
| + .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; } |
| +} |
| +@keyframes rst-shell-drop { from { opacity: 0; transform: translateY(-4px); } } |
| +@media (prefers-reduced-motion: reduce) { |
| + .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; } |
| +} |
| + |
| +/* ── Sidebar and console on a phone: the index and the back control ── |
| + Below 800px these two shells are two views of every URL, and the page |
| + says which with the view block the layout writes into the root: |
| + [rst-shell-sidebar~="index"] is the rail as a full-screen page of |
| + sections at a real URL; [rst-shell-sidebar~="page"] is the content, |
| + with a back control at the top inline-start whose href is the page's |
| + up block. No hamburger. The console is the same for its rail, under |
| + its bar. |
| + |
| + An old layout's root has no view, so none of these rules match it and |
| + it keeps its drawer (the legacy rules above). Scoped below 800px, like |
| + the card: at 800 and up both views are the rail beside the page, the |
| + back strip and the title are hidden by the rule just below, and |
| + nothing here needs undoing. ─────────────────────────────────────── */ |
| +.rst-shell__back, [rst-shell-back], .rst-shell__title, [rst-shell-title] { display: none; } |
| +@media (max-width: 799.98px) { |
| + /* The content view: a sticky strip of its own at the top, first in |
| + the reading order after the skip link. Its z-index 9 sits above the |
| + sticky form bar (5), so a long form scrolled under the strip does |
| + not paint over Back, and below a modal's scrim (10): above it, Back |
| + stayed undimmed and live under an open modal on a phone. */ |
| + .rst-shell-sidebar--page > .rst-shell__back, [rst-shell-sidebar~="page"] > [rst-shell-back], .rst-shell-console--page > .rst-shell__back, [rst-shell-console~="page"] > [rst-shell-back] { align-items: center; background: var(--rst-surface); border-block-end: 1px solid var(--rst-line); display: flex; inset-block-start: 0; min-block-size: 2.75rem; padding-inline: var(--rst-sp-1); position: sticky; z-index: 9; } |
| + /* The strip is sticky, not in the flow a plain scroll-to-fragment |
| + measures against, so a #fragment link's native landing scroll parks |
| + the target's top edge at the root's scroll origin — under the |
| + strip, the one thing on a content page actually sitting there. |
| + Reserving the strip's own height, border included, as scroll |
| + padding is what makes that landing spot the strip's bottom edge |
| + instead. */ |
| + :root:has(.rst-shell-sidebar--page), :root:has([rst-shell-sidebar~="page"]), :root:has(.rst-shell-console--page), :root:has([rst-shell-console~="page"]) { scroll-padding-block-start: calc(2.75rem + 1px); } |
| + .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; } |
| + /* The chevron is drawn, not an icon: there is no left chevron in the |
| + set and no icon mirroring, and a drawn one stays out of the |
| + accessible name. Two borders of a small square, turned; physical on |
| + purpose, like the switch's knob, because it is a drawing, and |
| + mirrored by the rule after it. */ |
| + .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); } |
| + [dir="rtl"] .rst-shell__back a::before, [dir="rtl"] [rst-shell-back] a::before { transform: rotate(135deg); } |
| + .rst-shell__back a:focus-visible, [rst-shell-back] a:focus-visible { outline: 2px solid var(--rst-accent); outline-offset: -2px; } |
| + .rst-shell-console--page > .rst-shell__rail, [rst-shell-console~="page"] > [rst-shell-rail] { display: none; } |
| + |
| + /* The index view: the rail is the page. main and the skip link go with |
| + it, so the skip link never points at nothing; the sidebar's brand, |
| + which would link to this very page, gives way to the title, the one |
| + h1 here. On the index a page has no main landmark, which is the |
| + point: the page is navigation. */ |
| + .rst-shell-sidebar--index > .rst-shell__main, [rst-shell-sidebar~="index"] > [rst-shell-main], .rst-shell-sidebar--index > .rst-skip, [rst-shell-sidebar~="index"] > [rst-skip], .rst-shell-console--index > .rst-shell__main, [rst-shell-console~="index"] > [rst-shell-main], .rst-shell-console--index > .rst-skip, [rst-shell-console~="index"] > [rst-skip], .rst-shell-sidebar--index .rst-shell__brand, [rst-shell-sidebar~="index"] [rst-shell-brand] { display: none; } |
| + .rst-shell-sidebar--index > .rst-shell__rail, [rst-shell-sidebar~="index"] > [rst-shell-rail] { background: var(--rst-bg); border-block-end: 0; display: flex; gap: 0; min-block-size: 100dvh; padding: var(--rst-sp-5) var(--rst-sp-4); } |
| + /* The console's index is the bar, then the rail, then the foot. The |
| + three share the window: the root is a column at least the window |
| + tall and the rail takes what the bar and the foot leave, so a short |
| + nav does not scroll at all. A rail a whole window tall made every |
| + index scroll by the bar's and the foot's height, however short its |
| + nav; a long nav still grows the rail and the page scrolls. The |
| + console rail is already border-box. */ |
| + .rst-shell-console--index, [rst-shell-console~="index"] { display: flex; flex-direction: column; min-block-size: 100dvh; } |
| + .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); } |
| + .rst-shell-sidebar--index .rst-shell__title, [rst-shell-sidebar~="index"] [rst-shell-title], .rst-shell-console--index .rst-shell__title, [rst-shell-console~="index"] [rst-shell-title] { display: block; font-size: 1.75rem; font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; margin: 0 0 var(--rst-sp-2); padding-inline: 0.25rem; } |
| + /* The rows: every link a full-width row, at least 3rem, 1rem text, a |
| + chevron at the end. The grouping is the markup apps already write, |
| + a group label and then its links: a run of rows is rounded at its |
| + first link (the nav's first, or the one after a label) and its last |
| + (the nav's last, or the one before a label). The :has() half is a |
| + rule of its own, so an engine without :has() drops only it and a |
| + run's last corner is square, which is cosmetic. aria-current is not |
| + marked here: the index is its own page, where "current" is not |
| + true. */ |
| + .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; } |
| + .rst-shell-sidebar--index .rst-shell__nav > a, [rst-shell-sidebar~="index"] [rst-shell-nav] > a, .rst-shell-sidebar--index .rst-shell__nav > a[aria-current], [rst-shell-sidebar~="index"] [rst-shell-nav] > a[aria-current], .rst-shell-console--index .rst-shell__nav > a, [rst-shell-console~="index"] [rst-shell-nav] > a, .rst-shell-console--index .rst-shell__nav > a[aria-current], [rst-shell-console~="index"] [rst-shell-nav] > a[aria-current] { align-items: center; background: var(--rst-surface); border: 1px solid var(--rst-line); border-block-start-width: 0; border-radius: 0; box-sizing: border-box; color: var(--rst-text); display: flex; font-size: 1rem; font-weight: 450; gap: var(--rst-sp-3); min-block-size: 3rem; padding-block: 0; padding-inline: 1rem 0.75rem; } |
| + .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); } |
| + .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); } |
| + .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); } |
| + .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); } |
| + [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); } |
| + .rst-shell-sidebar--index .rst-shell__nav > a:hover, [rst-shell-sidebar~="index"] [rst-shell-nav] > a:hover, .rst-shell-sidebar--index .rst-shell__nav > a:active, [rst-shell-sidebar~="index"] [rst-shell-nav] > a:active, .rst-shell-console--index .rst-shell__nav > a:hover, [rst-shell-console~="index"] [rst-shell-nav] > a:hover, .rst-shell-console--index .rst-shell__nav > a:active, [rst-shell-console~="index"] [rst-shell-nav] > a:active { background: var(--rst-accent-soft); } |
| + .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; } |
| + /* "You came from here": the back link points at #nav-<section>, and |
| + that row flashes once as the index comes back. */ |
| + .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; } |
| + .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; } |
| + /* The foot FOLLOWS the nav on the index, by a fixed gap, never auto: |
| + the rail has a min-height here, and an auto margin would float the |
| + foot to the bottom of the screen, where the language menu would open |
| + off it (the rail-foot comment above has the long version). */ |
| + .rst-shell-sidebar--index .rst-shell__rail-foot, [rst-shell-sidebar~="index"] [rst-shell-rail-foot] { margin-block-start: var(--rst-sp-6); } |
| +} |
| +@keyframes rst-shell-flash { from { background: var(--rst-accent-soft); } } |
| +@media (prefers-reduced-motion: reduce) { |
| + .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; } |
| +} |
| + |
| +/* ── rst-signin: the shipped sign-in card (partials/signin.html) ───── |
| + Brand at the start and the way in at the end, side by side from 48rem |
| + and stacked below it, so a phone reads the brand and then the door. |
| + The card paints its own surface and border because it usually sits |
| + on the stage shell's backdrop: whatever picture an app puts there, |
| + the controls stay on a known ground and their contrast is the |
| + theme's, not the picture's. The border is --rst-line-strong because |
| + it is the card's only edge against that picture, and |
| + ui/contrast_test.go holds it to 3:1 against the default art. |
| + overflow-wrap on the door is for the one long string it shows back — |
| + an address, which has no spaces to break at and would otherwise push |
| + a 320px screen sideways. */ |
| +.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); } |
| +@media (min-width: 48rem) { |
| + .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); } |
| +} |
| +/* The brand column reads mark, name, pitch. The pitch is the line the |
| + column is for, so it is the largest type on the card, bigger than the |
| + door's heading; the name above it is a label. A brand with no pitch |
| + still has its name at label size: the door's heading already says |
| + "Sign in to" that name. The mark takes the accent through |
| + currentColor, so an inline SVG drawn in currentColor follows the |
| + theme without colours of its own. */ |
| +.rst-signin__brand, [rst-signin-brand] { display: flex; flex-direction: column; gap: var(--rst-sp-3); min-inline-size: 0; } |
| +.rst-signin__mark, [rst-signin-mark] { block-size: 3rem; color: var(--rst-accent); inline-size: 3rem; } |
| +.rst-signin__mark > *, [rst-signin-mark] > * { block-size: 100%; display: block; inline-size: 100%; } |
| +.rst-signin__name, [rst-signin-name] { color: var(--rst-text-muted); font-size: var(--rst-fs-lg); font-weight: 600; margin: 0; } |
| +.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; } |
| +.rst-signin__door, [rst-signin-door] { display: flex; flex-direction: column; gap: var(--rst-sp-4); min-inline-size: 0; overflow-wrap: anywhere; } |
| +.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; } |
| +.rst-signin__door > p, [rst-signin-door] > p { margin: 0; } |
| +/* The door spaces its children with gap; a callout's and a field's own |
| + block margins would double it. */ |
| +.rst-signin__door > .rst-callout, [rst-signin-door] > [rst-callout] { margin: 0; } |
| +.rst-signin__form, [rst-signin-form] { display: flex; flex-direction: column; gap: var(--rst-sp-3); margin: 0; } |
| +.rst-signin__form > .rst-field, [rst-signin-form] > [rst-field] { margin: 0; } |
| +.rst-signin__remembered, [rst-signin-remembered] { color: var(--rst-text-muted); margin: 0; } |
| +/* The passkey button and its message are one child of the door. The |
| + message is a live region and must stay rendered, so it is never |
| + display: none; it is only spaced from the button once it has |
| + something to say. While the button is hidden — before the door's |
| + script has found a working WebAuthn, or on a browser without one — |
| + the whole pair is, so the door keeps no gap for a control nobody can |
| + see; the region is back in the page before the button can be |
| + pressed, which is all an announcement needs. The "or" between the |
| + pair and the email form goes with them: beside a hidden button the |
| + page has one way in, and an "or" would point at nothing. It is the |
| + wrapper's immediate neighbour, before it on Ask and after it on a |
| + remembered passkey, so one sibling selector each way reaches it. */ |
| +.rst-signin__passkey, [rst-signin-passkey] { display: flex; flex-direction: column; } |
| +.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; } |
| +.rst-signin__passkey-msg, [rst-signin-passkey-msg] { color: var(--rst-text-muted); margin: 0; } |
| +.rst-signin__passkey-msg:not(:empty), [rst-signin-passkey-msg]:not(:empty) { margin-block-start: var(--rst-sp-2); } |
| +/* The word sits on a hairline drawn by its own ::before and ::after, |
| + which carry no text, so a screen reader hears "or" in reading order |
| + between the two ways in and nothing else. */ |
| +.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); } |
| +.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; } |
| +/* Provider buttons an app adds itself (Google, Apple, …), one form per |
| + provider because each posts to its own route. Sibling forms carry no |
| + spacing of their own, so without a wrapper the buttons touch and read |
| + as one control split in half; the gap is the card form's, so the two |
| + stacks read alike. The 44rem cap is rst-form's measure: stretched |
| + past it a block button is a bar with a word in the middle. The forms' |
| + margins are zeroed because an app's own `form { margin-bottom }` would |
| + be added to the gap, the double spacing rst-form's rule exists to |
| + stop. */ |
| +.rst-signin__providers, [rst-signin-providers] { display: flex; flex-direction: column; gap: var(--rst-sp-3); max-inline-size: 44rem; } |
| +.rst-signin__providers > form, [rst-signin-providers] > form { margin: 0; } |
| + |
| +/* ── rst-stage: the stage shell (layouts/stage.html) ───────────────── |
| + One card centred on a full-page backdrop. [rst-stage] is <body> in |
| + the shell and a plain wrapper in the design-system gallery's frames, |
| + so nothing here assumes which element carries it. The scene is |
| + absolutely positioned inside it rather than fixed, so a page taller |
| + than the window scrolls the picture with it instead of sliding the |
| + card over a still image. The page column stacks its children and |
| + centres each one, rather than laying them in a row: the card is |
| + usually alone, but a line of text above or below it must not end up |
| + beside it. inline-size: 100% because [rst-page]'s auto margins make |
| + a grid item shrink to its content, and a card sized in percent would |
| + shrink with it. */ |
| +.rst-stage, [rst-stage] { box-sizing: border-box; display: grid; grid-template-rows: 1fr auto; isolation: isolate; min-block-size: 100dvh; position: relative; } |
| +.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; } |
| +.rst-stage__scene, [rst-stage-scene] { inset: 0; overflow: hidden; pointer-events: none; position: absolute; z-index: 0; } |
| +.rst-stage__scene > *, [rst-stage-scene] > * { block-size: 100%; display: block; inline-size: 100%; object-fit: cover; } |
| +.rst-stage__foot, [rst-stage-foot] { color: var(--rst-text-muted); padding: var(--rst-sp-4); position: relative; text-align: center; z-index: 1; } |
| +/* The art's two colours: the line colour and a little accent, each |
| + mixed into the page background, faint enough that the sign-in card's |
| + border still clears 3:1 against them in every theme and scheme — |
| + ui/contrast_test.go reads these two color-mix() calls and checks |
| + exactly that. Lower the percentages rather than loosen the test. |
| + non-scaling-stroke keeps the lines fine on a wide screen, where the |
| + viewBox is scaled up to cover it. */ |
| +.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; } |
| +.rst-stage-art__glow, [rst-stage-art-glow] { fill: color-mix(in srgb, var(--rst-accent) 8%, var(--rst-bg)); fill-opacity: 0.2; } |
| +/* Forced colours would paint the lines in the system's text colour at |
| + full strength, and a printout does not need a picture of waves. */ |
| +@media (forced-colors: active) { |
| + .rst-stage-art, [rst-stage-art] { display: none; } |
| +} |
| +@media print { |
| + .rst-stage-art, [rst-stage-art] { display: none; } |
| +} |
| + |
| /* error page — one centred column, whatever shell it lands in: the |
| status as a quiet label above the sentence, one or two ways out, and |
| the reference last, where an operator asked to quote it will find it. |
| @@ -1885,14 +2358,27 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| Touch: the two zooms nobody asked for. |
| |
| Mobile Safari zooms the whole page when it focuses a form control |
| - whose text is under 16px, and this file's base type is 14px on |
| - purpose. So every text-entry control comes back up to 16px where the |
| - pointer is coarse, and stays 14px under a mouse. max() rather than a |
| - flat 1rem, so a control deliberately set larger keeps its size; rem |
| - rather than px, so a reader who raised the browser default keeps the |
| - increase. This is the fix. The other one — user-scalable=no in the |
| - viewport tag — buys the same quiet by taking pinch-zoom away from |
| - everyone, which is a WCAG 1.4.4 failure and not on offer here. |
| + whose text is under 16px. The touch step at the top of this file |
| + makes body text 16px, but controls inherit their size and some sit in |
| + a smaller parent, so every text-entry control still has a floor. It |
| + is written twice, because one spelling cannot do both jobs: |
| + |
| + - the component floors (.rst-input, .rst-textarea, the search input) |
| + sit directly after the rule that resets each one's font, at that |
| + rule's weight, and before any rule that sizes the field on purpose. |
| + max(1rem, 1em) is the floor; 1em in font-size is the PARENT's size, |
| + so a floor that came after the primary field's rule would shrink |
| + it. That was a bug here once: a single floor sat |
| + here, after [rst-input~="primary"] at equal weight, and turned the |
| + 17px primary field into 16px. |
| + - the floor below, for an app's own input with no rst- attribute, |
| + is wholly inside :where(): the UA stylesheet is what sizes such a |
| + field, any author rule beats it, and any rule the app writes to size |
| + its own field still wins. |
| + |
| + rem rather than px so a reader who raised the browser default keeps |
| + the increase. user-scalable=no in the viewport tag would buy the same |
| + quiet by taking pinch-zoom from everyone, which fails WCAG 1.4.4. |
| |
| Double-tap is the second zoom: tap a button or a row twice in quick |
| succession and the second tap lands as a zoom gesture rather than a |
| @@ -1902,11 +2388,8 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| narrow column. :where() weighs nothing, so any rule can override it. |
| --------------------------------------------------------------------- */ |
| |
| -@media (pointer: coarse) { |
| - input, select, textarea, |
| - .rst-input, [rst-input], |
| - .rst-textarea, [rst-textarea], |
| - .rst-search input[type="search"], [rst-search] input[type="search"] { |
| +@media (pointer: coarse), (max-width: 40rem) { |
| + :where(input:not([type=checkbox]):not([type=radio]):not([type=range]), select, textarea) { |
| font-size: max(1rem, 1em); |
| } |
| } |
| @@ -1915,6 +2398,111 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| touch-action: manipulation; |
| } |
| |
| +/* ── Tap targets on small or touch screens ────────────────────────── |
| + Every interactive idiom is at least --rst-tap (44px) on both axes, |
| + measured as the box a tap ACTIVATES: the label of a label-backed |
| + control, the row of a stretched link. Anything that is display: |
| + inline today becomes inline-flex or flex here, because min sizes do |
| + nothing on an inline box. Each selector repeats the weight of the |
| + rule it raises: a floor lighter than the size it raises loses |
| + wherever it sits (the topbar's Menu summary is (0,2,1)). |
| + |
| + The block is last before the utilities so it wins ties with every |
| + component above it. ui/sizing_test.go fails when a rule styling an |
| + a, button, summary or label arrives without a row in the inventory |
| + this block implements, and ui/sizing_browser_test.go measures every |
| + control at 390 and 1024 with a coarse pointer and at 600 with a |
| + mouse. ─────────────────────────────────────────────────────────── */ |
| +@media (pointer: coarse), (max-width: 40rem) { |
| + :root { --rst-col-menu: var(--rst-tap); } |
| + |
| + .rst-btn, [rst-btn] { justify-content: center; min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); } |
| + /* min-inline-size for a field in a flex row (a bulk bar), which |
| + shrinks it past the floor; a stretched field is far wider anyway. */ |
| + .rst-input, [rst-input], .rst-textarea, [rst-textarea] { min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); } |
| + /* The box is the target, and so is the input inside it: a tap focuses |
| + the input, so the input stretches to the box's full height. */ |
| + .rst-search, [rst-search] { min-block-size: var(--rst-tap); padding-block: 0; } |
| + .rst-search input[type="search"], [rst-search] input[type="search"] { align-self: stretch; } |
| + .rst-search__clear, [rst-search-clear] { min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); } |
| + .rst-ftok a, [rst-ftok] a { block-size: var(--rst-tap); inline-size: var(--rst-tap); } |
| + .rst-help, [rst-help] { block-size: var(--rst-tap); inline-size: var(--rst-tap); } |
| + |
| + .rst-dropdown > summary, [rst-dropdown] > summary { align-items: center; min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); } |
| + .rst-dropdown__menu .rst-menu-group > summary, [rst-dropdown-menu] [rst-menu-group] > summary { align-items: center; display: flex; min-block-size: var(--rst-tap); } |
| + .rst-dropdown__menu a, [rst-dropdown-menu] a, .rst-dropdown__menu button, [rst-dropdown-menu] button, .rst-row-menu__panel a, [rst-row-menu-panel] a, .rst-row-menu__panel button, [rst-row-menu-panel] button, .rst-locale button, [rst-locale] button { align-items: center; box-sizing: border-box; display: flex; min-block-size: var(--rst-tap); } |
| + /* The menu cap's 8rem floor exists so a three-item menu opened in a |
| + short frame shows whole; three 44px rows are taller than 8rem, so |
| + here the floor is three taps plus the panel's padding and border |
| + (ui's TestAMenuOpenedInsideAShortFrameIsStillUsable). border-box |
| + above for the same reason: the dropdown's links were content-box, |
| + and their padding made each row 57px instead of 44. */ |
| + .rst-dropdown__menu, [rst-dropdown-menu], .rst-row-menu__panel, [rst-row-menu-panel] { max-block-size: min(20rem, max(calc(100dvh - 6rem), calc(3 * var(--rst-tap) + 1rem))); } |
| + .rst-combo__option, [rst-combo-option], .rst-dtp__row, [rst-dtp-row] { align-items: center; box-sizing: border-box; display: flex; min-block-size: var(--rst-tap); } |
| + |
| + /* The date field: a 44px pick button, and room in the input so typed |
| + text never runs under it. */ |
| + .rst-dtp__input, [rst-dtp-input] { padding-inline-end: 3rem; } |
| + .rst-dtp__pick, [rst-dtp-pick] { block-size: var(--rst-tap); inline-size: var(--rst-tap); inset-inline-end: 0; } |
| + .rst-cal__nav, [rst-cal-nav] { block-size: var(--rst-tap); inline-size: var(--rst-tap); } |
| + .rst-cal__day, [rst-cal-day] { block-size: var(--rst-tap); } |
| + /* The calendar docks to the bottom of the viewport on every small or |
| + touch screen. Anchored, it is placed from the field's inline start, |
| + and a date field can sit at the trailing end of a field row at any |
| + width; docked, it is always inside the viewport. The grid asks for |
| + seven taps and the panel is fit-content around it, with the |
| + scrollbar gutter reserved, so a classic scrollbar widens the PANEL |
| + and never narrows the DAYS. Both max sizes clamp to the viewport: |
| + below a 342px viewport the days narrow to about 41px and stay 44px |
| + tall, a deliberate exception to the 44px target, because seven full |
| + days and the panel's padding do not fit a narrower screen. The |
| + height cap is why the panel scrolls itself: six weeks of 44px rows |
| + are taller than a phone in landscape, and a fixed panel's clipped |
| + top cannot be reached by scrolling the page. top: auto because the |
| + base rule's physical top: 100% would otherwise be measured against |
| + the viewport; the block-start inset and the margins reset because |
| + .is-above sets them (the :is() weighs (0,3,0) and beats it). */ |
| + :is(.rst-cal, .rst-dtp .rst-cal, .rst-dtp.is-above .rst-cal), :is([rst-cal], [rst-dtp] [rst-cal], [rst-dtp].is-above [rst-cal]) { box-sizing: border-box; inline-size: fit-content; inset-block-end: var(--rst-sp-2); inset-block-start: auto; inset-inline: 0; margin: 0 auto; max-block-size: calc(100dvh - 2 * var(--rst-sp-2)); max-inline-size: calc(100vw - 2 * var(--rst-sp-2)); overflow-y: auto; overscroll-behavior: contain; position: fixed; scrollbar-gutter: stable; top: auto; } |
| + :is(.rst-cal, .rst-dtp .rst-cal) .rst-cal__grid, :is([rst-cal], [rst-dtp] [rst-cal]) [rst-cal-grid] { inline-size: calc(7 * var(--rst-tap)); max-inline-size: 100%; } |
| + |
| + /* The row's own controls. The checkbox's label grows by |
| + padding, so the 16px box stays 16px and the label is 44×44. */ |
| + .rst-row__action, [rst-row-action] { align-items: center; box-sizing: border-box; display: inline-flex; justify-content: center; min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); padding-inline: 1rem; } |
| + .rst-row-menu > summary, [rst-row-menu] > summary { block-size: var(--rst-tap); inline-size: var(--rst-tap); } |
| + .rst-selbox, [rst-selbox] { box-sizing: border-box; justify-content: center; min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); padding: 0; } |
| + .rst-lrow > .rst-nm, [rst-lrow] > .rst-nm { align-content: center; display: grid; min-block-size: var(--rst-tap); } |
| + a.rst-person, a[rst-person] { min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); } |
| + |
| + .rst-pagination a, [rst-pagination] a, .rst-pagination span, [rst-pagination] span { align-items: center; box-sizing: border-box; display: inline-flex; justify-content: center; min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); } |
| + .rst-seg-tabs a, [rst-seg-tabs] a { align-items: center; box-sizing: border-box; display: inline-flex; justify-content: center; min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); } |
| + .rst-switch, [rst-switch], .rst-choice__cards label, [rst-choice-cards] label, .rst-tblock > .rst-tblock__head, [rst-tblock] > [rst-tblock-head] { min-block-size: var(--rst-tap); } |
| + |
| + /* flex: none because the bar is a flex row that, at 390, shrank the |
| + close link to 26px wide to make room for the escalate link's text. */ |
| + .rst-bulkbar__close, [rst-bulkbar-close] { block-size: var(--rst-tap); flex: none; inline-size: var(--rst-tap); } |
| + .rst-bulkbar__escalate, [rst-bulkbar-escalate] { align-items: center; display: inline-flex; justify-content: center; min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); } |
| + .rst-modal-close, [rst-modal-close] { align-items: center; display: inline-flex; justify-content: center; min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); } |
| + .rst-modal-panel > nav a, [rst-modal-panel] > nav a { align-items: center; display: flex; min-block-size: var(--rst-tap); } |
| + .rst-back-nav a, [rst-back-nav] a { align-items: center; display: inline-flex; min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); } |
| + |
| + .rst-shell__brand, [rst-shell-brand] { align-items: center; display: inline-flex; min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); } |
| + .rst-shell-topbar .rst-shell__nav a, [rst-shell-topbar] [rst-shell-nav] a, .rst-shell-sidebar .rst-shell__nav a, [rst-shell-sidebar] [rst-shell-nav] a, .rst-shell-console .rst-shell__nav a, [rst-shell-console] [rst-shell-nav] a { align-items: center; box-sizing: border-box; display: flex; min-block-size: var(--rst-tap); } |
| + .rst-shell-topbar .rst-shell__menu > summary, [rst-shell-topbar] [rst-shell-menu] > summary, .rst-shell-console .rst-shell__menu > summary, [rst-shell-console] [rst-shell-menu] > summary { min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); } |
| + /* LEGACY: the old drawer's summary. */ |
| + .rst-shell-sidebar > .rst-shell__chrome > summary, [rst-shell-sidebar] > [rst-shell-chrome] > summary { min-block-size: var(--rst-tap); } |
| + /* The card's own summaries: the open Menu (its pressed state is a |
| + (0,3,1) rule) and the account menu inside it. */ |
| + .rst-shell-topbar .rst-shell__menu[open] > summary, [rst-shell-topbar] [rst-shell-menu][open] > summary, .rst-shell-console--page .rst-shell__menu[open] > summary, .rst-shell-console--index .rst-shell__menu[open] > summary, [rst-shell-console~="page"] [rst-shell-menu][open] > summary, [rst-shell-console~="index"] [rst-shell-menu][open] > summary { min-block-size: var(--rst-tap); min-inline-size: var(--rst-tap); } |
| + .rst-shell-topbar .rst-shell__tail > .rst-shell__account > summary, [rst-shell-topbar] [rst-shell-tail] > [rst-shell-account] > summary, .rst-shell-console--page .rst-shell__tail > .rst-shell__account > summary, .rst-shell-console--index .rst-shell__tail > .rst-shell__account > summary, [rst-shell-console~="page"] [rst-shell-tail] > [rst-shell-account] > summary, [rst-shell-console~="index"] [rst-shell-tail] > [rst-shell-account] > summary { min-block-size: var(--rst-tap); } |
| + /* The back control (its strip is 2.75rem already; the link is the |
| + target) and the index rows, which are 3rem: the nav-link floor above |
| + weighs the same as the index's row rule and comes later, so without |
| + this it would shrink the rows to 44px on exactly the screens the |
| + index is for. */ |
| + .rst-shell__back a, [rst-shell-back] a { min-inline-size: var(--rst-tap); } |
| + .rst-shell-sidebar--index .rst-shell__nav > a, [rst-shell-sidebar~="index"] [rst-shell-nav] > a, .rst-shell-console--index .rst-shell__nav > a, [rst-shell-console~="index"] [rst-shell-nav] > a { min-block-size: 3rem; } |
| +} |
| + |
| /* --------------------------------------------------------------------- |
| Utilities, last on purpose. |
| |