@layer base { /* Forms: input types, the .cbi-value row layout, and the focus/disabled/ * readonly/invalid states shared by every CBI widget. */ fieldset { margin-bottom: var(--fs-space-2); padding-top: var(--fs-space-2); } /* size: theme/30-tables.css (`legend`) — absorbed */ fieldset legend { display: block; line-height: 1; color: var(--fs-text); padding-top: var(--fs-space-5); } /* Control typography stays in BASE: moved to the theme layer, a bare-element rule out-ranks * every classed base refinement regardless of specificity, and the firewall's zone badges grew * from 10.8 to 13px. In base, classed rules beat it on specificity, which is the contract they * were written against. `select` is not a member — its face, size and leading live in * 60-inputs.css. */ label, input, button { font-family: var(--fs-font-sans); font-size: var(--fs-type); font-weight: var(--fs-weight-normal); line-height: normal; } .cbi-value { display: flex; flex-wrap: wrap; margin-bottom: var(--fs-space-4); align-items: baseline; } .cbi-value-field { /* logical, not margin-left: this is the gap AFTER the label column, and LuCI ships four RTL * languages (ar/fa/he/ur). Identical to margin-left while nothing in openwrt/luci stamps * dir=rtl, so it costs nothing and is already correct if one ever does. */ margin-inline-start: var(--fs-space-5); flex: 1; /* a flex item will not shrink below min-content by default, so a nested table or one long * unbreakable value pushes the whole row out of the card */ min-width: 0; } /* size + ink: theme/60-inputs.css (`.cbi-value-title`) — absorbed. Left: the label * column's geometry, which theme does not touch. */ .cbi-value label.cbi-value-title { /* the nudge that lands the label's first baseline on the field's, tuned for the unitless * leading in 20-typography.css */ padding-top: var(--fs-space-1); /* 180px is a column WIDTH, not a rhythm — what fits LuCI's longest option labels beside a * field — so it is deliberately not on the 4px step scale */ flex: 0 0 180px; /* `end`, not `right`: the label is aligned TOWARDS its field, which is a logical intent. * Not the same as base/95-luci.css's `.left/.right/.center`, which are LuCI's forcing * utilities — a class named `right` means right in any direction. Never sweep those onto * start/end. */ text-align: end; } .cbi-value > .cbi-section, .cbi-value > .cbi-tblsection { width: 100%; } label > input[type="checkbox"], label > input[type="radio"] { vertical-align: text-top; margin: 0; } label[for] { cursor: pointer; } /* The shared field box (the 210px x control-height default, #localtime's widening, and the * per-widget escapes: file, button types, checkbox/radio, .cbi-select, readonly, the * cell-filling widths) lives in theme/60-inputs.css — absorbed whole. The escapes had to move * with the box: each beat it here in-layer, and left behind they would lose to a theme rule by * layer whatever their specificity. */ .cbi-dynlist { height: auto; /* min-height: theme/60-inputs.css — absorbed */ display: inline-flex; flex-direction: column; } /* item paint: theme/60-inputs.css — absorbed. Left: the drag-and-drop machinery * UIDynamicList needs. */ .cbi-dynlist > .item { margin-bottom: var(--fs-space-1); padding: var(--fs-space-1) 2em var(--fs-space-1) var(--fs-space-2); position: relative; pointer-events: auto; /* needed for drag-and-drop in UIDynamicList */ overflow: hidden; word-break: break-all; cursor: move; /* drag-and-drop */ user-select: text; /* text selection in drag-and-drop */ } /* display/padding/border + radius + × ink: theme/60-inputs.css (the bare `.item::after` * plate + its dynlist refinement) — absorbed. Left: what pins the plate to the chip. */ .cbi-dynlist > .item::after { content: "×"; position: absolute; align-items: center; top: -1px; inset-inline-end: -1px; bottom: -1px; font-weight: var(--fs-weight-bold); pointer-events: auto; } /* indication line for drag-and-drop in UIDynamicList*/ .cbi-dynlist > .item.drag-over { border-top: 1px solid var(--fs-text); } /* Make item being dragged in UIDynamicList partially transparent*/ .cbi-dynlist > .item.dragging { opacity: .5; } /* prevent pointer changing when over the span element in UIDynamicList */ .cbi-dynlist > .item > span { pointer-events: none; } .cbi-dynlist > .add-item { display: flex; } /* indication line for drag-and-drop in UIDynamicList*/ .cbi-dynlist > .add-item > .cbi-input-text.drag-over { border-top: 1px solid var(--fs-text); } /* indication line for drag-and-drop in UIDynamicList*/ .cbi-dynlist > .add-item > .cbi-button-add.drag-over { border-top: 1px solid var(--fs-text); } .cbi-dynlist > .add-item > input, .cbi-dynlist > .add-item > button { /* flex: theme/60-inputs.css — absorbed */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* the same one-line control height, so a toggle lines up with the field above it */ .cbi-value-field > .cbi-checkbox, .cbi-value-field > div > .cbi-radio { height: var(--fs-control-h); display: inline-flex; align-items: center; } .cbi-radio { cursor: pointer; gap: var(--fs-space-1); } /* .cbi-select lives in theme/60-inputs.css — absorbed whole. Do not re-add an option rule here * either: layer beats specificity, so theme's bare `option` outranks even * `.cbi-select select option:checked`. */ /* Checkbox/radio drawing: theme/60-inputs.css — absorbed whole (it undoes the generic input * box, which lives there now). */ ::placeholder { color: var(--fs-dim); } /* Transitions and the focus/hover ring are the theme's one ring token (--fs-focus-ring / * --fs-focus-ring-invalid). A generic glow here rings every clicked button and every hovered * widget in a second, unnamed focus style. */ input[disabled], button[disabled], select[disabled], textarea[disabled], .cbi-dropdown[disabled]:not(.btn):not(.cbi-button), input[type="checkbox"][disabled]::before, input[type="checkbox"][disabled]::after, input[type="radio"][disabled]::before, input[type="radio"][disabled]::after { opacity: var(--fs-disabled-opacity); pointer-events: none; cursor: default; } /* readonly border fade: theme/60-inputs.css — absorbed with the generic box */ .cbi-optionals, .cbi-section-create { padding: 0 0 var(--fs-space-3) var(--fs-space-3); } /* `gap`, not a negative-margin pair: that trick predates flex gap and leaks, the outer -3px * pulling the row into whatever sits beside it */ .cbi-section-create { display: inline-flex; align-items: center; gap: var(--fs-space-2); } .cbi-section-create > * { flex: 1 1 auto; } /* the input's cell-filling width: theme/60-inputs.css — absorbed with the generic box */ /* .cbi-page-actions paint: theme/55-buttons.css — absorbed. The legacy `.actions` plate has no * theme override, so it stays: some luci-app-* still emits it. */ .actions { background: var(--fs-panel2); margin-bottom: var(--fs-space-4); /* a plate, so it takes the card's own gutter on every side */ padding: var(--fs-card-pad); border-top: var(--fs-hairline); border-radius: 0 0 var(--fs-radius-lg) var(--fs-radius-lg); text-align: end; } /* Physical, mirrored by hand — see `.close` in base/90-widgets.css for why `float` takes no * logical keyword. Unmirrored it contradicts its own container: `.actions` above sets * `text-align: end`, which follows the writing direction, so in RTL the plate aligns its * content one way and the secondary action the other. Measured in a live document at 1400px: * 582px from the reading edge in LTR, 10px in RTL. */ .actions .secondary-action, .cbi-page-actions .secondary-action{ float: right; } :root[dir="rtl"] .actions .secondary-action, :root[dir="rtl"] .cbi-page-actions .secondary-action { float: left; } .actions .secondary-action a, .cbi-page-actions .secondary-action a { line-height: var(--fs-control-h); } .actions .secondary-action a:hover, .cbi-page-actions .secondary-action a:hover { text-decoration: underline; } .cbi-page-actions > form { display: inline; margin: 0; } }