Files
op-packages/luci-theme-footstrap/styles/theme/65-dropdown.css
T

545 lines
30 KiB
CSS

@layer theme {
/* Generic .dropdown-menu popup — ui.js's split "Save & Apply" menu, plus whatever a third-party
* luci-app-* emits. NOT the theme's nav menu (that is a plain nested <ul>; see 10-chrome.css).
* Hover text is ACCENT to match the open cbi-dropdown list: this is a form control's menu. */
.dropdown-menu { background-color: var(--fs-panel); border-color: var(--fs-border); }
.dropdown-menu a { color: var(--fs-text); text-shadow: none; }
.dropdown-menu a:hover,
.dropdown-menu a:focus-visible { background: var(--fs-panel2); background-image: none; color: var(--fs-accent); }
/* Split "Save & Apply": a plain dropdown in the action bar carries no frame of its own.
*
* The guard is the same `:not(.btn):not(.cbi-button)` the widget rules below use, and it is
* load-bearing: the split control's outer div IS the button, and the class on it is the one
* ui.js picked for the SELECTED mode — `cbi-button-apply` for "Save & Apply", but
* `cbi-button-negative` the moment the user picks "Apply unchecked" (luci.js's ComboButton
* `classes` map). Unguarded, this rule (0,2,0) erased that variant's fill while its ink
* (--fs-on-danger) survived from the variant rule that sets no background — a button painted
* page-on-page: 1.13:1 in dark and 1.06:1 in light, measured on the footer. Only the mode
* repainted below was ever visible. */
.cbi-page-actions .cbi-dropdown:not(.btn):not(.cbi-button) {
border: 0; border-radius: var(--fs-radius); background: transparent;
}
/* Apply is the one variant with no fill of its own — `.cbi-button-action` and the rest carry
* theirs from theme/55-buttons.css, and `.cbi-button-apply` is filled only where it is THE
* primary action, which is this bar. Painted by SPECIFICITY — it used to be a rule written
* afterwards, i.e. a fix that depended on source order. */
.cbi-page-actions .cbi-dropdown.cbi-button-apply,
.cbi-page-actions .cbi-dropdown > .cbi-button {
background: var(--fs-accent);
background-image: none;
border: 0;
color: var(--fs-on-accent);
}
.cbi-page-actions .cbi-dropdown.cbi-button-apply:hover,
.cbi-page-actions .cbi-dropdown > .cbi-button:hover { filter: var(--fs-hover-lift); }
/* ---- cbi-dropdown widget: the JS select used across all forms ----
* Restyled to the design dropdown (panel2, rounded, chevron, focus ring) + themed open list.
* A native <select> is styled separately.
*
* The widget's MACHINERY is here too — absorbed from base/80-dropdown.css whole. ui.js drives
* it purely through attributes ([open], [multiple], [empty], [more], [display], [selected]);
* the rules below turn those into a working combobox. The display state machine is UNFLAGGED:
* upstream resolved the states with !important, but flag-vs-flag falls back to
* specificity-then-order anyway, so the machine below states the SAME resolution openly (see
* the state-machine block). The `ul` margins were flagged too, on the stated grounds that ui.js
* writes `margin` INLINE on an open list. IT DOES NOT — grepped on both releases: ui.js touches
* only `top`/`left`/`right`/`maxHeight`/`bottom` on that `ul`, and only under `'ontouchstart' in
* window` (see 90-responsive.css, where the flags that fight THOSE live). The flags came in with
* the absorbed widget from luci-theme-bootstrap, which has no @layer and needed one to beat its
* own `ul { margin: 0 0 10px 25px }`; here the layer split does that for free. They were not
* idle, though — they were beating THIS FILE: `margin: 0 !important` on (0,1,1) out-ranked the
* open popover's own `margin-top` at (0,2,1) below, so the list sat 1px off the control instead
* of the designed 6px, and the two `.btn` rules then needed flags of their own purely to escape
* the first one. Measured on the gallery: dropping all three moves exactly one value (that gap,
* 1px -> 7px) and nothing else. */
.cbi-dropdown {
display: inline-flex;
cursor: pointer;
height: auto;
position: relative;
padding: 0;
}
.cbi-dropdown img {
/* constrain a vector icon of any intrinsic size to the text line */
max-height: 1.6em;
width: auto;
}
.cbi-dropdown > ul {
margin: 0;
padding: 0;
list-style: none;
overflow-x: hidden;
overflow-y: hidden;
display: flex;
width: 100%;
}
/* clears the button's own side inset so the label starts on its text, not on its border.
* Unflagged: (0,3,1) over the rule above's (0,1,1) — it only ever needed a flag to beat that
* rule's flag. */
.cbi-dropdown.btn > ul:not(.dropdown),
.cbi-dropdown.cbi-button > ul:not(.dropdown) {
margin: 0 0 0 var(--fs-space-3);
}
.cbi-dropdown.btn.spinning > ul:not(.dropdown),
.cbi-dropdown.cbi-button.spinning > ul:not(.dropdown) {
margin: 0;
}
.cbi-dropdown > ul.preview {
display: none;
}
/* no `display` here: `.open` states its own below, and `.more` is display: none until the
* [multiple][more]/[multiple][empty] showers flip it — one declaration per state, or the
* audit's same-selector shadow check fires on this very pair */
.cbi-dropdown > .open,
.cbi-dropdown > .more {
flex-grow: 0;
flex-shrink: 0;
flex-direction: column;
justify-content: center;
text-align: center;
}
/* margin only: base also gave the button's chevron a .5em padding and a border-left divider,
* but both were DEAD on arrival — the theme's `.open` rule below zeroes padding and border in
* a later layer. A dead declaration is deleted, not relocated. */
.cbi-dropdown.btn > .open,
.cbi-dropdown.cbi-button > .open {
margin-inline-start: .5em;
}
/* display only: the placeholder's ink is the theme's own `li[placeholder]` --fs-faint below
* (base's --fs-dim had already lost to it by layer — moving it would recolour the row) */
.cbi-dropdown:not(.btn):not(.cbi-button) > ul > li[placeholder] {
display: none;
}
.cbi-dropdown > ul > li {
display: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
flex-grow: 1;
align-items: center;
align-self: center;
color: inherit;
}
.cbi-dropdown > ul > li .hide-open { display: initial; }
.cbi-dropdown > ul > li .hide-close { display: none; }
/* the chip divider on a BUTTON dropdown's preview; the form dropdown zeroes it below */
.cbi-dropdown > ul > li[display]:not([display="0"]) {
border-inline-start: 1px solid var(--fs-border);
}
.cbi-dropdown[empty] > ul {
max-width: 1px;
}
.cbi-dropdown > ul > li > form {
display: none;
margin: 0;
padding: 0;
pointer-events: none;
}
.cbi-dropdown > ul > li img {
vertical-align: middle;
margin-inline-end: .25em;
}
.cbi-dropdown > ul > li > form > input[type="checkbox"] {
margin: 0 var(--fs-space-1) 0 0;
}
/* ---- the display state machine, on specificity ----
* Every shower OUT-SPECIFIES the hider it overrides: li[display] (0,2,2) > the li hider
* (0,1,2); [open] > ul.dropdown > li (0,3,2) likewise; the placeholder rules (0,5,2 and
* 0,6,2) > the placeholder hider (0,4,2); the form rule (0,4,3) > its hider (0,1,3).
* [empty]'s block (0,2,2) loses to [open]'s flex (0,3,2) deliberately — an open empty list
* still lays out as a popup — which is exactly the resolution the old flags produced among
* themselves (importance never separated them; specificity always did). */
.cbi-dropdown > ul > li[display],
.cbi-dropdown[open] > ul.preview,
.cbi-dropdown[open] > ul.dropdown > li,
.cbi-dropdown[multiple] > ul > li > label,
.cbi-dropdown[multiple][open] > ul.dropdown > li,
.cbi-dropdown[multiple][more] > .more,
.cbi-dropdown[multiple][empty] > .more {
flex-grow: 1;
display: flex;
}
/* The ONE state the old flags actually carried: a closed optional single dropdown with an
* empty value. ui.js hands the [display] attribute to the placeholder row itself
* (`sel[0] ?? items[0]`), and the placeholder hider (0,4,2) out-specifies the generic
* li[display] shower (0,2,2) — unflagged, the closed control rendered EMPTY (this is the
* "popup collapses" the old base comment measured). :not([open]) keeps this out of the open
* list, where the [optional][open] rule below owns the row — the states cannot overlap, so
* nothing here rests on source order. */
.cbi-dropdown:not([open]):not(.btn):not(.cbi-button) > ul > li[placeholder][display] {
flex-grow: 1;
display: flex;
}
.cbi-dropdown[empty] > ul > li,
.cbi-dropdown[optional][open] > ul.dropdown > li[placeholder] {
display: block;
}
.cbi-dropdown[multiple][open] > ul.dropdown > li > form {
display: flex;
}
.cbi-dropdown[open] > ul.dropdown > li .hide-open { display: none; }
/* `min-width` because the thing that overflows an open row is a CHILD with an inline width, so
* wrapping the row (theme/65 below) cannot reach it: form.js's RichListValue writes the
* description block as `<div class="hide-close" style="min-width:25vw">`, which assumes the list
* grows to fit — true on a theme that leaves ul.dropdown unbounded, false here (capped at
* min(92vw, 420px), 90-responsive.css). The inline floor is a VIEWPORT fraction, so the wider the
* screen the worse it gets: measured on a live router at 2559px, 25vw = 640px inside a 406px row
* — scrollWidth 664 vs clientWidth 406, and the row's own `overflow: hidden` cut "…for routing
* with/withou" mid-word, losing the whole "Requires hardware NAT support" sentence (issue #15,
* reported again after 0.10.1, whose fix only addressed the row). At 1280px the same option fits,
* which is why it does not reproduce on a laptop. An author !important is the only thing that
* outranks an inline declaration; releasing the floor lets the text wrap inside its row. */
.cbi-dropdown[open] > ul.dropdown > li .hide-close { display: initial; min-width: 0 !important; }
.cbi-dropdown[open] > ul.dropdown > li:last-child {
margin-bottom: 0;
}
.cbi-dropdown[open] > ul.dropdown > li[unselectable] {
opacity: .7;
}
.cbi-dropdown:not(.btn):not(.cbi-button) {
background: var(--fs-panel2);
border: 1px solid var(--fs-border);
border-radius: var(--fs-radius);
color: var(--fs-text);
min-height: var(--fs-ctl-h);
/* absorbed from base's generic field box with the width below. The fixed height is
* load-bearing next to the min-height: without it a closed dropdown grows to its
* tallest child (the Save & Apply split control carries a 40px button — measured,
* 38 -> 42px on the gallery). */
height: var(--fs-control-h);
font-size: var(--fs-type);
transition: border-color var(--fs-dur), box-shadow var(--fs-dur);
/* Width absorbed from base/30-forms.css, where it was grouped with .cbi-dynlist's — so the
* theme sized the dynlist and base still sized the dropdown, two halves of the same
* decision in two layers. Same numbers, and they are NOT the dynlist's (240/440): a
* dropdown holds one value, a dynlist a column of them. */
min-width: 210px;
max-width: 400px;
width: auto;
/* The widget's own box. In base these two carried `!important` — not to beat an inline
* style, but to beat base's OWN generic form-field rule, which sets `display: inline-block`
* and `padding: 4px` on this very selector at a higher specificity (0,3,0 vs 0,1,0). A later
* LAYER beats it for free, which is exactly what the layer split is for. */
display: inline-flex;
padding: 0;
}
/* …and the button dropdown (the Save & Apply split control), where the flag was beating the
* theme's own `.cbi-button` padding. Specificity does it: 0,2,0 over 0,1,0. */
.cbi-dropdown.btn, .cbi-dropdown.cbi-button { display: inline-flex; padding: 0; }
.cbi-dropdown:not(.btn):not(.cbi-button):hover { border-color: var(--fs-dim); }
.cbi-dropdown:focus, .cbi-dropdown[open] {
border-color: var(--fs-accent);
box-shadow: var(--fs-focus-ring);
outline: none;
}
/* `.cbi-dropdown > ul.dropdown > li` is the same menu row on a BUTTON dropdown (the Save & Apply
* split control), which the `:not(.btn):not(.cbi-button)` guard excludes — so that menu kept
* base's .25em rows while every other dropdown got these. One menu, one row. */
.cbi-dropdown:not(.btn):not(.cbi-button) > ul > li,
.cbi-dropdown > ul.dropdown > li {
padding: var(--fs-space-2) var(--fs-space-3); color: var(--fs-text);
font-family: var(--fs-font-sans);
/* one menu row: a grid step, which is what a line of text at this scale rounds to. NOT
* `calc(--fs-type * --fs-leading)` — that is 19.5px, and a half-pixel on a BOX edge is
* the thing the scale exists to stop. Absorbed with the machinery above. */
min-height: var(--fs-space-5);
}
/* …and the row's CONTENTS, which is where that font was being lost. A MULTI-select wraps every
* item in `<form><label><input type=checkbox> text</label></form>` (ui.js builds the checkbox
* that way), and `.cbi-value-field *` in theme/60-inputs.css hands mono to every descendant of a
* field — it matches that form and that label DIRECTLY, while the rule above only reaches the
* `<li>`, whose font the label then overrides rather than inherits. So the same widget drew two
* fonts depending on one option: a single-select list, whose text sits in the `<li>` itself,
* came out sans; every multi-select list came out mono, in the open menu AND in the closed
* control that lists what is selected. Measured on the router with ui.Dropdown: `<li>` Manrope,
* its `<form>`/`<label>` JetBrains Mono.
*
* It is a LABEL, not a value — the same distinction `<strong>` gets in theme/45-misc.css — and
* mono spacing is what makes a wordy option list look stretched (`3G Band 1`, `LTE B1`, reported
* against luci-app-modeminfo's Bands page). `inherit`, not the token again: the row above is the
* one place the menu's font is chosen. The filter/create `<input>` inside the list is left
* alone: what the user TYPES there is a value. */
.cbi-dropdown > ul > li > form,
.cbi-dropdown > ul > li label { font-family: inherit; }
.cbi-dropdown:not(.btn):not(.cbi-button) > ul > li[display]:not([display="0"]) { border-inline-start: 0; }
.cbi-dropdown > ul > li[placeholder] { color: var(--fs-faint); }
/* the multiselect's inline filter/create inputs — moved from base with the generic `input`
* box in theme/60-inputs.css: in base these beat it in-layer, left behind they would lose
* to it by layer and render as free-standing 210px fields inside the open list */
.cbi-dropdown > ul > li input[type="text"] { height: var(--fs-space-5); }
.cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child { width: 100%; }
/* Custom chevron in the toggle (stock text/svg arrow hidden). `.open` is the only hit target that
* opens the list — on a button dropdown (.cbi-dropdown.btn, e.g. Diagnostics) a click anywhere
* else RUNS the button. `font-size: 0` collapsed the stock `line-height: 2em`, so the span
* measured 28x0 and the chevron (an absolute ::after) sat outside its own box: aiming at it hit
* the button. Hence a real box, chevron centred in it. */
.cbi-dropdown > .open {
display: flex;
font-size: 0; line-height: 0; border: 0;
position: relative; padding: 0;
align-self: stretch;
min-width: 34px; min-height: var(--fs-ctl-h-sm);
align-items: center;
cursor: pointer;
}
/* `.more` is the "…" a multi-value dropdown shows when its chips overflow. base wrote it in ONE
* rule with `.open`; the theme took `.open` for the chevron above and left its twin behind, so
* the two halves of the same toggle strip sat in different layers. Same geometry, absorbed —
* and hidden by default here (the [multiple][more]/[multiple][empty] showers reveal it),
* with base's muted ink. */
.cbi-dropdown > .more { line-height: 2em; padding: 0 .3em; color: var(--fs-dim); display: none; }
.cbi-dropdown > .open > * { display: none; }
.cbi-dropdown > .open::after {
content: ""; position: absolute; inset: 0; margin: auto;
width: 7px; height: 7px;
border-right: 2px solid var(--fs-dim); border-bottom: 2px solid var(--fs-dim);
transform: translateY(-2px) rotate(45deg); /* optical centre of a "v" */
}
/* A BUTTON dropdown (Save & Apply, Diagnostics) draws that chevron on the button's own FILL, where
* --fs-dim is a grey nobody measured against it — on the accent-filled Save & Apply it read as a
* smudge, not an arrow. currentColor takes whatever ink the variant already set, so there is no
* second list of variants to keep in sync (the same reason base draws this widget's divider with
* a colourless `border-left`). The form-control dropdown keeps the muted chevron on purpose:
* there the arrow is an affordance beside the value, not the content. */
.cbi-dropdown.btn > .open::after,
.cbi-dropdown.cbi-button > .open::after { border-color: currentColor; }
/* open list -> design popover (this was once stated four times over; these are the values
* that were winning). */
.cbi-dropdown[open] { z-index: var(--fs-z-dropdown); }
.cbi-dropdown[open] > ul.dropdown {
/* what makes it a popup at all — out of flow, stacked from the --fs-z-* scale;
* absorbed with the machinery */
display: block;
position: absolute;
z-index: var(--fs-z-dropdown);
transition: max-height var(--fs-dur-move);
overflow-y: auto;
background: var(--fs-panel);
border: 1px solid var(--fs-border);
border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow-pop);
padding: var(--fs-space-1-5);
/* the gap that detaches the popover from its control. It was DEAD until the `ul` margin
* flags above came out (they out-ranked it from a lower specificity), so the list sat 1px
* off the control — the one value that moved when they went. */
margin-top: var(--fs-space-1-5);
}
.cbi-dropdown[open] > ul.dropdown > li {
border-radius: var(--fs-radius-sm); padding: var(--fs-space-2) var(--fs-space-3);
border-inline-start: 0; color: var(--fs-text);
position: relative;
/* An OPEN row wraps; only the closed preview ellipsizes. `.cbi-dropdown > ul > li` (0,1,2)
* sets nowrap + overflow:hidden + ellipsis, which is right for the collapsed control — one
* line showing the current value — and wrong for the menu: the list is capped at
* min(92vw, 420px) (90-responsive.css), so an option that carries a description ran out of
* box and was cut mid-word. Measured on Network → Firewall, "Hardware flow offloading":
* "Requires hardware NAT support…" ended at the box edge with no ellipsis, because the cut
* fell inside a nested element (issue #15). Raising the cap instead would let one verbose
* option decide the width of every menu on the page. */
white-space: normal; overflow-wrap: anywhere;
/* no border-bottom divider: on a rounded box it curls up at the corners. The straight
* inset strip below (::after) is the divider, so the hover pill stays rounded. */
border-bottom: 0;
}
.cbi-dropdown[open] > ul.dropdown > li:not(:last-child)::after {
content: ""; position: absolute; inset-inline: 11px; bottom: 0;
height: 1px; background: var(--fs-border);
}
.cbi-dropdown[open] > ul.dropdown > li:hover,
.cbi-dropdown[open] > ul.dropdown > li.focus { background: var(--fs-panel2); color: var(--fs-accent); }
/* After hover on purpose: hovering the selected row must still read as selected.
* The surface is OPAQUE (--fs-panel2) and the accent is carried by the inset rail, not by a
* tint of itself: accent text on --fs-accent-soft (a translucent tint of the accent) measured
* 4.21:1 in dark, under the 4.5:1 AA floor — the tint drags the background toward the text and
* eats its own contrast. axe never saw it because no gallery case rendered an OPEN dropdown
* with a chosen value; the widget shipped that way on every dark router. */
.cbi-dropdown[open] > ul.dropdown > li[selected] {
/* @mirror selected-row/paint */
background: var(--fs-panel2); color: var(--fs-accent); box-shadow: inset 2px 0 0 var(--fs-accent);
/* @endmirror */
}
/* THE FOCUSED ROW WEARS THE THEME'S RING, not the browser's.
*
* ui.js focuses a row as it opens the menu (the first selected one, or the first row), and the
* UA answers a programmatic focus with `outline: auto` — Chrome's 1px system-blue ring, drawn
* OUTSIDE the row and following its 8px radius, so opening a dropdown that already has a value
* flashed a loud blue capsule around the top row on every single-select and multi-select on the
* router. Every other control in this theme wears --fs-focus-ring; this row wore whatever the
* browser shipped.
*
* Inset rather than the token's outer 3px: the menu clips its own overflow, so an outer ring is
* cut on the first and last rows. The rail is restated INSIDE this ring so a focused row that is
* also selected keeps its rail — a shorthand cannot merge with the one below.
*
* Both selectors carry `[selected]`-level weight on purpose: the selected-row paint below is
* (0,4,1) and a bare `li:focus-visible` ties with it, which would leave the focus ring to source
* order. `docs/conventions.md`: win on specificity, never on order. */
.cbi-dropdown[open] > ul.dropdown > li[selected]:focus-visible,
.cbi-dropdown[open] > ul.dropdown > li:focus-visible {
outline: none;
box-shadow: inset 2px 0 0 var(--fs-accent), inset 0 0 0 2px var(--fs-accent-soft);
}
/* A MULTI-select row says "chosen" with its CHECKBOX, so it does not also need the rail.
*
* The rail is `inset 2px 0 0` on a row with an 8px radius, so it is drawn as an ARC, not a bar —
* fine as the single-select cue it was designed for, one row at a time, and wrong down a column
* of them: three chosen bands rendered three blue crescents stacked against the menu's edge and
* read as a broken frame (reported against luci-app-modeminfo's Bands page). The checkbox is a
* non-colour cue in its own right, which is what WCAG 2.2 SC 1.4.1 asks of "selected" — a
* single-select row has none, and keeps the rail.
*
* Written as an override rather than by narrowing the paint below, because those three
* declarations are pinned byte-identical to `select option:checked` in theme/60-inputs.css
* (tools/mirror.mjs) — a native `<select multiple>` option has no checkbox, so the rail is still
* the only cue there and the two must not diverge. `:not(:focus-visible)` keeps this off the
* focused row, so the ring above owns that state alone instead of tying with it. */
.cbi-dropdown[multiple][open] > ul.dropdown > li[selected]:not(:focus-visible) { box-shadow: none; }
.cbi-dropdown[multiple][open] > ul.dropdown > li[selected]:focus-visible {
box-shadow: inset 0 0 0 2px var(--fs-accent-soft);
}
.cbi-dropdown[open] > ul.dropdown > li.create-item { border-top: 1px solid var(--fs-border); margin-top: var(--fs-space-1); }
/* The dropdown's "add item" field is a text input and takes the same radius as every other
* one (theme/60-inputs.css). It said 8px here, and won or lost on source order. */
.cbi-dropdown .create-item-input { background: var(--fs-panel2); border: 1px solid var(--fs-border); border-radius: var(--fs-radius); color: var(--fs-text); }
/* dropdown add-item (e.g. Community Lists) — the .cbi-dropdown styles itself, so the
* wrapper stays transparent: no double frame. */
.cbi-dynlist > .add-item:has(> .cbi-dropdown) {
background: none; border: 0; overflow: visible; box-shadow: none;
}
.cbi-section-table .td .cbi-dropdown,
.cbi-section-table .td .cbi-input-select,
.cbi-section-table .td select,
.cbi-section-table .td input[type="text"] {
min-width: 0;
max-width: none;
width: 100%;
}
/* Below ~960px of available width a multi-dropdown row no longer fits — card each config row.
* Same shape as the fs-dt stack (theme/30-tables.css): label ABOVE its cell, a read-only cell
* half the row, a widget cell the whole width — so an OpenVPN instance (6 columns) folds into
* 3 short lines instead of 6 right-flushed label:value ones.
*
* A @container, NOT the measurement the data tables use, and it must stay one: these rows are
* full of widgets that bake in a width from the layout they were rendered in, so un-collapsing
* one to take a reading CHANGES what is read — on the router the firewall zone table then
* claimed 1747px where it really needs 1190px, and overflowed its section by 557px. Do NOT
* "finish the job" by measuring it. */
@container fs-content (max-width: 960px) {
.table.cbi-section-table,
.table.cbi-section-table .tbody,
.table.cbi-section-table .td { display: block; width: auto; }
/* Hide the header in BOTH markups — a card repeats every label, so the header is a duplicate
* that only ran off the right edge. form.js wraps titles+descr in a `.thead`; the Lua CBI
* half the third-party apps still render through (luci-app-openvpn) emits a bare
* `tr.cbi-section-table-titles` with no .thead, which is why hiding only `.thead` left the
* OpenVPN header on screen. `.tr.table-titles` is deliberately NOT listed: it comes from
* L.ui.Table, which never renders a .cbi-section-table — that header is hidden by the fs-dt
* stack in theme/30-tables.css. */
.table.cbi-section-table .thead,
.table.cbi-section-table .tr.cbi-section-table-titles,
.table.cbi-section-table .tr.cbi-section-table-descr { display: none; }
/* no align-items: flex-start — stretch keeps a pair's cells (and their separators) the
* same height when one wraps; see the data card's .tr in theme/30-tables.css. */
.table.cbi-section-table .tr {
display: flex; flex-wrap: wrap; width: auto;
border: 1px solid var(--fs-border);
border-radius: var(--fs-radius-lg);
background: var(--fs-panel2);
padding: var(--fs-space-2) var(--fs-space-3-5) var(--fs-space-3);
margin-bottom: var(--fs-space-3);
}
.table.cbi-section-table .tr:hover { background: var(--fs-panel2); }
/* `.table.cbi-section-table`, not `.cbi-section-table`: the desktop cell padding is written as
* `.table.cbi-section-table .td` (theme/30-tables.css) and outranks a class-lighter selector,
* so a plainer one here silently kept the desktop padding.
*
* margin: 0 is load-bearing. The Lua CBI gives a table CELL the same `.cbi-value-field` class
* a FORM field carries, and base indents that class by 20px. A stacked cell then measured
* 50% + 20px, two no longer fit on a line, and each pair-cell wrapped to its own — which is
* why the OpenVPN row stayed one column deep. */
.table.cbi-section-table .td {
flex: 1 1 100%; min-width: 0; margin: 0;
padding-block: var(--fs-space-1-5); padding-inline: 0 var(--fs-space-2-5); border: 0; text-align: start;
/* same as the data card's cells (theme/30-tables.css): a carded cell holds opaque
* tokens (a real-length IPv6 GUA is 39 chars with no break point — colons are not
* break opportunities), and without `anywhere` one ran 156px past a 360px viewport
* on the Interfaces page. overflow-wrap inherits, so the inner spans get it too;
* white-space beats the `.nowrap` class LuCI puts on some cells. */
overflow-wrap: anywhere; white-space: normal;
}
/* A cell that only READS a value (dummy text, a flag, one button) is short, so two fit on a
* line; anything with a real widget keeps the full width (the 100% basis above) — a dropdown
* at half a phone's width is unusable. Both spellings of each widget: `CBI.DummyValue` from
* form.js, `dvalue` from the Lua CBI's typename. */
.table.cbi-section-table .td[data-widget="CBI.DummyValue"],
.table.cbi-section-table .td[data-widget="CBI.FlagValue"],
.table.cbi-section-table .td[data-widget="CBI.Button"],
.table.cbi-section-table .td[data-widget="dvalue"],
.table.cbi-section-table .td[data-widget="fvalue"],
.table.cbi-section-table .td[data-widget="button"] { flex-basis: 50%; }
/* PINNED to the data table's copy in theme/30-tables.css: the same card, needed under a CLASS
* there (.fs-stacked, measured) and under this @container here, and CSS cannot OR the two
* guards. tools/mirror.mjs holds the copies byte-identical — a duplicate detector goes QUIET
* once they differ, exactly when it should shout. */
.table.cbi-section-table .td[data-title]::before {
/* @mirror table-card/label */
content: attr(data-title); display: block; margin-bottom: var(--fs-space-1);
font-size: var(--fs-type-xs); font-weight: var(--fs-eyebrow-weight); letter-spacing: var(--fs-eyebrow-tracking);
text-transform: uppercase; color: var(--fs-eyebrow-color);
/* @endmirror */
}
/* The row's buttons are not a label/value pair: full width, each button stretching to
* share the line (Edit | Delete). PINNED — see above. (These six declarations were the
* data table's six in a DIFFERENT ORDER, so a grep could not tell they were one rule.) */
.table.cbi-section-table .td.cbi-section-actions {
/* @mirror table-card/actions */
display: flex; flex: 1 1 100%; justify-content: flex-start;
flex-wrap: wrap; gap: var(--fs-space-2); padding-top: var(--fs-space-2);
/* @endmirror */
}
.table.cbi-section-table .td.cbi-section-actions::before { content: none; }
/* flex-wrap: LuCI groups the row's buttons in ONE inner <div>, so the td's own wrap
* (above) never fires — the div is a single flex item. Four buttons are ~330px; on a
* 320px phone the carded interfaces row overflowed its section by 17px. PINNED to the
* data table's copy (theme/30-tables.css), which hit the same trap on Startup. */
.table.cbi-section-table .td.cbi-section-actions > * {
/* @mirror table-card/actions-inner */
flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: var(--fs-space-2);
/* @endmirror */
}
.table.cbi-section-table .td .cbi-dropdown,
.table.cbi-section-table .td select,
.table.cbi-section-table .td input[type="text"] { width: 100%; min-width: 0; }
.table.cbi-section-table .td .zone-forwards { width: 100%; }
}
/* ---- widget popups must escape the table card ----
* The standalone-table card (theme/30-tables.css) clips row backgrounds to its rounded corners
* with `overflow: hidden`. But a form table (Diagnostics: three command widgets in one row) also
* holds .cbi-dropdown widgets whose open list is absolutely positioned — the clip cropped it at
* the table's edge and dragged the table's box around with it. So drop the clip where a widget
* lives; the `.cbi-dropdown[open]` z-index above lifts the list over the following (unpositioned)
* cards.
*
* Dropping it squares the frame off, though: a real <table> inherits `border-collapse: collapse`,
* which IGNORES border-radius — the card's corners came from the clip, not the border. The
* separated model restores them, and at zero spacing it lays out identically. */
.table:has(.cbi-dropdown),
.table:has(.cbi-dynlist) {
overflow: visible;
border-collapse: separate;
border-spacing: 0;
}
}