mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-11 02:44:57 +08:00
821 lines
48 KiB
CSS
821 lines
48 KiB
CSS
@layer theme {
|
||
/* Value rows.
|
||
* display:flex + align-items:baseline come from base — do not restate, and do not switch to
|
||
* center: a row's field can be taller than its control when a `.cbi-value-description` sits
|
||
* under it, and center then pushes the label to the middle of control+description, visibly
|
||
* below the control. baseline lines the label up with the field's first line for every
|
||
* widget. */
|
||
.cbi-value { justify-content: space-between; gap: var(--fs-space-4); padding: var(--fs-space-2) 0; margin: 0; border-bottom: var(--fs-hairline); }
|
||
/* stacked controls in a field (Local Time: date input + Sync buttons) need breathing room —
|
||
* the raw <br> collapses them. Exclude the password reveal group: it must stay seamless. */
|
||
.cbi-value-field > .control-group { margin-top: var(--fs-space-2); }
|
||
.cbi-value-field > .control-group:not(:has(.cbi-input-password)) {
|
||
display: inline-flex; flex-wrap: wrap; gap: var(--fs-space-2); align-items: center;
|
||
}
|
||
/* The hairline separates one value row from the NEXT one, so it is dropped whenever no value row
|
||
* follows — not only at the end of the section. A section ending with a table or an action bar
|
||
* otherwise draws the line across the top of whatever comes next, cutting through a framed
|
||
* table's rounded corner. `:last-child` was the special case of this. */
|
||
.cbi-value:not(:has(+ .cbi-value)) { border-bottom: 0; }
|
||
/* A toggle/checkbox field has no text baseline: `.cbi-checkbox` is inline-flex with no line box,
|
||
* so its synthesised baseline is the box's BOTTOM edge and align-items:baseline floats the
|
||
* toggle ~5px above the label. Centre those rows instead — but only where no help sentence sits
|
||
* under the control, since centring then drops the label to the middle of control+description,
|
||
* the regression the comment above warns about. */
|
||
.cbi-value:not(:has(.cbi-value-description)):has(> .cbi-value-field > .cbi-checkbox, > .cbi-value-field > div > .cbi-radio) {
|
||
align-items: center;
|
||
}
|
||
/* The generic control transition — absorbed from base's transition group. ONLY the
|
||
* transition: control typography stays in base on purpose (see the note there — as a
|
||
* bare-element theme rule it out-layered `.zonebadge-empty`'s smaller font, measured
|
||
* 10.8 -> 13px on the firewall). border-COLOR, not `border`: the shorthand animates
|
||
* border-width too — a layout animation on every hover instead of a paint one. */
|
||
input, button {
|
||
transition: var(--fs-field-transition);
|
||
}
|
||
|
||
/* keep base's padding-top (--fs-space-1) — it is tuned for base's align-items: baseline. */
|
||
.cbi-value-title {
|
||
color: var(--fs-dim); font-size: var(--fs-type); font-weight: var(--fs-weight);
|
||
/* stated, not inherited: the control rule above hands every label `line-height: normal`,
|
||
* and this one is TEXT — dropping it silently hands the label the font's own default
|
||
* (measured: 18px -> normal, off the scale entirely). Absorbed from base with the rule
|
||
* that made it necessary. */
|
||
line-height: var(--fs-leading);
|
||
}
|
||
/* Mono is for the VALUES a field holds. The exclusions:
|
||
* buttons are chrome, not data — excluded here rather than relying on 55-buttons.css being
|
||
* concatenated later. `:where()` keeps the exclusion at zero specificity, so the <select>
|
||
* rule below still wins its own font.
|
||
* .cbi-value-description is LuCI's inline HELP SENTENCE and lives inside .cbi-value-field.
|
||
* `strong`/`b` — LuCI writes `<strong>MAC:</strong> ac:1f:6b:…`, so the strong is the LABEL
|
||
* and the text after it the value. Feeding the label to the mono rule makes it mono AND
|
||
* weight 700, a face of its own: 227 elements over seven pages in bold mono, and 20 KB of a
|
||
* 600-weight mono face fetched to draw them.
|
||
* The label rule itself is in theme/45-misc.css and must ASSIGN the sans face: a <strong> merely
|
||
* excluded here still INHERITS mono from .cbi-value-field — excluding it alone was measured and
|
||
* changed nothing. */
|
||
.cbi-value-field,
|
||
.cbi-value-field *:where(:not(.btn, .cbi-button, .cbi-value-description)) { font-family: var(--fs-font-mono); }
|
||
/* text-wrap: the help sentence is a <div>, so base's prose rule (which reaches `p` and the
|
||
* section/map descriptions) never covered it — and this is the one that wraps most, since it
|
||
* sits in the field column. Same reasoning as there. */
|
||
.cbi-value-description { font-family: var(--fs-font-sans); text-wrap: pretty; }
|
||
|
||
/* A text field in a CBI form is not a fixed 210px: that width is base's, and the same 210px
|
||
* holds far less here — the field is monospaced and padded --fs-space-3 a side instead of 4,
|
||
* so a server URL was clipped mid-domain while the .cbi-dynlist directly under it is 440px
|
||
* wide (issue #5). Elastic, with the dynlist's own
|
||
* ceiling so the two line up. Number/date/time keep the fixed width: they hold short values, and
|
||
* a 440px box for a port number reads as a mistake. */
|
||
.cbi-value-field input[type="text"],
|
||
.cbi-value-field input[type="url"],
|
||
.cbi-value-field input[type="search"] { width: 100%; max-width: var(--fs-field-max); }
|
||
/* A .cbi-value is designed to hold ONE title+field pair, and base lays it out as a flex row. Some
|
||
* apps cram several pairs into one .cbi-value, and the flex row then flows them side by side
|
||
* until the fields overflow the card. Give each title and each field its own full-width row —
|
||
* the stacked layout base already uses on a phone — scoped by `:has()` to the abnormal
|
||
* multi-PAIR case only, so every normal single-field row keeps its label-beside-field layout.
|
||
*
|
||
* The test is the second TITLE, not the second field, and that is the whole of this rule's
|
||
* safety: `:has()` is live, so keying on a second field means any app that appends one at
|
||
* runtime — a status line, an output pane, a spinner holder — flips the row from "label beside
|
||
* field" to "everything stacked" under the user's finger. Measured: pressing a button in such a
|
||
* row moved that button 31px down. */
|
||
.cbi-value:has(.cbi-value-title ~ .cbi-value-title) > .cbi-value-title,
|
||
.cbi-value:has(.cbi-value-title ~ .cbi-value-title) > .cbi-value-field {
|
||
flex: 1 1 100%;
|
||
margin-inline-start: 0;
|
||
text-align: start;
|
||
}
|
||
/* fields inside a table cell fill it — moved from base with the generic 210px box below:
|
||
* in base these beat the box in-layer, left behind they would lose to it by layer */
|
||
.td > input[type="text"],
|
||
.td > input[type="password"],
|
||
.td > select,
|
||
.td > .cbi-dropdown:not(.btn):not(.cbi-button),
|
||
.cbi-dynlist > .add-item > .cbi-dropdown { width: 100%; }
|
||
.cbi-section-create > * > input { width: 100%; }
|
||
|
||
/* --- live components that bypassed the theme --- */
|
||
/* A CBI row is a <div class="cbi-value"> on a form and a <td class="cbi-value-field"> in a
|
||
* section table — same <var>, and the theme had claimed only the table half. */
|
||
div.cbi-value var, .td.cbi-value-field var { color: var(--fs-accent); }
|
||
.td.cbi-value-field var.cbi-tooltip-container { border-bottom-color: var(--fs-accent); }
|
||
.cbi-filebrowser { border-color: var(--fs-border); background: var(--fs-panel); border-radius: var(--fs-radius-lg); }
|
||
.cbi-filebrowser > ul > li:hover { background: var(--fs-panel2); }
|
||
.cbi-filebrowser .upload { border-top-color: var(--fs-border); }
|
||
|
||
/* A file-browser row must hold its own buttons, and the row CLIPS what does not fit rather than
|
||
* showing it. base sizes the row by proportion (name `flex: 10`, actions `flex: 3`), which is
|
||
* sound for base's own ~20px buttons in a text-height row; this theme's button is 36px tall with
|
||
* 14px of side padding and a file row carries up to three of them, so at 23% of the row they do
|
||
* not fit — and `.cbi-filebrowser > *` is `overflow: hidden`, so a button touches the clip box
|
||
* on both axes and loses its rounded corners, which reads as a broken button.
|
||
*
|
||
* So: size the action column to its CONTENT and let the name give way (a name can ellipsize, a
|
||
* button cannot), give the children a real gutter, and space the rows. It wraps rather than
|
||
* clips when even that is not enough. */
|
||
.cbi-filebrowser > * { border-bottom-color: var(--fs-border); padding: var(--fs-space-1-5) var(--fs-space-2); }
|
||
.cbi-filebrowser > ul { display: flex; flex-direction: column; gap: var(--fs-space-1); }
|
||
.cbi-filebrowser > ul > li { align-items: center; flex-wrap: wrap; gap: var(--fs-space-2); }
|
||
.cbi-filebrowser > ul > li > div:first-child { flex: 1 1 auto; min-width: 0; }
|
||
/* `0 1 auto`, not `0 0 auto`: the actions block is three buttons (Deselect / Download / Delete) and
|
||
* it may WRAP but may not shrink, so on a 320px screen it kept its one-line width and stuck 23px
|
||
* out of the browser box — measured in the gallery at 320px, where `Delete` ended past the card.
|
||
* Shrinking is what lets its own `flex-wrap` fire; `min-width: 0` is what lets shrinking reach
|
||
* below the buttons' min-content, which is the pair every flex item in this theme needs. */
|
||
.cbi-filebrowser > ul > li > div:last-child {
|
||
flex: 0 1 auto; min-width: 0;
|
||
display: flex; flex-wrap: wrap; gap: var(--fs-space-2); justify-content: flex-end;
|
||
}
|
||
.cbi-filebrowser > ul > li > div:last-child > button { margin: 0; }
|
||
/* moved from base with the generic 210px box (same layer arithmetic as the .td rules) */
|
||
.cbi-filebrowser .upload > div > input { width: 100%; }
|
||
|
||
/* Form widgets — toggles, file input. Applies to every CBI form. */
|
||
|
||
/* A raw checkbox inside a button-styled label sat flush against its text: base zeroes the
|
||
* checkbox margin (30-forms.css) and only a literal space separated the ✓ from the words
|
||
* (issue #5.1 — attendedsysupgrade's "Keep settings" confirm: label.btn > input[type=checkbox]
|
||
* + text). Restore a gap. The toggle-switch .cbi-checkbox below is unaffected — its input is
|
||
* opacity:0, so this margin moves nothing there. */
|
||
.btn > input[type="checkbox"], .btn > input[type="radio"] { margin-inline-end: .5em; }
|
||
|
||
/* checkbox -> toggle switch
|
||
*
|
||
* The pill, the knob and the knob's travel are all on --fs-density-box. As bare literals they
|
||
* make LuCI's most common control the only one in the theme that ignores the Density axis, while
|
||
* every button and field around it moves: measured, the switch held 40x22 at Compact and at
|
||
* Large while the fields beside it went 38 -> 32.3 -> 43.7. The switch is a box that holds a
|
||
* knob, which is the
|
||
* box tier by definition. Local names rather than new global tokens: one component needs one
|
||
* number and three things derived from it.
|
||
*
|
||
* Scoped to `.cbi-checkbox > label[for]`, not a bare `.cbi-checkbox > label`: ui.js's
|
||
* Checkbox.render() appends a SECOND label when `tooltip` is set (form.Flag.tooltip — e.g.
|
||
* luci-app-firewall's zones.js on `masq`), `label.cbi-tooltip-container` holding the hint icon,
|
||
* and the bare selector painted it as a second switch too — measured on the gallery's tooltip
|
||
* fixture, both labels 40x22 at rgb(55,62,71), flush against each other with no gap. `[for]` is
|
||
* the discriminator rather than `input[type="checkbox"] + label`: Checkbox.render() always
|
||
* builds the switch label as `E('label', { 'for': id })` and the tooltip label with no `for` at
|
||
* all, so the attribute alone tells the two apart, at a fifth of the bytes — the sibling
|
||
* combinator repeated across five rules (this one, its ::before/::after, and the two
|
||
* forced-colors rules in 95-a11y-media.css) put cascade.css 186 B over its budget; rescoping
|
||
* dropped it back under. */
|
||
.cbi-checkbox { display: inline-flex; align-items: center; gap: var(--fs-space-2); }
|
||
.cbi-checkbox > input[type="checkbox"] { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; }
|
||
.cbi-checkbox > label[for] {
|
||
--sw-w: calc(40px * var(--fs-density-box));
|
||
--sw-h: calc(22px * var(--fs-density-box));
|
||
--sw-knob: calc(16px * var(--fs-density-box));
|
||
--sw-pad: calc(2px * var(--fs-density-box));
|
||
display: inline-block; position: relative; margin: 0;
|
||
width: var(--sw-w); height: var(--sw-h); border-radius: var(--fs-radius-pill);
|
||
background: var(--fs-track); border: var(--fs-hairline);
|
||
cursor: pointer; transition: background var(--fs-dur), border-color var(--fs-dur); vertical-align: middle;
|
||
}
|
||
/* The input is opacity:0 at 0x0, so this label IS the whole hit box of LuCI's most common
|
||
* control — and at 22px tall it sits under the 24px floor of WCAG 2.5.8. The floor is about the
|
||
* CLICKABLE area, not the drawing: an invisible overlay stretches the target to 44x30 without
|
||
* fattening the pill (a visual 44x24 was tried and looked bloated). position:absolute, so it
|
||
* adds nothing to layout; the knob keeps ::after.
|
||
*
|
||
* The inset stays literal while the pill above scales: this is an accessibility floor, and a
|
||
* floor that shrinks with a density setting is not one. At Compact the target is still 38x26.7,
|
||
* over the minimum. */
|
||
.cbi-checkbox > label[for]::before {
|
||
content: ""; position: absolute; inset: -4px -2px;
|
||
}
|
||
.cbi-checkbox > label[for]::after {
|
||
content: ""; position: absolute; top: var(--sw-pad); inset-inline-start: var(--sw-pad);
|
||
width: var(--sw-knob); height: var(--sw-knob); border-radius: 50%;
|
||
background: var(--fs-dim);
|
||
/* transform, not `left`: `left` is a layout property, so the knob re-laid out the row on
|
||
* every frame of the slide. translateX is composited. */
|
||
transform: translateX(0);
|
||
transition: transform var(--fs-dur-move), background var(--fs-dur);
|
||
}
|
||
/* The tooltip-container label (above) is now unstyled by the switch rules, so it falls back to
|
||
* the browser's plain inline label plus base's `cursor: help` — the icon glyph, no pill. It still
|
||
* needs a gap from the switch, and `.cbi-checkbox`'s own `gap` above gives it one instead of a
|
||
* margin declared here: the checkbox input inside that flex row is `position: absolute`, so it is
|
||
* never a flex item, and `gap` only ever opens BETWEEN flex items — an ordinary `.cbi-checkbox`
|
||
* with just the switch label has one flex item and gets no gap at all (measured: cb1's box is
|
||
* still 40px wide, unchanged), while the tooltip fixture's two labels get exactly one. --fs-space-2
|
||
* (8px), same number the removed margin used — the gap the theme already uses beside an inline
|
||
* checkbox glyph (`.btn > input[type="checkbox"]` above) — measured on the gallery fixture: top
|
||
* edges 0.5px apart (the icon glyph's own line box, not this gap) and left-to-right edge 8px,
|
||
* both unchanged from before this rescope. */
|
||
.cbi-checkbox > input[type="checkbox"]:checked + label { background: var(--fs-accent); border-color: transparent; }
|
||
/* The travel is derived rather than hand-written, so it cannot drift out of step with the three
|
||
* sizes above. The `- 2px` is the pill's two 1px BORDERS and is the whole subtlety: --sw-w is a
|
||
* border-box width, while an absolutely-positioned ::after is placed against the PADDING box, so
|
||
* the knob has 2px less room and deriving without that term lands it off-centre — the travel
|
||
* comes out 20px where symmetry wants 18. The border does not scale with density, so the term
|
||
* stays literal. */
|
||
.cbi-checkbox > input[type="checkbox"]:checked + label::after {
|
||
transform: translateX(calc(var(--sw-w) - var(--sw-knob) - var(--sw-pad) * 2 - 2px));
|
||
background: var(--fs-on-accent);
|
||
}
|
||
/* The real checkbox is opacity:0 at 0x0 above, so base's `input:focus::before` glow paints on a
|
||
* 0x0 box. The <label> IS the switch, so the ring goes there, or the most common control in LuCI
|
||
* shows nothing under keyboard focus.
|
||
*
|
||
* -solo, because on the CHECKED switch — half the switches on any page — neither other channel
|
||
* exists: the rule above fills the track with --fs-accent and sets `border-color: transparent`,
|
||
* so the border flip is accent on accent (measured: fill rgb(86,157,245) against a border of the
|
||
* same accent at .97 alpha) and the tinted ring is accent at .15 alpha. */
|
||
.cbi-checkbox > input[type="checkbox"]:focus-visible + label {
|
||
border-color: var(--fs-accent);
|
||
box-shadow: var(--fs-focus-ring-solo);
|
||
}
|
||
.cbi-checkbox > input[type="checkbox"]:disabled + label { opacity: var(--fs-disabled-opacity); cursor: not-allowed; }
|
||
|
||
/* Raw checkbox/radio — the drawing, absorbed from base whole: it undoes the generic `input` box
|
||
* below, and left in base every one of those overrides would lose to that rule by layer. The box
|
||
* is 16px on the grid and the tick or dot is inset FROM it rather than restating its own number,
|
||
* so resizing the box cannot leave the mark off-centre. LuCI's own forms wrap checkboxes in the
|
||
* .cbi-checkbox toggle above; the raw drawing is what a third-party app's bare markup gets. */
|
||
input[type="checkbox"],
|
||
input[type="radio"] {
|
||
--bd-color: var(--fs-border);
|
||
--fg-color: var(--fs-text);
|
||
/* 16px on the grid, but on the BOX tier, not the space tier. `var(--fs-space-4)` was the
|
||
* same 16px at Normal and the wrong multiplier everywhere else: space moves to .65 at
|
||
* Compact and stays 1 at Large, so the tick box went to 10.4px (smaller than any other
|
||
* control on the page — measured) and then refused to grow at Large. A checkbox is a box
|
||
* that has to HOLD an icon, which is what --fs-density-box is defined for. */
|
||
--box: calc(16px * var(--fs-density-box));
|
||
--mark-inset: 3px;
|
||
/* ---- the drawing is 16px; the target is 24px ----
|
||
*
|
||
* WCAG 2.2 SC 2.5.8 lets an undersized target pass on SPACING — 24px centre to centre — which
|
||
* is what a 16px tick box has always relied on. That holds while the theme decides the layout
|
||
* and stops the moment a third-party app lays out its own rows, putting this checkbox and a
|
||
* link inside the 24px. A target that passes only because of somebody else's markup is not a
|
||
* target that passes.
|
||
*
|
||
* So the ELEMENT is 24px and the drawing stays 16px, centred inside it: the two pseudo
|
||
* elements are already absolutely positioned, so centring costs `inset: 0; margin: auto`.
|
||
* Nothing moves visually — the box, the tick and the focus ring are all drawn by the pseudo
|
||
* elements, and the element itself has neither background nor border.
|
||
*
|
||
* `max()`, not a flat 24px: the Large density scales `--box` to 18.4px, and a target must
|
||
* never SHRINK when the user asks for a bigger UI. */
|
||
--hit: max(24px, var(--box));
|
||
appearance: none;
|
||
width: var(--hit);
|
||
height: var(--hit);
|
||
/* …and the target does not cost a row its height: the element is 24px but occupies its old
|
||
* `--box` in layout, the negative margin pulling the extra area back out of the flow so the
|
||
* box overflows itself symmetrically. Measured on a third-party listing: rows went 42px ->
|
||
* 51px with the target simply grown, and 43px with the margin, the target still answering to
|
||
* a hit test at its centre. */
|
||
margin: calc((var(--box) - var(--hit)) / 2);
|
||
color: var(--fg-color);
|
||
position: relative;
|
||
display: inline-block;
|
||
cursor: pointer;
|
||
background: none;
|
||
border: none;
|
||
/* padding/border-radius are NOT reset: the generic input box's values land on an
|
||
* appearance-none 16px box whose pseudo-elements are absolute — invisible, and what the
|
||
* widget has always computed */
|
||
}
|
||
input[type="checkbox"]::before,
|
||
input[type="checkbox"]::after,
|
||
input[type="radio"]::before,
|
||
input[type="radio"]::after {
|
||
position: absolute;
|
||
content: "";
|
||
}
|
||
input[type="checkbox"]::before,
|
||
input[type="radio"]::before {
|
||
/* centred in the 24px target, and direction-agnostic: `inset: 0; margin: auto` on a box with
|
||
* a fixed size is the one centring that needs no translate and therefore behaves the same in
|
||
* RTL, where an `inset-inline-start: 50%` + `translate: -50%` pair would push the drawing the
|
||
* wrong way. */
|
||
inset: 0;
|
||
margin: auto;
|
||
width: var(--box);
|
||
height: var(--box);
|
||
background: linear-gradient(var(--fs-panel), var(--fs-panel2));
|
||
border: 1px solid var(--bd-color);
|
||
transition: var(--fs-field-transition);
|
||
}
|
||
/* each box states its own radius — one shared 2px re-declared to 50% for the radio would
|
||
* be a same-selector shadow inside this layer */
|
||
input[type="checkbox"]::before { border-radius: 2px; }
|
||
input[type="radio"],
|
||
input[type="radio"]::before {
|
||
border-radius: 50%;
|
||
}
|
||
/* keyboard/click focus rings on the DRAWN box — the theme's one ring token, replacing
|
||
* base's old generic glow; :focus (not :focus-visible) keeps the pre-absorption behaviour
|
||
* of ringing a clicked box. Both states (plain and invalid) are declared once, further
|
||
* down this file, in the selector lists every other same-body ring joins — see the two
|
||
* `--fs-focus-ring`/`--fs-focus-ring-invalid` rules below the text-input group. */
|
||
input[type="checkbox"]::after,
|
||
input[type="radio"]::after {
|
||
/* the mark is inset from the DRAWING, and the drawing is centred in the target, so the mark
|
||
* centres the same way rather than measuring from the target's own edge */
|
||
inset: 0;
|
||
margin: auto;
|
||
width: calc(var(--box) - var(--mark-inset) * 2);
|
||
height: calc(var(--box) - var(--mark-inset) * 2);
|
||
}
|
||
input[type="checkbox"]:checked::after,
|
||
input[type="radio"]:checked::after {
|
||
/* OURS. Drawn as a STROKE, which is this theme's icon language (see --spinner-icon in
|
||
* base/95-luci.css and --fs-icon-refresh in 02-tokens.css: fill:none, round caps, a 24 grid).
|
||
* It replaces a solid filled path that was byte-identical to luci-theme-bootstrap's — and to
|
||
* luci-theme-openwrt-2020's, which is the same project's artwork, not evidence the drawing is
|
||
* generic. A mask keys on ALPHA, so a stroked path masks exactly as a filled one did. */
|
||
--checkmark-icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'><path d='M4.5 12.5 9.5 17.5 19.5 6.5'/></svg>");
|
||
-webkit-mask: var(--checkmark-icon) center/cover no-repeat;
|
||
mask: var(--checkmark-icon) center/cover no-repeat;
|
||
background: var(--fg-color);
|
||
}
|
||
input[type="radio"]:checked:after {
|
||
/* the radio's dot — filled, because a ring would read as "not selected". OURS: our grid,
|
||
* our radius (the fill is the one shape a stroke cannot express). */
|
||
--checkmark-icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='6.5' fill='%23000'/></svg>");
|
||
}
|
||
input[type="checkbox"].cbi-input-invalid,
|
||
input[type="radio"].cbi-input-invalid {
|
||
--bd-color: var(--fs-danger);
|
||
--fg-color: var(--fs-danger);
|
||
}
|
||
|
||
/* The generic field box, absorbed from base's `input, textarea, select, …` group; each other
|
||
* member lives with its component rule now. Bare `input` keeps the historical 210px box as the
|
||
* fallback for every type the exhaustive text-input list below does not name — and for a
|
||
* typeless <input>, which IS a text input. The widget types that must NOT wear this box carry
|
||
* their own theme rules: a base override would lose to this rule by layer whatever its
|
||
* specificity. */
|
||
input {
|
||
display: inline-block;
|
||
width: 210px;
|
||
height: var(--fs-control-h);
|
||
padding: var(--fs-space-1) var(--fs-space-2);
|
||
background: var(--fs-panel);
|
||
color: var(--fs-text);
|
||
border: var(--fs-hairline);
|
||
border-radius: var(--fs-radius);
|
||
}
|
||
/* base's old `#localtime { width: 300px }` media rule was NOT moved here: the elastic
|
||
* .cbi-value-field width above already out-layered it (measured on the live router: the
|
||
* field rendered 440px, not 300) — the rule was dead, and a dead rule is deleted, not
|
||
* relocated */
|
||
|
||
/* native file input — the `initial`/auto resets are absorbed from base: the generic box
|
||
* above would otherwise dress the native chooser in a 210px bordered field */
|
||
input[type="file"] {
|
||
color: var(--fs-dim); font-size: var(--fs-type);
|
||
padding: initial;
|
||
border: initial;
|
||
line-height: initial;
|
||
box-shadow: none;
|
||
/* no !important: this out-specifies the generic `input { width: 210px }` on its own.
|
||
* height/background are NOT reset: the chooser keeps the generic box's control height
|
||
* and panel tint, as it always has. */
|
||
width: auto;
|
||
}
|
||
|
||
/* native <select> — custom chevron + theme (matching the text inputs). The chevron data-URI
|
||
* lives in 03-palettes.css per palette/mode block, because a URI cannot read var(); it
|
||
* replaced a hardcoded grey that ignored palette and dark mode. */
|
||
select, select.cbi-input-select, .cbi-value-field select {
|
||
/* unprefixed only: the floor (Chrome 108/Firefox 101/Safari 15.4) clears unprefixed
|
||
* `appearance` everywhere (Chrome 84, Firefox 80, Safari 15.4 — docs/css.md). -webkit- was
|
||
* 96 B across these four rules for nothing this sheet still has to run on. */
|
||
appearance: none;
|
||
background-color: var(--fs-panel2);
|
||
background-image: var(--fs-select-chevron);
|
||
/* right inset from the scale, not a literal 12 — the .cbi-select shell below states the same
|
||
* position and two spellings of one number is how they would drift apart */
|
||
/* two 6px bands forming the V, the far one a band-width further in; --fs-space-3 stays the
|
||
* single spelling of the inset, as the note above says */
|
||
background-repeat: no-repeat; background-size: 6px 6px;
|
||
background-position: right calc(var(--fs-space-3) - 1px) center, right calc(var(--fs-space-3) + 5px) center;
|
||
padding: var(--fs-space-2) var(--fs-space-8) var(--fs-space-2) var(--fs-space-3);
|
||
border: var(--fs-hairline); border-radius: var(--fs-radius);
|
||
color: var(--fs-text); font-family: var(--fs-font-sans); font-size: var(--fs-type);
|
||
line-height: 1.3; min-height: var(--fs-ctl-h); cursor: pointer;
|
||
/* width/display absorbed from base's generic field box: a select's natural width is its
|
||
* longest option, so without the fixed box two selects in one form column ragged-edge;
|
||
* display stated because Chromium's base-select UA default is inline-flex, not the
|
||
* inline-block the widget has always computed */
|
||
width: 210px;
|
||
display: inline-block;
|
||
transition: var(--fs-field-transition);
|
||
}
|
||
/* group-then-refine, directly under its group: the listbox variant must NOT take the closed
|
||
* control's one-line height. A SURFACE token, not an ink one: this once read --fs-on-accent
|
||
* (the ink meant to go ON an accent fill), which resolves to plain white and painted a white
|
||
* listbox in dark mode. `height: inherit` absorbed from base with the width above — without
|
||
* it the fixed box would cap a multi-row listbox at one line. */
|
||
select[multiple] { background-color: var(--fs-panel2); color: var(--fs-text); height: inherit; }
|
||
/* :focus joins the shared accent ring in the text-input group below. */
|
||
/* absorbed from base's reset: 100% (= inherit) keeps the option on its select's size, and
|
||
* only <option> needs the 2em leading — the other controls carry line-height: normal above */
|
||
option { font-size: 100%; line-height: 2em; }
|
||
select option { background: var(--fs-panel); color: var(--fs-text); }
|
||
|
||
/* Native <select> drop list: the OS renders it, so the `select option` colours above are ignored
|
||
* on most platforms. Chromium's customisable-select (`appearance: base-select`) renders the
|
||
* picker in the DOM, so it can be themed like the .cbi-dropdown menu; @supports-gated, and
|
||
* browsers without it keep the styled closed control plus the native list.
|
||
*
|
||
* Scoped to `.cbi-value-field select` only: a third-party app can mount a raw <select> outside
|
||
* the CBI form and drive it its own way, and early Chrome builds mis-render such a select as a
|
||
* single-item list. Those sit outside .cbi-value-field, so this scope leaves them native. */
|
||
@supports (appearance: base-select) {
|
||
.cbi-value-field select { appearance: base-select; }
|
||
.cbi-value-field select::picker-icon { display: none; }
|
||
::picker(select) {
|
||
appearance: base-select;
|
||
background: var(--fs-panel); border: var(--fs-hairline);
|
||
border-radius: var(--fs-radius-lg); box-shadow: var(--fs-shadow-pop);
|
||
padding: var(--fs-space-1-5); margin-top: var(--fs-space-1-5);
|
||
}
|
||
.cbi-value-field select option {
|
||
padding: var(--fs-space-2) var(--fs-space-3); border-radius: var(--fs-radius-sm); font-size: var(--fs-type);
|
||
/* no `color` here: `select option` above already sets --fs-text, and this restated it
|
||
* (switched off against 174 options, nothing moved). `background: transparent` is NOT the
|
||
* same — it deliberately undoes that rule's --fs-panel so the row's own hover shows. */
|
||
background: transparent; min-height: 0;
|
||
}
|
||
.cbi-value-field select option::checkmark { display: none; }
|
||
.cbi-value-field select option:hover, .cbi-value-field select option:focus-visible { background: var(--fs-panel2); }
|
||
/* Opaque surface + inset rail, same shape as the open .cbi-dropdown's selected row
|
||
* (theme/65-dropdown.css): accent text on --fs-accent-soft measured 4.21:1 in dark,
|
||
* under AA — the tint of the accent eats its own contrast. */
|
||
.cbi-value-field select option:checked {
|
||
/* @mirror selected-row/paint */
|
||
background: var(--fs-panel2); color: var(--fs-accent); box-shadow: var(--fs-row-rail);
|
||
/* @endmirror */
|
||
}
|
||
}
|
||
|
||
/* .cbi-select — the legacy luci-compat shell (a div around a native <select>), absorbed from base
|
||
* whole: the generic field box, the shell's own paint and a focus ring on the theme's token. The
|
||
* shell is a DIV, so display and width must be stated. `:not(select)` because the class also
|
||
* appears ON a bare <select>: a replaced element cannot draw the ::before chevron, so shell
|
||
* paint there means a gradient with no glyph. */
|
||
/* It paints like the themed <select>, being the same control in an older shell — never a
|
||
* skeuomorphic surface of its own: a gradient from panel to border is lighter at the bottom in
|
||
* the dark palettes, and nothing else in the theme has a gradient at all. The geometry below is
|
||
* the `select` rule above, property for property, so a legacy shell and a modern select line up
|
||
* in one form column. This is the non-JS fallback: the people who see it already have something
|
||
* broken. */
|
||
.cbi-select:not(select) {
|
||
display: inline-block;
|
||
width: 210px;
|
||
/* `height`, NOT `min-height` — the inner select fills the shell with `height: 100%`, and a
|
||
* percentage against an auto-height parent has no definite containing block to resolve
|
||
* against, so it falls back to content height: measured, the select collapsed 30px -> 15px
|
||
* inside a 38px shell, text at the top and half the hit area gone. The themed `select` can
|
||
* use a floor because it IS the control; a wrapper has to state a height for its child. */
|
||
height: var(--fs-ctl-h);
|
||
color: var(--fs-text);
|
||
font-size: var(--fs-type);
|
||
border: var(--fs-hairline);
|
||
border-radius: var(--fs-radius);
|
||
padding: 0;
|
||
position: relative;
|
||
background-color: var(--fs-panel2);
|
||
background-image: var(--fs-select-chevron);
|
||
background-repeat: no-repeat;
|
||
background-size: 6px 6px;
|
||
background-position: right calc(var(--fs-space-3) - 1px) center, right calc(var(--fs-space-3) + 5px) center;
|
||
transition: var(--fs-field-transition);
|
||
}
|
||
/* The disabled cue used to live ENTIRELY on the ▾ plate: base/30-forms.css dims
|
||
* `.cbi-select[disabled]::before` and never the shell itself. With the plate gone the state had
|
||
* no visual at all, so the shell carries it now. */
|
||
.cbi-select[disabled]:not(select) {
|
||
opacity: var(--fs-disabled-opacity);
|
||
pointer-events: none;
|
||
}
|
||
/* .focus and .cbi-input-invalid.focus join the shared accent/danger ring rules further down
|
||
* this file — same two bodies as every other :focus/:focus-within ring on this token. */
|
||
/* the native <select> inside the shell: invisible, filling it, clearing the ▾ plate on the
|
||
* right. The inset is a scale step; the right margin is the plate's width, so it stays
|
||
* em-relative to the glyph it clears. Every themed-select declaration above (chevron,
|
||
* padding, min-height) must be answered here, or the inner select paints like a free-standing
|
||
* one inside the 32px shell. */
|
||
.cbi-select select,
|
||
.cbi-select select:focus {
|
||
appearance: none;
|
||
outline: none;
|
||
border: none;
|
||
background: transparent;
|
||
background-image: none;
|
||
box-shadow: none;
|
||
height: 100%;
|
||
min-height: 0;
|
||
width: 100%;
|
||
/* clears the CHEVRON, which the shell now paints on its own background — the same
|
||
* --fs-space-8 inset the themed select uses. `margin-right: .6em` used to clear the ▾ plate
|
||
* instead, and an em-relative margin against a px-positioned glyph is how the two drifted. */
|
||
padding: 0 var(--fs-space-8) 0 var(--fs-space-3);
|
||
font-size: var(--fs-type);
|
||
line-height: normal;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* spinner / loading */
|
||
.spinning, .cbi-value em, em.spinning { color: var(--fs-dim); }
|
||
|
||
/* Text inputs and other field widgets, rounded and themed to match the selects and dropdowns
|
||
* (base leaves them at radius 3 and white). Excludes checkbox/radio and file, handled above.
|
||
*
|
||
* The type list must be EXHAUSTIVE: an omission does not fall back to "unstyled", it falls back
|
||
* to BASE, which still paints a stock white 3px-radius field beside a themed one. */
|
||
.cbi-input-text, .cbi-input-password, .cbi-input-textarea,
|
||
.cbi-section-create-name, .cbi-value-field input.create-item-input,
|
||
input[type="text"], input[type="password"], input[type="number"],
|
||
input[type="email"], input[type="url"], input[type="search"],
|
||
input[type="tel"], input[type="date"], input[type="time"],
|
||
input[type="datetime-local"], input[type="month"], input[type="week"],
|
||
input[type="color"],
|
||
textarea {
|
||
background: var(--fs-panel2);
|
||
border: var(--fs-hairline);
|
||
border-radius: var(--fs-radius);
|
||
color: var(--fs-text);
|
||
padding: var(--fs-space-2) var(--fs-space-3);
|
||
min-height: var(--fs-ctl-h);
|
||
box-shadow: none;
|
||
transition: var(--fs-field-transition);
|
||
}
|
||
/* width/font-size absorbed from base's generic field box, same numbers — a textarea has
|
||
* always sat at the fixed 210px (the elastic rule above names only text/url/search) */
|
||
/* `max-width: 100%` beside the width, because a textarea is the one field whose intrinsic width
|
||
* comes from an ATTRIBUTE (`cols`, default 20) rather than from the box: measured at a 300px
|
||
* viewport, two textareas in the gallery stood 19px outside their card while every other control
|
||
* in the same column fitted. The 210px is the designed width and still applies wherever there is
|
||
* room for it. */
|
||
textarea { min-height: 84px; line-height: var(--fs-leading); width: 210px; max-width: 100%; font-size: var(--fs-type); }
|
||
/* A textarea's native scrollbar is a square strip and a rounded corner does not clip it, so on a
|
||
* scrolling config field the grey bar pokes past the radius as a square notch. Fix: a slim
|
||
* self-rounded thumb floated off the edges (transparent border plus background-clip:
|
||
* padding-box), a transparent track, and a track margin keeping the thumb clear of the corners.
|
||
*
|
||
* Deliberately no `scrollbar-width`/`scrollbar-color`: either makes Chromium switch to the
|
||
* standard scrollbar and ignore this whole ::-webkit block — and that standard bar is the
|
||
* square, unclipped one. Firefox has no ::-webkit and clips natively.
|
||
*
|
||
* 16px wide so ::-webkit-resizer, sized off the scrollbar, gets a bigger grab area; the 5px
|
||
* transparent thumb border still leaves it 6px visible. */
|
||
textarea::-webkit-scrollbar { width: 16px; height: 16px; }
|
||
textarea::-webkit-scrollbar-track { background: transparent; margin: var(--fs-space-4) 0 var(--fs-space-2-5); }
|
||
textarea::-webkit-scrollbar-thumb {
|
||
background: var(--fs-dim); border-radius: var(--fs-radius-pill);
|
||
border: 5px solid transparent; background-clip: padding-box;
|
||
}
|
||
textarea::-webkit-scrollbar-thumb:hover { background: var(--fs-text); }
|
||
/* The bottom-right square where the vertical bar meets the resize grip defaulted to white
|
||
* and poked past the rounded corner — transparent, so the frame shows. */
|
||
textarea::-webkit-scrollbar-corner { background: transparent; }
|
||
/* The resize grip highlights the frame's bottom-right corner — an accent arc tracing the frame's
|
||
* own radius, with no straight legs. Two borders plus the corner radius draw the arc, and the
|
||
* legs vanish because the box is shrunk to exactly the radius: the resizer is the scrollbar's
|
||
* 16px, so a `16px − radius` margin leaves a radius-sized box whose right and bottom border IS
|
||
* the quarter curve. (A mask would give a shorter arc, but ::-webkit-resizer ignores mask, and a
|
||
* baked-in SVG colour would not follow the palette.) */
|
||
/* PHYSICAL on purpose, and mirrored by hand below: ::-webkit-resizer is a UA-drawn corner whose
|
||
* side follows the writing direction, and it does not accept logical properties — with only the
|
||
* LTR rule the arc drew on the wrong corner in an RTL document (a stray quarter-circle on the
|
||
* left of every textarea). */
|
||
textarea::-webkit-resizer {
|
||
background: transparent;
|
||
border-right: 2px solid var(--fs-accent);
|
||
border-bottom: 2px solid var(--fs-accent);
|
||
border-bottom-right-radius: var(--fs-radius);
|
||
margin: calc(16px - var(--fs-radius)) 0 0 calc(16px - var(--fs-radius));
|
||
}
|
||
:root[dir="rtl"] textarea::-webkit-resizer {
|
||
border-right: 0; border-left: 2px solid var(--fs-accent);
|
||
border-bottom-right-radius: 0; border-bottom-left-radius: var(--fs-radius);
|
||
margin: calc(16px - var(--fs-radius)) calc(16px - var(--fs-radius)) 0 0;
|
||
}
|
||
|
||
.cbi-input-text:hover, .cbi-input-password:hover, .cbi-input-textarea:hover,
|
||
input[type="text"]:hover, input[type="password"]:hover, input[type="number"]:hover,
|
||
input[type="email"]:hover, input[type="url"]:hover, input[type="search"]:hover,
|
||
textarea:hover { border-color: var(--fs-dim); }
|
||
|
||
/* bare `input:focus` absorbed from base's generic glow onto the same ring: it is the fallback
|
||
* for the types the list does not name and for a typeless <input>; on checkbox/radio it lands
|
||
* on the invisible element box UNDER the drawn ::before ring above — coincident boxes, one
|
||
* visible ring, as the old glow behaved. `select:focus` and the keyboard-removable dynlist
|
||
* chip (ui.js gives it a tabindex) join here rather than repeating the same three
|
||
* declarations under their own selector: one shared body instead of three, 149 B off
|
||
* cascade.css. */
|
||
.cbi-input-text:focus, .cbi-input-password:focus, .cbi-input-textarea:focus,
|
||
.cbi-section-create-name:focus,
|
||
input:focus,
|
||
input[type="text"]:focus, input[type="password"]:focus, input[type="number"]:focus,
|
||
input[type="email"]:focus, input[type="url"]:focus, input[type="search"]:focus,
|
||
input[type="tel"]:focus, input[type="date"]:focus, input[type="time"]:focus,
|
||
textarea:focus,
|
||
select:focus,
|
||
.cbi-dynlist > .item:focus {
|
||
border-color: var(--fs-accent);
|
||
box-shadow: var(--fs-focus-ring);
|
||
outline: none;
|
||
}
|
||
/* The plain accent ring above, restated for four components whose selector cannot join that
|
||
* list without changing what it matches (a pseudo-element, a legacy shell class, two
|
||
* `:focus-within` containers) — same two declarations, one shared body instead of four, 186 B
|
||
* off cascade.css. `:is()` around the `:has()` compound keeps the list forgiving: unwrapped, a
|
||
* `:has()` compound sharing a selector list with plain ones fails the WHOLE rule below the
|
||
* floor (Firefox < 121; docs/css.md "Two shapes fail it"), instead of just losing that one
|
||
* selector. */
|
||
input[type="checkbox"]:focus::before, input[type="radio"]:focus::before,
|
||
.cbi-select.focus,
|
||
.cbi-dynlist > .add-item:focus-within,
|
||
.control-group:is(:has(> .cbi-input-password)):focus-within {
|
||
border-color: var(--fs-accent);
|
||
box-shadow: var(--fs-focus-ring);
|
||
}
|
||
|
||
/* Invalid state keeps the danger cue. `.cbi-value-error input` is the Lua-CBI path and belongs
|
||
* HERE, not in base: base does declare a red border-color for it, but the rule above sets the
|
||
* `border` SHORTHAND, and a shorthand in a later layer wipes the longhand out regardless of
|
||
* specificity — so every third-party app on luci-compat rendered the field in --fs-border grey
|
||
* and had lost its field-error cue while the modern .cbi-input-invalid path worked.
|
||
*
|
||
* `.cbi-input-invalid:focus` is not redundant with the rule it sits beside: the generic
|
||
* `input:focus` above is (0,1,1) and would repaint a focused invalid field accent, and
|
||
* `.cbi-input-invalid:focus` at (0,2,0) is what stops it — the TYPED `:focus` rules above are
|
||
* (0,2,1) and still outrank this, so a typed invalid field focuses accent, exactly as before
|
||
* the absorption. The checkbox/radio `::before` ring and the legacy `.cbi-select` shell need
|
||
* the identical danger body for the same reason on their own elements — one shared rule
|
||
* instead of four, 186 B off cascade.css. */
|
||
input[type="checkbox"].cbi-input-invalid:focus::before,
|
||
input[type="radio"].cbi-input-invalid:focus::before,
|
||
.cbi-select.cbi-input-invalid.focus,
|
||
.cbi-input-invalid, input.cbi-input-invalid, textarea.cbi-input-invalid,
|
||
.cbi-value-error input, .cbi-value-error textarea,
|
||
.cbi-input-invalid:focus {
|
||
border-color: var(--fs-danger);
|
||
box-shadow: var(--fs-focus-ring-invalid);
|
||
}
|
||
/* The same rejected value shown by a ui.Dropdown rather than an <input>: the widget itself and,
|
||
* when it is closed, the <li> that displays the value. base carried these two and the theme only
|
||
* the input, so half of one state lived in a layer the theme cannot reach. */
|
||
.cbi-input-invalid.cbi-dropdown:not(.btn):not(.cbi-button),
|
||
.cbi-input-invalid.cbi-dropdown:not([open]) > ul > li { border-color: var(--fs-danger); }
|
||
|
||
/* readonly keeps its faded border — moved from base with the generic box (its border-color
|
||
* longhand would lose to the box's `border` shorthand by layer). The doubled [readonly] is
|
||
* a deliberate specificity step: at (0,1,1) this ties the typed input rules above and the
|
||
* cascade would rest on file order — in base the same tie made the fade silently LOSE to
|
||
* the typed border, so a readonly text field never faded at all; (0,2,1) states the intent. */
|
||
:is(input, select, textarea)[readonly][readonly] {
|
||
border-color: color-mix(in srgb, var(--fs-border) calc(var(--fs-disabled-opacity) * 100%), transparent);
|
||
pointer-events: auto;
|
||
cursor: auto;
|
||
}
|
||
|
||
/* dynamic list: keep base's layout (inline-flex column of chips + one add-item row) — do NOT
|
||
* set display/flex-wrap here or the add row breaks. Only colours, rounding, a compact +
|
||
* button, and no wrapping box: capped narrow so the chip's × sits near the text instead of
|
||
* floating across a full-width field. */
|
||
.cbi-dynlist {
|
||
gap: var(--fs-space-1-5);
|
||
background: transparent; border: 0; padding: 0;
|
||
/* `min(240px, 100%)`, not a bare 240: the floor is there so a chip's × sits near its text
|
||
* rather than floating across a full-width field, and it becomes a defect the moment the
|
||
* column is narrower than the floor — measured in the gallery at a 320px viewport, the list
|
||
* held 240px inside a 210px field and every chip, the add row and its input stood 29px
|
||
* outside the card. A floor that exceeds the room is not a floor, it is an overflow. */
|
||
min-height: 0; min-width: min(240px, 100%); max-width: var(--fs-field-max); width: 100%;
|
||
vertical-align: top;
|
||
}
|
||
.cbi-dynlist > .item {
|
||
background: var(--fs-panel);
|
||
border: var(--fs-hairline);
|
||
border-radius: var(--fs-radius-sm);
|
||
color: var(--fs-text);
|
||
box-shadow: none;
|
||
margin: 0;
|
||
transition: var(--fs-field-transition);
|
||
}
|
||
/* a chip is keyboard-removable (ui.js gives it a tabindex): :focus joins the shared accent
|
||
* ring in the text-input group above. */
|
||
/* The chip's × plate is a BUTTON in base's paint group (`.item::after, .btn, .cbi-button`) —
|
||
* absorbed with it, bare as base had it (coverage: an .item outside a dynlist keeps its
|
||
* plate). The machinery — content/position/edges/align — stays in base with the widget. */
|
||
.item::after {
|
||
display: inline-block;
|
||
cursor: pointer;
|
||
background: linear-gradient(var(--fs-panel), var(--fs-panel) 25%, var(--fs-panel2));
|
||
padding: 0 var(--fs-space-3);
|
||
color: var(--fs-text);
|
||
line-height: 2em;
|
||
border: var(--fs-hairline);
|
||
border-radius: var(--fs-radius);
|
||
transition: var(--fs-field-transition);
|
||
}
|
||
/* the hover cue is the lift, not a colour: the plate's fill is a gradient (see
|
||
* --fs-hover-lift in 02-tokens.css) — replaces base's old focus-glow-on-hover */
|
||
.item:hover::after { filter: var(--fs-hover-lift); }
|
||
.cbi-dynlist > .item::after {
|
||
/* display/padding absorbed from base's machinery rule: the bare plate above says
|
||
* inline-block / space-3, and by layer it would beat base's refinement */
|
||
display: inline-flex;
|
||
padding: 0 var(--fs-space-2);
|
||
color: var(--fs-danger);
|
||
border-radius: 0 var(--fs-radius-sm) var(--fs-radius-sm) 0;
|
||
}
|
||
/* text add-item = one bordered input group (input + + button attached) */
|
||
.cbi-dynlist > .add-item {
|
||
align-items: stretch;
|
||
background: var(--fs-panel2); border: var(--fs-hairline);
|
||
border-radius: var(--fs-radius-sm); overflow: hidden;
|
||
transition: var(--fs-field-transition);
|
||
}
|
||
/* :focus-within joins the shared accent ring in the text-input group above. */
|
||
.cbi-dynlist > .add-item > input,
|
||
/* `width: auto` keeps the flex basis in charge: this input sits INSIDE .cbi-value-field, so the
|
||
* elastic width above would otherwise hand it 100% of the field on top of `flex: 1 1 auto`. */
|
||
.cbi-dynlist > .add-item > .cbi-input-text {
|
||
flex: 1 1 auto; min-width: 0; width: auto;
|
||
border: 0; background: transparent;
|
||
box-shadow: none; min-height: var(--fs-ctl-h-sm); border-radius: 0;
|
||
}
|
||
|
||
/* password reveal: input + ∗ button as ONE seamless rounded group.
|
||
* `gap: 0` is load-bearing: base gives every .control-group a gap, and this is the one group
|
||
* whose members must touch — the group draws the single border and clips them. */
|
||
.control-group:has(> .cbi-input-password) {
|
||
display: inline-flex; align-items: stretch; gap: 0;
|
||
background: var(--fs-panel2);
|
||
border: var(--fs-hairline);
|
||
border-radius: var(--fs-radius);
|
||
overflow: hidden;
|
||
transition: var(--fs-field-transition);
|
||
}
|
||
.control-group:has(> .cbi-input-password) > .cbi-input-password {
|
||
border: 0; border-radius: 0;
|
||
background: transparent; box-shadow: none;
|
||
min-height: var(--fs-ctl-h);
|
||
}
|
||
/* :focus-within (wrapped in :is() for the shared list) joins the accent ring in the
|
||
* text-input group above. */
|
||
/* base pulls .cbi-section-create up 3px to hug a table border, which glues the "Add" button
|
||
* to the last row — give it a real gap instead. */
|
||
.cbi-section-create, .cbi-section-create.cbi-tblsection-create { margin-top: var(--fs-space-3); }
|
||
|
||
/* `.hidden` must win over every component rule in this layer. An enumerated list
|
||
* (`.cbi-value.hidden`, `.cbi-section.hidden`, …) only ever covers the elements someone has
|
||
* already been bitten by, and the one it misses is the Enter-key submit every legacy Lua page
|
||
* carries: luci-compat opens each form with `<input type="submit" class="hidden">`, and an
|
||
* attribute selector counts as a class, so `input[type="submit"]` at (0,1,1) beats a bare
|
||
* `.hidden` at (0,1,0) and a stray "Save" button floats above the page title (issue #12).
|
||
* Doubling the class states the invariant once at (0,2,0) instead of chasing elements. An
|
||
* `!important` would do it too, and would invert the layer order for every future override. */
|
||
.hidden.hidden { display: none; }
|
||
/* RangeSlider (UIRangeSlider) — the one LuCI widget base leaves to native browser rendering.
|
||
* Token-driven track/thumb. */
|
||
input[type="range"] {
|
||
appearance: none;
|
||
width: 100%; height: 6px; min-height: 0; padding: 0;
|
||
background: var(--fs-track); border: 0; border-radius: var(--fs-radius-pill);
|
||
outline: none; box-shadow: none; cursor: pointer;
|
||
}
|
||
input[type="range"]::-webkit-slider-thumb {
|
||
appearance: none;
|
||
width: 16px; height: 16px; border-radius: 50%;
|
||
background: var(--fs-accent); border: 2px solid var(--fs-panel);
|
||
cursor: pointer;
|
||
}
|
||
input[type="range"]::-moz-range-thumb {
|
||
width: 16px; height: 16px; border-radius: 50%;
|
||
background: var(--fs-accent); border: 2px solid var(--fs-panel); cursor: pointer;
|
||
}
|
||
input[type="range"]::-moz-range-track { background: var(--fs-track); height: 6px; border-radius: var(--fs-radius-pill); }
|
||
/* -solo: the track sets `border: 0` and `outline: none`, so there is no second channel here
|
||
* either — the tinted ring was the whole indicator on a 6px bar. */
|
||
input[type="range"]:focus { box-shadow: var(--fs-focus-ring-solo); }
|
||
/* THE SAME CEILING THE ELASTIC TEXT INPUTS TAKE, and the reason is the whole point of the
|
||
* token: a `.cbi-value-field` is `flex: 1`, so `width: 100%` here meant the slider ate the
|
||
* entire card while the select two rows above stopped at 210px and the readout chip was flung
|
||
* to the card's right edge. Every stretching control in a form row now ends at one x. */
|
||
.cbi-range-slider { display: inline-flex; align-items: center; gap: var(--fs-space-3); width: 100%; max-width: var(--fs-field-max); }
|
||
.cbi-range-slider-value {
|
||
background: var(--fs-panel2); border: var(--fs-hairline); border-radius: var(--fs-radius-sm);
|
||
padding: var(--fs-space-1) var(--fs-space-2); color: var(--fs-accent); font-family: var(--fs-font-mono);
|
||
/* NOT 600: no face is bundled, so 600 either drags a 20 KB file back in or is faked by
|
||
* synthetic emboldening, which smears a monospace grid.
|
||
* The chip already reads as emphatic from its surface, border and accent ink. */
|
||
font-weight: var(--fs-weight-normal); font-size: var(--fs-type); white-space: nowrap;
|
||
}
|
||
.cbi-range-slider-calc, .cbi-range-slider-calc-units { color: var(--fs-dim); font-size: var(--fs-type); }
|
||
}
|