mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-11 02:44:57 +08:00
841 lines
49 KiB
CSS
841 lines
49 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 because a `.cbi-value-description`
|
||
* sits under it, and center then pushed 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 (select, dropdown, toggle, dynlist, textarea). */
|
||
.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;
|
||
}
|
||
.cbi-value:last-child { 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. Under align-items:baseline the row
|
||
* then hangs the toggle from the label's baseline and floats it ~5px ABOVE the label, on every
|
||
* toggle row across the UI. Centre those rows instead — but ONLY when no help sentence sits under
|
||
* the control: with a `.cbi-value-description` present, centring drops the label to the middle
|
||
* of control+description, the exact regression the align-items 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 (Sync with browser, Start/Restart/Stop) are chrome, not data — excluded here
|
||
* rather than relying on theme/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 —
|
||
* this rule was setting prose in JetBrains Mono.
|
||
* `strong`/`b` — the most expensive line in this theme. LuCI writes `<strong>MAC:</strong>
|
||
* ac:1f:6b:…`: the strong is the LABEL, the text after it the value. Feeding the label to
|
||
* the mono rule made it mono AND weight 700, a face of its own — measured on the live
|
||
* router, 227 elements over seven pages in bold mono, and 20 KB of jetbrains-mono-600
|
||
* fetched to draw them: 30% of the whole font payload. Manrope 700 costs zero bytes and
|
||
* label-vs-value now differs by TYPEFACE, not by weight (the 600 mono face is gone, and
|
||
* so is every other bundled face).
|
||
* 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. Measured — excluding alone
|
||
* changed nothing, all 227 still drew bold mono. */
|
||
.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 was a fixed 210px — base's width, straight out of bootstrap's
|
||
* cascade. The same 210px holds far LESS here: the field is monospaced (the rule above) and
|
||
* padded --fs-space-3 a side (12px at the default density) instead of 4, so Attended
|
||
* Sysupgrade's server URL was clipped mid-domain
|
||
* — while the .cbi-dynlist directly under it on that same page is 440px wide, which is what
|
||
* makes the mismatch the thing you actually see (issue #5). Elastic now, 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; base lays it out as a flex row —
|
||
* label (180px) + field. Some apps cram SEVERAL pairs into one .cbi-value
|
||
* (luci-app-3ginfo-lite's "SIM card menu" modal: three title/field pairs in one), and the
|
||
* flex row then flows them side by side until the fields overflow the card — the exact bug
|
||
* a user reported. Give each title and each field its own full-width row (the same stacked
|
||
* layout base already uses on a phone), left-aligned, so nothing overflows and the pairs read
|
||
* top to bottom. Scoped by :has() to the abnormal multi-PAIR case ONLY, so every normal
|
||
* single-field row (i.e. every real form on the router) 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: it re-evaluates the moment the DOM changes, so keying on
|
||
* `.cbi-value-field ~ .cbi-value-field` meant any app that APPENDS a second field to a row at
|
||
* runtime — a status line, an output pane, a spinner holder, all ordinary shapes — flipped the
|
||
* whole row from "label beside field" to "everything stacked" under the user's finger. Measured
|
||
* on the stand: pressing a button in such a row moved that button 31px DOWN with this rule and
|
||
* 0px with it disabled, which is the "the buttons jump when I press them" report.
|
||
*
|
||
* What the rule exists for is several title/field PAIRS in one row, and a pair is announced by
|
||
* its TITLE (luci-app-3ginfo-lite's SIM-card row: 3 titles, 3 fields — still matched here).
|
||
* A second field with no second title is not a pair, it is an app adding something to the one
|
||
* field it already had, and nothing about that asks for a re-layout. Rendered markup is matched
|
||
* exactly as before; only the runtime append stops triggering it. */
|
||
.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 COULD NOT HOLD ITS OWN BUTTONS, and the row CLIPPED what did not fit
|
||
* instead of showing it — `Delete` came out sliced by the browser's border. base sizes the row
|
||
* by PROPORTION (name `flex: 10`, actions `flex: 3`), which was sound for base's own ~20px
|
||
* buttons sitting 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 (Deselect, Download, Delete). At 23% of
|
||
* the row they do not fit, base gives the browser's children no padding, and
|
||
* `.cbi-filebrowser > *` is `overflow: hidden` — so a button touched the clip box on BOTH axes,
|
||
* right edge and top edge, and lost its rounded corners to it. A shaved corner reads as a broken
|
||
* button, not as a missing 8px.
|
||
*
|
||
* 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 — with none, one row's
|
||
* button ran into the next row's. It wraps rather than clips when even that is not enough.
|
||
*
|
||
* This widget takes two clicks inside luci-app-firewall's ipsets or the wireless page to reach
|
||
* on a router, which is why it went unseen; docs/gallery.html now renders it open. */
|
||
.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, and every one of them
|
||
* used to be a bare literal — so LuCI's most common control was the only one in the theme that
|
||
* ignored the Density axis outright: 40x22 at Compact, 40x22 at Large, while every button and
|
||
* field around it moved (38 -> 32.3 -> 43.7). 02-tokens.css states the rule the other way round:
|
||
* "add a size OUTSIDE these ladders and it silently stops responding to the axis". The switch is
|
||
* a box that holds a knob, which is the box tier by definition; the pager buttons were moved off
|
||
* bare pixels for exactly this reason (55-buttons.css). Local names, not new global tokens: one
|
||
* component needs one number and three things derived from it. */
|
||
.cbi-checkbox { display: inline-flex; align-items: center; }
|
||
.cbi-checkbox > input[type="checkbox"] { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; }
|
||
.cbi-checkbox > label {
|
||
--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/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 (the visual 44x24 version 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 pill is 34x18.7, so the
|
||
* target is 38x26.7 — still over the 24px minimum (measured). */
|
||
.cbi-checkbox > label::before {
|
||
content: ""; position: absolute; inset: -4px -2px;
|
||
}
|
||
.cbi-checkbox > label::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);
|
||
}
|
||
.cbi-checkbox > input[type="checkbox"]:checked + label { background: var(--fs-accent); border-color: transparent; }
|
||
/* The travel is derived rather than hand-written (it was a literal 18px), 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 room the knob actually has is 2px less. Deriving it without
|
||
* that term overshoots by 2px and the knob lands off-centre against its own left inset
|
||
* (measured: travel 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 / 0x0 above, so base's `input:focus::before` glow paints on
|
||
* a 0x0 box — invisible. The <label> IS the switch, so the ring goes there. Without it the
|
||
* single most common control in LuCI showed nothing under keyboard focus.
|
||
*
|
||
* -solo, because on the CHECKED switch — which is 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 this border flip is accent on accent (measured on the
|
||
* gallery: fill rgb(86,157,245), border rgba(86,157,245,.97)) and the tinted ring was
|
||
* accent at .15 alpha. A checked, keyboard-focused toggle showed literally nothing. */
|
||
.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 (width/height/padding/background/border), and left behind in base every one of
|
||
* those overrides would lose to that rule by layer. The box is 16px on the grid; the
|
||
* tick/dot is inset FROM it rather than restating its own number — the three used to be
|
||
* 14/2/10 by hand, so resizing the box left 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 — and
|
||
* that is what a 16px tick box has always relied on. It holds while the theme decides the
|
||
* layout and stops holding the moment a third-party app lays out its own rows: CI's
|
||
* luci-app-filemanager listing put this checkbox and a file link inside the 24px, and the
|
||
* page gate reported both. A target that passes only because of what someone else's markup
|
||
* happens to do is not a target that passes.
|
||
*
|
||
* So the ELEMENT is 24px and the drawing stays 16px, centred inside it — the two pseudo
|
||
* elements below are already absolutely positioned, so centring them costs `inset: 0;
|
||
* margin: auto` and nothing else. Nothing moves visually: the box, the tick and the focus
|
||
* ring are all drawn by `::before`/`::after` at `--box`, and the element itself has neither
|
||
* background nor border. What grows is the area a finger may land on.
|
||
*
|
||
* `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 it occupies its
|
||
* old `--box` in layout: the negative margin pulls the extra area back out of the flow, so
|
||
* the box overflows itself symmetrically and everything around it sits exactly where it did.
|
||
* Measured on luci-app-filemanager's listing, which is where CI reported the finding: with
|
||
* the target simply grown, its rows went 42px -> 51px, i.e. a third-party page redesigned by
|
||
* a theme's accessibility fix; with the margin, 43px against 42px and the target still
|
||
* answers 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 */
|
||
input[type="checkbox"]:focus::before,
|
||
input[type="radio"]:focus::before {
|
||
border-color: var(--fs-accent);
|
||
box-shadow: var(--fs-focus-ring);
|
||
}
|
||
input[type="checkbox"].cbi-input-invalid:focus::before,
|
||
input[type="radio"].cbi-input-invalid:focus::before {
|
||
border-color: var(--fs-danger);
|
||
box-shadow: var(--fs-focus-ring-invalid);
|
||
}
|
||
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 (select/.cbi-select above, textarea/dropdown in
|
||
* theirs). 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 (checkbox/radio, file,
|
||
* button/submit/reset, range) carry their own theme rules: a base override would lose to
|
||
* this rule by layer no matter its specificity, which is why they were absorbed together. */
|
||
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 {
|
||
/* -webkit- only: Firefox has had the unprefixed property since 80. */
|
||
appearance: none; -webkit-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 */
|
||
background-repeat: no-repeat; background-position: right var(--fs-space-3) center; background-size: 12px;
|
||
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; }
|
||
select:focus { border-color: var(--fs-accent); box-shadow: var(--fs-focus-ring); outline: none; }
|
||
/* 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 (a stock white list on a dark theme). 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: browsers without it keep the styled
|
||
* closed control + 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 (podkop-plus rebuilds its device filter via
|
||
* select.replaceChildren), and early Chrome builds (~135) mis-render such a select as a
|
||
* single-item list — the app's dropdown then looks broken. 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 (padding 0 + the two-stop gradient
|
||
* + the ▾ plate) and a focus ring on the theme's one token. The shell is a DIV, so
|
||
* display/width must be stated — they do not fall out of any UA default.
|
||
* :not(select) because the class also appears ON a bare <select> (docs/gallery.html renders
|
||
* that shape): a replaced element cannot draw the ::before ▾, so shell paint there means a
|
||
* gradient with no glyph — that shape keeps the themed-select rules above instead. */
|
||
/* IT PAINTS LIKE THE THEMED <select>, because it is the same control wearing an older shell.
|
||
* It used to be the theme's only skeuomorphic surface: `linear-gradient(--fs-panel, --fs-border)`
|
||
* on the box AND again on the ▾ plate. In the dark palettes --fs-border is LIGHTER than the
|
||
* panel, so it was also the only control that got brighter towards the bottom. Nothing else in
|
||
* the theme has a gradient at all.
|
||
* The geometry below is not invented — it is the `select` rule above, property for property
|
||
* (panel2 fill, the palette-aware --fs-select-chevron at the same position and size, the same
|
||
* --fs-ctl-h floor, the same 210px box), 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, and it should not additionally look like a control from another theme. */
|
||
.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-position: right var(--fs-space-3) center;
|
||
background-size: 12px;
|
||
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;
|
||
}
|
||
.cbi-select.focus {
|
||
border-color: var(--fs-accent);
|
||
box-shadow: var(--fs-focus-ring);
|
||
}
|
||
/* the alarm must survive focus: .focus above would repaint an invalid shell accent */
|
||
.cbi-select.cbi-input-invalid.focus {
|
||
border-color: var(--fs-danger);
|
||
box-shadow: var(--fs-focus-ring-invalid);
|
||
}
|
||
/* 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; -webkit-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 & other field widgets — round + theme to match the selects/dropdowns (base
|
||
* leaves them at radius 3 / white). Excludes checkbox/radio (toggles) and file (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. color/datetime-local/
|
||
* month/week were missing and rendered as visibly different widgets next to a themed input
|
||
* in the same form. */
|
||
.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 poked past the radius — a square notch out of the
|
||
* frame. Fix: a slim self-rounded thumb floated off the edges (transparent border +
|
||
* background-clip: padding-box), a transparent track, and a track margin keeping the thumb
|
||
* clear of the corners — nothing square ever reaches one.
|
||
*
|
||
* 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 that caused the bug. 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 ("this field is resizable") —
|
||
* an accent arc tracing the frame's own radius (= the Rounding setting), no straight legs.
|
||
* Two borders + the corner radius draw the arc; 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 whole right+bottom border IS the quarter curve. (A mask
|
||
* would give a shorter arc, but ::-webkit-resizer ignores mask — it filled a solid square —
|
||
* and a baked-in SVG colour would not follow the palette or the accent hue.) */
|
||
/* 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 */
|
||
.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 {
|
||
border-color: var(--fs-accent);
|
||
box-shadow: var(--fs-focus-ring);
|
||
outline: none;
|
||
}
|
||
|
||
/* Invalid state keeps the danger cue. `.cbi-value-error input` is the LUA-CBI path and it
|
||
* belongs HERE, not in base: base/70-buttons.css 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. Probed on the gallery — the field rendered
|
||
* --fs-border grey, so every third-party app on luci-compat (which emits .cbi-value-error
|
||
* from full_valueheader.htm) had lost its field-error cue entirely, while the modern
|
||
* .cbi-input-invalid path worked. */
|
||
.cbi-input-invalid, input.cbi-input-invalid, textarea.cbi-input-invalid,
|
||
.cbi-value-error input, .cbi-value-error textarea {
|
||
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); }
|
||
/* the alarm must survive focus: the generic `input:focus` above is (0,1,1) and would repaint
|
||
* a focused invalid field accent; (0,2,0) outranks it. The TYPED :focus rules (0,2,1) still
|
||
* outrank this — a typed invalid field focuses accent, exactly as before the absorption. */
|
||
.cbi-input-invalid:focus {
|
||
border-color: var(--fs-danger);
|
||
box-shadow: var(--fs-focus-ring-invalid);
|
||
}
|
||
|
||
/* 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), so it needs the ring */
|
||
.cbi-dynlist > .item:focus {
|
||
border-color: var(--fs-accent);
|
||
box-shadow: var(--fs-focus-ring);
|
||
outline: none;
|
||
}
|
||
/* 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);
|
||
}
|
||
.cbi-dynlist > .add-item:focus-within { border-color: var(--fs-accent); box-shadow: var(--fs-focus-ring); }
|
||
.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);
|
||
}
|
||
.control-group:has(> .cbi-input-password):focus-within {
|
||
border-color: var(--fs-accent);
|
||
box-shadow: var(--fs-focus-ring);
|
||
}
|
||
/* 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 — CBI dependency hiding was
|
||
* leaking podkop's conditional fields back onto the page, and the enumerated list that fixed
|
||
* that (`.cbi-value.hidden`, `.cbi-section.hidden`, `tr.hidden`, …) only ever covered the
|
||
* elements someone had already been bitten by. The one it did NOT cover is the Enter-key
|
||
* submit every legacy Lua page carries: luci-compat's cbi/header.htm opens each form with
|
||
* `<input type="submit" value="Save" class="hidden">`, and an ATTRIBUTE selector counts as a
|
||
* class — theme/55-buttons.css's `input[type="submit"]` is 0,1,1 against a bare `.hidden`'s
|
||
* 0,1,0, so it won, and a stray "Save" button floated above the page title on every
|
||
* luci-compat view (issue #12, luci-app-openvpn). Doubling the class states the invariant
|
||
* ONCE at 0,2,0 instead of chasing elements: hidden is hidden, whatever the element and
|
||
* whatever component rule this layer also matched. An `!important` would have done it too,
|
||
* and would have inverted the layer order for every future override — this does not. */
|
||
.hidden.hidden { display: none; }
|
||
/* RangeSlider (UIRangeSlider) — the one LuCI widget base leaves to native browser rendering.
|
||
* Token-driven track/thumb. */
|
||
input[type="range"] {
|
||
-webkit-appearance: none; 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 {
|
||
-webkit-appearance: none; appearance: none;
|
||
width: 16px; height: 16px; border-radius: 50%;
|
||
background: var(--fs-accent); border: 2px solid var(--fs-panel);
|
||
box-shadow: var(--fs-shadow); 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;
|
||
/* same shadow the -webkit thumb carries — Firefox had silently drifted flat */
|
||
box-shadow: var(--fs-shadow);
|
||
}
|
||
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); }
|
||
}
|