@layer theme { /* baseline, not center: a taller field (help sentence under the control) would push a * centered label off the control's first line */ .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 need a gap, a raw
collapses it; password-reveal excluded, 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; } /* hairline separates a row from the NEXT one, so it drops whenever none follows, not only at the section's end */ .cbi-value:not(:has(+ .cbi-value)) { border-bottom: 0; } /* .cbi-checkbox has no text baseline (synthesised at its bottom edge, floats ~5px above the label) — centred, except with a help sentence under it (see the baseline note above) */ .cbi-value:not(:has(.cbi-value-description)):has(> .cbi-value-field > .cbi-checkbox, > .cbi-value-field > div > .cbi-radio) { align-items: center; } /* ONLY the transition here, shared by every field box below (`textarea` included); typography stays in base/30-forms.css. border-COLOR not the shorthand, which also animates width */ input, button, textarea { transition: var(--fs-field-transition); } .cbi-value-title { color: var(--fs-dim); font-size: var(--fs-type); font-weight: var(--fs-weight); /* stated not inherited: this is TEXT, the control rule hands labels line-height: normal (measured 18px -> normal without this) */ line-height: var(--fs-leading); } /* mono is for VALUES: buttons, the help sentence and `` LABELS excluded — a label * fed to mono also goes bold, 227 elements/20 KB of face for nothing. theme/45-misc.css's * label rule must ASSIGN sans; excluding `` here alone still inherits mono */ .cbi-value-field, .cbi-value-field *:where(:not(.btn, .cbi-button, .cbi-value-description)) { font-family: var(--fs-font-mono); } .cbi-value-description { font-family: var(--fs-font-sans); text-wrap: pretty; } /* not the generic input's fixed 210px (above): mono + padded --fs-space-3, 210px clipped a URL mid-domain (issue #5); elastic, capped to the dynlist's ceiling */ .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); } /* base assumes ONE title+field pair; stack full-width on a SECOND title (not a second field — a runtime-appended field would re-stack under the user's finger, measured: a button moved 31px) */ .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; } .td > input[type="text"], .td > input[type="password"], .td > select, .td > .cbi-dropdown:not(.btn):not(.cbi-button), .cbi-dynlist > .add-item > .cbi-dropdown, .cbi-section-create > * > input { width: 100%; } /* --- live components that bypassed the theme --- */ /* a CBI row is
on a form, in a table — same , 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); } /* base sizes a row by proportion for its own ~20px buttons; this theme's 36px button (3 per * row) does not fit at 23% and `overflow: hidden` clips the corners — size actions to content, * let the name ellipsize, wrap when 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`: may wrap but not shrink below min-content, or it sticks out (measured 23px past the box at 320px before min-width: 0) */ .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; } .cbi-filebrowser .upload > div > input { width: 100%; } /* Form widgets — toggles, file input. Applies to every CBI form. */ /* the shared checkbox rule below gives the input a -4px margin (16px box in a 24px hit area, Normal * density), so inside a .btn label the mark sat flush against the text; .5em restores the gap. * The toggle switch is unaffected: its own margin:0 wins on a different ancestor */ .btn > input[type="checkbox"], .btn > input[type="radio"] { margin-inline-end: .5em; } /* checkbox -> toggle switch, drawn on the INPUT — docs/css.md "Inputs: the toggle switch, * drawn on the input" has the forum regression this replaced. density-scaled local tokens. */ /* overflow-wrap: anywhere for the one caption word too long even at full width (pseudo-loc: a * 14-letter Russian compound grown to 20 pushed the row 24.6px past the field at 320px/Large) */ .cbi-checkbox { display: inline-flex; align-items: center; gap: var(--fs-space-2); overflow-wrap: anywhere; } /* label[for] stays in the DOM but draws nothing: out of flow, out of the flex item count, so gap opens only on the switch input + an optional caption */ .cbi-checkbox > label[for] { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; } .cbi-checkbox > input[type="checkbox"] { --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)); --sw-travel: calc(var(--sw-w) - var(--sw-knob) - var(--sw-pad) * 2 - 2px); /* appearance/--bd-color/--fg-color come from the shared input[type=checkbox] rule below; * flex-shrink: 0 keeps the pill at its width beside a caption (docs/css.md "Inputs: the toggle switch, drawn on the input, not the label") */ display: inline-block; position: relative; margin: 0; flex-shrink: 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); } /* hit-target overlay to 44x30 (WCAG 2.5.8 floor 24px, pill is 22px); inset stays literal, not * density-scaled — a floor that shrinks with Density is not one */ .cbi-checkbox > input[type="checkbox"]::before { inset: -4px -2px; } .cbi-checkbox > input[type="checkbox"]::after { 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 re-lays out the row every frame, translateX is composited */ transform: translateX(0); transition: transform var(--fs-dur-move), background var(--fs-dur); } .cbi-checkbox > input[type="checkbox"]:checked { background: var(--fs-accent); border-color: transparent; } /* derived so it cannot drift from the sizes above; -2px is the pill's two 1px borders (the knob sits against the padding box, so it has 2px less room — travel is 18px not 20) */ .cbi-checkbox > input[type="checkbox"]:checked::after { transform: translateX(var(--sw-travel)); background: var(--fs-on-accent); } /* the knob starts at the inline-start edge, so RTL travels the other way (translateX is physical) */ :root[dir="rtl"] .cbi-checkbox > input[type="checkbox"]:checked::after { transform: translateX(calc(var(--sw-travel) * -1)); } /* the input IS the switch, ring goes here; -solo: checked switch already spends its border channel on accent, only the tinted shadow shows */ .cbi-checkbox > input[type="checkbox"]:focus-visible { border-color: var(--fs-accent); box-shadow: var(--fs-focus-ring-solo); } .cbi-checkbox > input[type="checkbox"]:disabled { cursor: not-allowed; } /* opacity comes from base/30-forms.css input[disabled] */ /* raw checkbox/radio, absorbed from base: undoes the generic input box below. 16px box on the * grid, mark inset FROM it. `.cbi-checkbox > input` also matches the switch's own input * (unscoped, it wants this rule's appearance:none) — selectors that WOULD paint the switch * exclude it individually below instead. */ input[type="checkbox"], input[type="radio"] { --bd-color: var(--fs-border); --fg-color: var(--fs-text); /* box tier not space tier: --fs-space-4 gave 16px at Normal but 10.4px at Compact (smaller than any control) and never grew at Large */ --box: calc(16px * var(--fs-density-box)); --mark-inset: 3px; /* WCAG 2.5.8's 24px spacing floor holds only while the theme owns the layout; ELEMENT is * 24px (drawing stays 16px, centred), max() so it never shrinks under --box at Large */ --hit: max(24px, var(--box)); appearance: none; width: var(--hit); height: var(--hit); /* negative margin pulls the extra hit area back out of flow (without it, rows measured 42px -> 51px) */ 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 not reset: land on an appearance-none box whose pseudo-elements are absolute, invisible either way */ } input[type="checkbox"]::before, input[type="checkbox"]::after, input[type="radio"]::before, input[type="radio"]::after { position: absolute; content: ""; } input[type="checkbox"]:not(:where(.cbi-checkbox > input))::before, input[type="radio"]::before { /* inset:0; margin:auto centres a fixed box the same in RTL as LTR; a translate pair would not */ 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); } input[type="checkbox"]::before { border-radius: 2px; } input[type="radio"], input[type="radio"]::before { border-radius: 50%; } /* keyboard/click focus ring on the DRAWN box, the theme's one ring token */ input[type="checkbox"]:not(:where(.cbi-checkbox > input))::after, input[type="radio"]::after { /* mark inset from the DRAWING, which is centred in the target, not 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"]:not(:where(.cbi-checkbox > input)):checked::after, input[type="radio"]:checked::after { /* OURS: stroked path (this theme's icon language), replacing a fill byte-identical to luci-theme-bootstrap's; mask keys on alpha so a stroke masks like a fill did */ --checkmark-icon: url("data:image/svg+xml,"); -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 { /* radio dot filled, not ringed — a ring reads as "not selected" */ --checkmark-icon: url("data:image/svg+xml,"); } input[type="checkbox"].cbi-input-invalid, input[type="radio"].cbi-input-invalid { --bd-color: var(--fs-danger); --fg-color: var(--fs-danger); } /* generic field box, absorbed from base; bare input keeps the historical 210px fallback for types the list below does not name */ 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); font-size: var(--fs-type-field); border: var(--fs-hairline); border-radius: var(--fs-radius); } /* base's old #localtime{width:300px} media rule not moved here: the elastic field width above already out-layers it (measured 440px, not 300) */ input[type="file"] { color: var(--fs-dim); font-size: var(--fs-type); padding: initial; border: initial; line-height: initial; box-shadow: none; /* no !important: width:auto out-specifies the generic input{width:210px} alone */ width: auto; } /* native stays 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); /* background:transparent undoes `select option`'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); } /* same shape as the open .cbi-dropdown's selected row (theme/65-dropdown.css): accent on --fs-accent-soft measures 4.21:1 in dark, under AA */ .cbi-value-field select option:checked { background: var(--fs-panel2); color: var(--fs-accent); box-shadow: var(--fs-row-rail); } } /* .cbi-select — legacy luci-compat shell around a native , which cannot draw the ::before chevron */ .cbi-select:not(select) { /* height not min-height: the child's height:100% falls back to content height against an auto-height parent (measured collapse 30px -> 15px in a 38px shell) */ height: var(--fs-ctl-h); padding: 0; position: relative; /* the shell draws no text; the shared rule's field size belongs to its select child */ font-size: var(--fs-type); } /* base/30-forms.css only dims the old ▾ plate; with the plate gone the disabled state had no visual, so the shell carries it now */ .cbi-select[disabled]:not(select) { opacity: var(--fs-disabled-opacity); pointer-events: none; } /* invisible, filling the shell, clearing the plate by the same --fs-space-8 inset the themed select uses */ .cbi-select select, .cbi-select select:focus { appearance: none; outline: none; border: none; background: transparent; box-shadow: none; height: 100%; min-height: 0; width: 100%; padding-block: 0; padding-inline: var(--fs-space-3) var(--fs-space-8); font-size: var(--fs-type-field); line-height: normal; cursor: pointer; } /* :where() keeps this at 0,1,0: a bare :not() would be 0,2,0 and beat `.alert-message p` (0,1,1). * A button keeps its own label ink while it spins; dim on a solid fill measured 1.00-2.24:1. */ .spinning:where(:not(.btn, .cbi-button)), .cbi-value em, em.spinning { color: var(--fs-dim); } /* type list must be EXHAUSTIVE: an omission falls back to the generic `input` box above, beside a themed one, not to "unstyled" */ .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; } /* max-width:100% beside the fixed 210px: a textarea's intrinsic width comes from `cols` (default 20), which put two textareas 19px outside their card at 300px before this */ textarea { min-height: 84px; line-height: var(--fs-leading); width: 210px; max-width: 100%; font-size: var(--fs-type-field); } /* a square native scrollbar is not clipped by a rounded corner; self-rounded thumb + transparent track fixes it. no scrollbar-width/-color: either switches Chromium to the square standard bar */ 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); } textarea::-webkit-scrollbar-corner { background: transparent; } /* accent arc traces the frame's own radius (two borders shrunk to 16px - radius); mirrored by * hand below, ::-webkit-resizer ignores logical properties */ 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); } /* fallback ring for untyped inputs; excludes the toggle switch, whose :focus-visible-only ring must not double up */ .cbi-input-text:focus, .cbi-input-password:focus, .cbi-input-textarea:focus, .cbi-section-create-name:focus, input:focus:not(:where(.cbi-checkbox > input)), 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; } /* same ring, shared by 4 selectors that cannot join the list above; :is() around :has() keeps the list forgiving — unwrapped, a :has() compound sharing a list fails the WHOLE rule below the floor */ input[type="checkbox"]:not(:where(.cbi-checkbox > input)):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); } /* .cbi-value-error input (Lua-CBI) must live here: base's red border-color is a longhand and * this rule's border shorthand wipes it by layer regardless of specificity. * The same rejected value shown by a ui.Dropdown: base carried the widget and its closed
  • , theme only the input half. * The enhanced select's validator fails on the hidden