mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-02 01:03:56 +08:00
533 lines
28 KiB
CSS
533 lines
28 KiB
CSS
@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 <br> 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 `<strong>` 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 `<strong>` 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 <div class="cbi-value"> on a form, <td class="cbi-value-field"> in a table — same <var>, 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,<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 {
|
||
/* radio dot filled, not ringed — a ring reads as "not selected" */
|
||
--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);
|
||
}
|
||
|
||
/* 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 <select> chevron + theme; the chevron is --fs-select-chevron (two gradient bands), the RTL rule below mirrors its position */
|
||
select, select.cbi-input-select, .cbi-value-field select, .cbi-select:not(select) {
|
||
/* unprefixed only, floor clears it everywhere — docs/css.md "The browser floor" */
|
||
appearance: none;
|
||
background-color: var(--fs-panel2);
|
||
background-image: var(--fs-select-chevron);
|
||
/* right inset is the scale step, not a literal 12; the .cbi-select shell shares this rule */
|
||
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-block: var(--fs-space-2); padding-inline: var(--fs-space-3) var(--fs-space-8);
|
||
border: var(--fs-hairline); border-radius: var(--fs-radius);
|
||
color: var(--fs-text); font-family: var(--fs-font-sans); font-size: var(--fs-type-field);
|
||
line-height: 1.3; min-height: var(--fs-ctl-h); cursor: pointer;
|
||
width: 210px;
|
||
display: inline-block;
|
||
transition: var(--fs-field-transition);
|
||
}
|
||
/* background-position has no logical keyword, so it is mirrored by hand; the two bands swap offsets
|
||
* to keep the chevron's left-leaning half on the left. The padding above is logical and follows. */
|
||
:root[dir="rtl"] :is(select, .cbi-select:not(select)) {
|
||
background-position: left calc(var(--fs-space-3) + 5px) center, left calc(var(--fs-space-3) - 1px) center;
|
||
}
|
||
/* height:inherit so the multiple/listbox variant does not take the closed control's one-line height */
|
||
select[multiple] { height: inherit; }
|
||
option { font-size: 100%; line-height: 2em; }
|
||
select option { background: var(--fs-panel); color: var(--fs-text); }
|
||
|
||
/* the OS renders the native list, so `select option` above is mostly ignored; Chromium's
|
||
* base-select renders it in the DOM instead, themeable like .cbi-dropdown; @supports-gated,
|
||
* scoped to .cbi-value-field so a third-party app's raw <select> 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 <select>; it takes the themed select rule above and
|
||
* keeps only its box. :not(select): the class also appears on a bare <select>, 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 <li>, theme only the input half.
|
||
* The enhanced select's validator fails on the hidden <select>, so its widget (the next sibling) is matched through it;
|
||
* (0,5,1) beats 65-dropdown's border shorthand (0,3,0), its hover (0,4,0) and its focus ring (0,2,0); the closed <li> takes the border only, the ring sits on the widget */
|
||
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,
|
||
.cbi-input-invalid.cbi-dropdown:not(.btn):not(.cbi-button),
|
||
select.cbi-input-invalid[data-fs-select="1"] + .cbi-dropdown:not(.btn):not(.cbi-button) {
|
||
border-color: var(--fs-danger);
|
||
box-shadow: var(--fs-focus-ring-invalid);
|
||
}
|
||
.cbi-input-invalid.cbi-dropdown:not([open]) > ul > li { border-color: var(--fs-danger); }
|
||
|
||
/* doubled [readonly] states intent at (0,2,1): at (0,1,1) it only tied the typed input rules above and silently lost */
|
||
: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;
|
||
}
|
||
|
||
/* keep base's layout (inline-flex chips + add-item row); only colours, rounding, a compact + button, narrow cap so a chip's × sits near its text */
|
||
.cbi-dynlist {
|
||
gap: var(--fs-space-1-5);
|
||
background: transparent; border: 0; padding: 0;
|
||
/* min(240px,100%) not a bare 240: a floor that exceeds the room is an overflow (measured 29px outside a 210px field at 320px) */
|
||
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);
|
||
}
|
||
.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);
|
||
}
|
||
.item:hover::after { filter: var(--fs-hover-lift); }
|
||
.cbi-dynlist > .item::after {
|
||
display: inline-flex;
|
||
padding: 0 var(--fs-space-2);
|
||
color: var(--fs-danger);
|
||
/* logical: base pins the × with inset-inline-end, so in RTL the rounded corners must follow it */
|
||
border-radius: 0;
|
||
border-start-end-radius: var(--fs-radius-sm);
|
||
border-end-end-radius: var(--fs-radius-sm);
|
||
}
|
||
.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 > input,
|
||
/* width:auto keeps the flex basis in charge — this input sits inside .cbi-value-field, the elastic width above would otherwise hand it 100% 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;
|
||
}
|
||
/* UIDynamicList drop indicator (ui.js puts .drag-over on the dragenter target): (0,4,0) out-ranks the border
|
||
* shorthands above and in 55-buttons; [draggable] (ui.js sets it on every .item) lifts the item past .item:focus (0,3,0) */
|
||
.cbi-dynlist > .item[draggable].drag-over,
|
||
.cbi-dynlist > .add-item > :is(.cbi-input-text, .cbi-button-add).drag-over {
|
||
border-top: 1px solid var(--fs-text);
|
||
}
|
||
|
||
/* input + button as ONE seamless group; gap:0 is load-bearing — base gives every .control-group a gap, this is the one whose members must touch */
|
||
.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);
|
||
}
|
||
.cbi-section-create, .cbi-section-create.cbi-tblsection-create { margin-top: var(--fs-space-3); }
|
||
|
||
/* `.hidden.hidden` beats component rules up to (0,3,0) that come earlier in source, without
|
||
* !important (which would invert layer order); it also covers the Enter-key submit legacy Lua
|
||
* pages carry (issue #12). The @container table rules and fs-stacked actions outrank it.
|
||
* :not([class*=":"]) excludes a Tailwind/UnoCSS class fs-sheets.js re-hosts into this layer —
|
||
* docs/css.md "Inputs: `.hidden.hidden`, and the Tailwind/UnoCSS exception". */
|
||
.hidden.hidden:not([class*=":"]) { display: none; }
|
||
/* the WCAG 2.5.8 target (24px min) split from the 6px track — docs/css.md "Inputs: the range
|
||
* slider's hit target and its line-box" has the two-part fix and its numbers */
|
||
input[type="range"] {
|
||
appearance: none;
|
||
width: 100%; height: 24px; min-height: 0; padding: 0;
|
||
margin-block: -9px;
|
||
background: transparent; border: 0; border-radius: 0;
|
||
outline: none; box-shadow: none; cursor: pointer;
|
||
}
|
||
input[type="range"]::-webkit-slider-runnable-track {
|
||
width: 100%; height: 6px;
|
||
background: var(--fs-track); border: 0; border-radius: var(--fs-radius-pill);
|
||
}
|
||
input[type="range"]::-moz-range-track {
|
||
width: 100%; height: 6px;
|
||
background: var(--fs-track); border: 0; border-radius: var(--fs-radius-pill);
|
||
}
|
||
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;
|
||
/* WebKit centres on the INPUT's box not the track: (6-16)/2 = -5px */
|
||
margin-top: -5px;
|
||
}
|
||
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"]:focus::-webkit-slider-runnable-track { box-shadow: var(--fs-focus-ring-solo); }
|
||
input[type="range"]:focus::-moz-range-track { box-shadow: var(--fs-focus-ring-solo); }
|
||
/* same ceiling as the elastic text inputs; vertical-align:middle decouples the line-box from the input's own edge, which moved 6px -> 24px above */
|
||
.cbi-range-slider { display: inline-flex; align-items: center; vertical-align: middle; 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 bold mono face bundled, drags in 20 KB or fakes it with synthetic emboldening */
|
||
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); }
|
||
}
|