@layer theme { /* buttons straight in a field (Enable/Disable, Start/Restart/Stop) only get whitespace * spacing inline-block, collapsing rows on wrap — wrapping flex with a real gap instead */ .cbi-value-field:has(> .cbi-button) { display: inline-flex; flex-wrap: wrap; gap: var(--fs-space-2); align-items: center; justify-content: flex-end; } /* native input types listed on purpose: LuCI also emits * (sysauth, luci-compat cbi/*.htm), mostly with .cbi-button; luci-app-splash emits bare ones. * Base only gives those width/height: auto — coverage is a contract (docs/gallery.html) */ .cbi-button, .btn, input[type="button"], input[type="submit"], input[type="reset"] { display: inline-flex; align-items: center; justify-content: center; /* auto, stated: theme/60-inputs.css's generic input box says 210x32px, a base reset would lose to it by layer */ width: auto; height: auto; min-height: var(--fs-ctl-h); padding: var(--fs-space-2) var(--fs-space-3-5); font-family: var(--fs-font-sans); font-weight: var(--fs-weight); line-height: 1.1; color: var(--fs-text); background: var(--fs-panel2); border: var(--fs-hairline); border-radius: var(--fs-radius); box-shadow: none; text-shadow: none; /* border-COLOR not the shorthand, which also animates width — a layout animation on every hover */ transition: var(--fs-field-transition); } /* bare `button` rides along: harmless on a border:0 box, meaningful once an app gives it one */ .cbi-button:hover, .btn:hover, button:hover, input[type="button"]:hover, input[type="submit"]:hover, input[type="reset"]:hover { border-color: var(--fs-dim); } /* keyboard focus only: box-shadow:none above kills any stray glow, plain mouse :focus shows * nothing on purpose — the pressed cue below already answers a click */ .cbi-button:focus-visible, .btn:focus-visible, input[type="button"]:focus-visible, input[type="submit"]:focus-visible, input[type="reset"]:focus-visible { outline: none; border-color: var(--fs-accent); box-shadow: var(--fs-focus-ring); } /* solid variants sit ON --fs-accent/-good/-danger, an accent-tinted ring is invisible against * them — ring in the surface colour with a solid accent halo (--fs-focus-ring-solo) instead */ .cbi-button-positive:focus-visible, .cbi-button-action:focus-visible, .cbi-button-negative:focus-visible, .cbi-button-remove:focus-visible, .cbi-button-apply:focus-visible, .cbi-button-save:focus-visible, .btn.primary:focus-visible, .btn.danger:focus-visible, .btn.error:focus-visible, .btn.success:focus-visible, .btn.info:focus-visible { outline: none; border-color: transparent; box-shadow: var(--fs-focus-ring-solo); } /* the sole copy of the pressed inset shadow (base's identical one was absorbed) */ .cbi-button:active, .btn:active, .btn.active, input[type="button"]:active, input[type="submit"]:active, input[type="reset"]:active { box-shadow: inset 0 2px 4px color-mix(in srgb, var(--fs-border) 40%, transparent); } /* three fills, each shared by `.cbi-button-*` (LuCI's own views) and `.btn.` (third-party * apps), or a recolour lands on one half and not the other. * The `.important` twins are needed: at 0,2,0 they beat the neutral `input[type=button]` paint (0,1,1) * for dockerman's ``. */ .cbi-button-positive, .btn.success { background: var(--fs-good); border-color: transparent; color: var(--fs-on-good); } .cbi-button-action, .cbi-button-action.important, .btn.primary, .btn.info { background: var(--fs-accent); border-color: transparent; color: var(--fs-on-accent); } .cbi-button-negative, .cbi-button-negative.important, .cbi-button-remove, .btn.danger, .btn.error { background: var(--fs-danger); border-color: transparent; color: var(--fs-on-danger); } /* the lift is for the fills above plus the page-action Save & Apply: brightness deepens a solid * fill and washes out an outline or a tint, so neutral Apply/Save keep their hairline. * `.td .cbi-button-apply` is the one outline that does lift. */ .cbi-button-positive:hover, .btn.success:hover, .cbi-button-action:hover, .btn.primary:hover, .btn.info:hover, .cbi-button-negative:hover, .cbi-button-remove:hover, .btn.danger:hover, .btn.error:hover, .cbi-page-actions .cbi-button-apply:hover, .td .cbi-button-apply:hover { filter: var(--fs-hover-lift); border-color: transparent; } .cbi-button-reset, .cbi-button-link { background: transparent; border-color: var(--fs-border); color: var(--fs-dim); } /* .btn owns size words in full now: the plain rule above re-sets padding in a later * layer, so a size word carrying only font-size (base's old copy) got default padding */ .btn.large { font-size: var(--fs-type-lg); min-height: var(--fs-ctl-h-lg); padding: var(--fs-space-2) var(--fs-space-4); } .btn.small { font-size: var(--fs-type-xs); min-height: var(--fs-ctl-h-sm); padding: var(--fs-space-1) var(--fs-space-2-5); border-radius: var(--fs-radius-sm); } .cbi-section .table[id] .td .btn, .cbi-section .table[id] .td .cbi-button { padding: var(--fs-space-1) var(--fs-space-2-5); font-size: var(--fs-type); } /* size only: ui.changes puts .alert-message on the apply/rollback modal itself, so a paint here * (0,2,0, after the fills) turned 'Apply unchecked' into the same panel2 grey as 'Cancel' */ .alert-message .btn, .alert-message .cbi-button { border-radius: var(--fs-radius); padding: var(--fs-space-2) var(--fs-space-3); } .alert-message .btn:not(.cbi-button-positive, .cbi-button-action, .cbi-button-negative, .cbi-button-remove, .primary, .info, .success, .danger, .error):hover { border-color: var(--fs-accent); color: var(--fs-accent); } /* Save/Apply footer: base renders a tall half-empty plate with bottom-only radius, made a compact rounded bar */ .cbi-page-actions { background: transparent; border: 0; border-radius: 0; box-shadow: none; padding: var(--fs-space-2) 0; margin: 0 0 var(--fs-card-gap); text-align: end; display: flex; align-items: center; justify-content: flex-end; gap: var(--fs-space-2); /* WRAP the bar rather than let items shrink into it: a no-wrap row put the whole squeeze on * the Save & Apply split control's caption (measured up to 119px cut at 320px/Large) instead * of sharing it — docs/css.md "Buttons: the page-action bar wraps, not shrinks" has the * before/after numbers. */ flex-wrap: wrap; } .cbi-page-actions .cbi-button, .cbi-page-actions .btn { min-height: var(--fs-ctl-h); padding: 0 var(--fs-space-5); border-radius: var(--fs-radius); font-weight: var(--fs-weight); vertical-align: middle; } /* Reset takes Save's same ladder step (-soft/-fill) off the DANGER role, since it discards * unsaved changes; label is --fs-text for the same over-tint reason as Save's below. */ .cbi-page-actions .cbi-button-reset { background: var(--fs-danger-soft); border: 1px solid var(--fs-danger-line); color: var(--fs-text); } .cbi-page-actions .cbi-button-reset:hover { background: var(--fs-danger-fill); border-color: var(--fs-danger-line-hi); } /* no `.spinning` padding ladder here, and it must not come back — base/95-luci.css puts the * glyph IN THE FLOW instead, reserving its own space (the ladder lost twice: issues #15, #22) */ input[type="file"]::file-selector-button { margin-inline-end: var(--fs-space-3); padding: var(--fs-space-2) var(--fs-space-3-5); font-family: var(--fs-font-sans); font-size: var(--fs-type); font-weight: var(--fs-weight); color: var(--fs-text); background: var(--fs-panel2); border: var(--fs-hairline); border-radius: var(--fs-radius-sm); cursor: pointer; } input[type="file"]::file-selector-button:hover { border-color: var(--fs-accent); color: var(--fs-accent); } /* row-action buttons (Edit/Delete/Start-Stop): neutral outline, variants colour the ones that * carry meaning. Neutral :hover comes FIRST — after it, it would grey the coloured variants * back out on hover at equal specificity. 32px clears WCAG 2.5.8's 24px target/spacing rule; * 28px did not (adjacent 24px circles overlapped). */ .cbi-section-actions .cbi-button, .cbi-section-actions .btn, .cbi-section-table .cbi-button { min-height: var(--fs-ctl-h-sm); padding: 0 var(--fs-space-3); font-size: var(--fs-type); /* flex-basis: auto, not a definite basis: with one, the automatic minimum resolves to 0 in * every row and flex-grow divides by count instead of by need — docs/css.md "Buttons: * `flex-basis: auto`, not the definite basis base gave every row action" has the * regression this fixed and the measured widths before/after. */ flex-basis: auto; } /* a ComboButton is a widget: its open list is a child of the box, and its label ellipsizes in its own
  • */ :is(.cbi-section-actions .cbi-button, .cbi-section-actions .btn, .cbi-section-table .cbi-button):not(.cbi-dropdown) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .td .cbi-button, .cbi-section-actions .cbi-button, .cbi-section-table .cbi-button { background: transparent; border: var(--fs-hairline); color: var(--fs-dim); } .td .cbi-button:hover { border-color: var(--fs-dim); color: var(--fs-text); } .td .cbi-button-action, .td .cbi-button-edit, .td .cbi-button-apply, .cbi-section-actions .cbi-button-edit, .cbi-section-actions .cbi-button-action { color: var(--fs-accent); border-color: var(--fs-accent-line); background: transparent; } /* hover ink is --fs-text, not the role colour: text of colour C on a tint of C eats its own * contrast (Save: accent on accent-soft = 4.25:1, AA fail) — role reads from fill + border */ .td .cbi-button-action:hover, .td .cbi-button-edit:hover, .td .cbi-button-apply:hover, .cbi-section-actions .cbi-button-action:hover { background: var(--fs-accent-soft); color: var(--fs-text); border-color: var(--fs-accent-line-hi); } /* an enabled/positive toggle in a table cell (System -> Startup) rendered blue text on a green fill = unreadable; green outline instead */ .td .cbi-button-positive, .cbi-section-actions .cbi-button-positive { color: var(--fs-good); border-color: var(--fs-good-line); background: transparent; } /* without this border-colour the positive toggle read less responsive than its hover neighbours (which brighten to -line-hi) */ .td .cbi-button-positive:hover, .cbi-section-actions .cbi-button-positive:hover { background: var(--fs-good-soft); color: var(--fs-text); border-color: var(--fs-good-line-hi); } .td .cbi-button-negative, .td .cbi-button-remove, .cbi-section-actions .cbi-button-remove, .cbi-section-actions .cbi-button-negative { color: var(--fs-danger); border-color: var(--fs-danger-line); background: transparent; } .td .cbi-button-negative:hover, .td .cbi-button-remove:hover, .cbi-section-actions .cbi-button-negative:hover { background: var(--fs-danger-soft); color: var(--fs-text); border-color: var(--fs-danger-line-hi); } /* Save & Apply is THE primary action, Save demoted so only one reads primary */ .cbi-page-actions .cbi-button-apply, .cbi-page-actions .cbi-dropdown .cbi-button-apply { background: var(--fs-accent); border: 1px solid transparent; color: var(--fs-on-accent); text-shadow: none; } /* a TINT at rest, not a bare outline, or Save reads disabled beside the solid Save & Apply; * -soft/-fill off the role ladder. Label --fs-text, not --fs-accent: accent on accent-soft * measures 4.25:1, an AA failure (docs/design-system.md). */ .cbi-page-actions .cbi-button-save, .cbi-page-actions .cbi-button-apply + .cbi-button-save, .cbi-page-actions .cbi-button-negative + .cbi-button-save { background: var(--fs-accent-soft); border: 1px solid var(--fs-accent-line); color: var(--fs-text); text-shadow: none; } .cbi-page-actions .cbi-button-save:hover { background: var(--fs-accent-fill); border-color: var(--fs-accent-line-hi); } /* buttons that are part of a compound field widget */ .cbi-dynlist > .add-item > button, .cbi-dynlist > .add-item > .cbi-button-add { /* min-width was a bare 38px, the number --fs-ctl-h already states — a restated value is the drift this ladder exists to prevent */ flex: 0 0 auto; width: auto; min-width: var(--fs-ctl-h); min-height: var(--fs-ctl-h-sm); padding: 0 var(--fs-space-3); margin: 0; border: 0; border-inline-start: var(--fs-hairline); border-radius: 0; background: transparent; color: var(--fs-dim); } .cbi-dynlist > .add-item > button:hover, .cbi-dynlist > .add-item > .cbi-button-add:hover { color: var(--fs-accent); } .control-group:has(> .cbi-input-password) > button.cbi-button-neutral { border: 0; border-radius: 0; margin: 0; background: transparent; color: var(--fs-dim); padding: 0 var(--fs-space-3); min-height: var(--fs-ctl-h); border-inline-start: var(--fs-hairline); } .control-group:has(> .cbi-input-password) > button.cbi-button-neutral:hover { color: var(--fs-accent); } /* SQUARE, ON THE LADDER: the only buttons once set in bare pixels (40x34, 34 being a height in * no scale) and opted out of the Density axis; square because the glyph is an arrow, not a label */ .pager .btn.prev, .pager .btn.next { flex: 0 0 auto; width: var(--fs-ctl-h); min-width: var(--fs-ctl-h); height: var(--fs-ctl-h); padding: 0; border: var(--fs-hairline); } .pager .btn.prev:hover, .pager .btn.next:hover { border-color: var(--fs-accent); color: var(--fs-accent); } }