@layer base { /* Widgets with no generic equivalent: CBI section tables, interface/zone badges, * the uci change dialog, spinners, the file browser, and a few rules luci-mod-* * depends on. */ /* .hidden: theme/60-inputs.css */ form.inline { display: inline; margin-bottom: 0; } /* #syslog: absorbed into pages/70-syslog.css — in base it LOST to the theme layer's generic * form-field box (a layer beats specificity), so the log rendered as a 210px column. */ /* :not(.placeholder): the empty-table row gets no hover cue (theme/30-tables.css spares it), * but this rule paints the CELL and a placeholder row carrying no `.cbi-section-table-row` * class escapes that guard — measured on UPnP (issue #15). */ .cbi-section-table .tr:not(.placeholder):hover .td, .cbi-section-table .tr:not(.placeholder):hover .th, .cbi-section-table .tr:not(.placeholder):hover::before { background-color: var(--fs-panel2); } .cbi-section-table .tr.cbi-section-table-descr .th { font-weight: var(--fs-weight-normal); } .cbi-section-table-titles { font-weight: var(--fs-weight-bold); } /* GridSection name column, discriminated by markup not release: docs/css.md "base/95-luci.css: * the GridSection name column, read from the markup, not the release" (issue #9). Column * geometry lives in theme/30-tables.css, next to the real cells it lines up with. */ .cbi-section-table-titles.named[data-title]::before, .cbi-section-table-titles.named[data-title] ~ .cbi-section-table-descr.named::before, .cbi-section-table-row[data-title]:is(:not(:has(> .cbi-section-table-titles)))::before { content: attr(data-title) " "; display: table-cell; vertical-align: middle; } .left { text-align: left !important; } .right { text-align: right !important; } .center { text-align: center !important; } .top { vertical-align: top !important; } .middle { vertical-align: middle !important; } .bottom { vertical-align: bottom !important; } table table td, .cbi-value-field table td { border: none; } /* cell-filling field widths (+ numeric-cell refinement): theme/30-tables.css — * absorbed with the generic field box, which would out-layer them here */ .table.cbi-section-table .td.cbi-section-table-cell { white-space: nowrap; text-align: end; } .td.cbi-section-actions { text-align: end; vertical-align: middle; width: 15%; } .td.cbi-section-actions > * { display: flex; } .td.cbi-section-actions > :not(.cbi-dropdown) > *, .td.cbi-section-actions > * > form > * { flex: 1 1 4em; margin: 0 var(--fs-space-1); /* WCAG 2.2 SC 2.5.8: 1px apart, targets sat inside each other */ } .td.cbi-section-actions > * > form { display: inline-flex; margin: 0; } .cbi-rowstyle-2, .tr.table-titles, .tr.cbi-section-table-titles { background: var(--fs-panel2); } th[data-sortable-row] { cursor: pointer; } th[data-sort-direction="asc"]::after { content: "\a0\25b2"; } th[data-sort-direction="desc"]::after { content: "\a0\25bc"; } /* Indent and the ::before glyph's `inset-inline-start` are one relationship (both 1.25em), * held in one local. */ .cbi-value-description { --help-indent: 1.25em; margin-block: var(--fs-space-1) 0; margin-inline: var(--help-indent) 0; position: relative; } .cbi-value-description:not(:empty)::before { /* OURS, drawn as a stroke like --spinner-icon below and --fs-icon-refresh in 02-tokens.css * (fill:none, round caps, 24 grid) — not the solid filled path luci-theme-bootstrap used. */ --help-icon: url("data:image/svg+xml,"); position: absolute; inset-inline-start: calc(var(--help-indent) * -1); content: "\a0"; display: inline-block; width: 1em; height: 1em; margin-inline-end: var(--fs-space-1); background: var(--fs-accent); mask-image: var(--help-icon); mask-size: cover; -webkit-mask-image: var(--help-icon); -webkit-mask-size: cover; } /* tint (6%-not-12% reasoning): theme/30-tables.css — absorbed */ .cbi-section-error { border: 1px solid var(--fs-danger); border-radius: var(--fs-radius); padding: var(--fs-space-2); margin-bottom: var(--fs-space-4); } .cbi-section-error ul { margin-block: 0; margin-inline: var(--fs-space-5) 0; } .cbi-section-error ul li { font-weight: var(--fs-weight-bold); /* colour: theme/35-alerts.css — absorbed */ } .ifacebox { border: var(--fs-hairline); margin: 0 var(--fs-space-3); text-align: center; white-space: nowrap; border-radius: var(--fs-radius-sm); display: inline-flex; flex-direction: column; line-height: var(--fs-leading-tight); /* a plate of single-line labels, not prose */ min-width: 100px; } .ifacebox .ifacebox-head { border-bottom: var(--fs-hairline); /* inside the box's 1px border, so radius = the box's less that border */ border-radius: calc(var(--fs-radius-sm) - 1px) calc(var(--fs-radius-sm) - 1px) 0 0; padding: var(--fs-space-0-5); } /* !important fights luci-mod-network's inline zone-colour style=: docs/css.md "base/95-luci.css: * the dark-mode zone tint, and why it has no grey fallback". */ [data-darkmode="true"] .zonebadge[style], [data-darkmode="true"] .ifacebox-head[style] { background: linear-gradient(rgba(var(--zone-color-rgb), .4), rgba(var(--zone-color-rgb), .3)) !important; } /* generalised once: docs/css.md "base/95-luci.css: the overflow-wrap fix generalised once, not * restated per page". */ .ifacebox .ifacebox-body { padding: var(--fs-space-1); white-space: normal; overflow-wrap: anywhere; } #cbi-network-interface .ifacebox img { width: 24px; height: 24px; } .ifacebadge { display: inline-block; border: var(--fs-hairline); padding: var(--fs-space-0-5); color: var(--fs-text); border-radius: var(--fs-radius-sm); cursor: default; line-height: var(--fs-leading-tight); } .ifacebadge img { width: 32px; height: 32px; vertical-align: middle; } .ifacebadge-active { font-weight: var(--fs-weight-bold); } .network-status-table { display: flex; flex-wrap: wrap; } .network-status-table .ifacebox { margin: var(--fs-space-2); flex-grow: 1; } .network-status-table .ifacebox-body { display: flex; flex-direction: column; height: 100%; text-align: start; } /* same fix as .ifacebox-head above, same doc section (RU caption "Подключение IPv4 * (внешняя сеть)" measured 41.3px past #view at 320px/Large, 0px in English). */ .network-status-table .ifacebox-head { white-space: normal; overflow-wrap: anywhere; } .network-status-table .ifacebox-body > * { margin: var(--fs-space-1); } .network-status-table .ifacebox-body > span { flex: 10 10 auto; height: 100%; } .network-status-table .ifacebox-body > div { margin: -.125em; display: flex; flex-wrap: wrap; } #dsl_status_table .ifacebox-body span > strong { display: inline-block; min-width: 35%; } .ifacebadge.large, .network-status-table .ifacebox-body .ifacebadge { flex: 1; padding: var(--fs-space-1); /* capped at the field: 220px and an unbroken IPv6 ran a modal's Status badge 215px past its field at 360px */ min-width: min(220px, 100% - 2 * var(--fs-space-1)); margin: var(--fs-space-1); } .ifacebadge.large > * { min-width: 0; overflow-wrap: anywhere; } .network-status-table .ifacebox-body .ifacebadge { display: flex; } .ifacebadge.large { display: inline-flex; } .network-status-table .ifacebox-body .ifacebadge > span { overflow: hidden; text-overflow: ellipsis; } .ifacebadge > *, .ifacebadge.large > * { margin: 0 var(--fs-space-1); } .zonebadge { padding: var(--fs-space-0-5); border-radius: var(--fs-radius-sm); display: inline-block; white-space: nowrap; color: var(--fs-text); } .zonebadge > em, .zonebadge > strong { margin: 0 var(--fs-space-1); display: inline-block; } /* .zonebadge input width: theme/30-tables.css — absorbed with the generic box */ .zonebadge > .ifacebadge { margin-inline-start: var(--fs-space-0-5); } .zonebadge-empty { border: 1px dashed var(--fs-border); /* width/style only — colour + ink: theme/45-misc.css */ font-style: italic; font-size: smaller; } div.cbi-value var, .td.cbi-value-field var { font-style: italic; /* colour: theme/60-inputs.css — absorbed */ } div.cbi-value var[data-tooltip], .td.cbi-value-field var[data-tooltip], div.cbi-value var.cbi-tooltip-container, .td.cbi-value-field var.cbi-tooltip-container { cursor: help; border-bottom: 1px dotted var(--fs-accent); } div.cbi-value var.cbi-tooltip-container, .td.cbi-value-field var.cbi-tooltip-container .cbi-tooltip { font-style: normal; white-space: normal; color: var(--fs-text); } #modal_overlay > .modal.uci-dialog, #modal_overlay > .modal.cbi-modal { max-width: 900px; } #modal_overlay > .modal.login > button.important { /* Coverage vocabulary, not a live control: no luci-base call site passes ui.showModal a * `login` class, and this theme's own login is a server-rendered form. Inherited from * luci-theme-bootstrap so a foreign app emitting it still gets a themed result. */ font-size: var(--fs-type-lg); margin-top: var(--fs-space-2); width: 100%; } .uci-change-list { line-height: var(--fs-leading); /* prose leading for a list of changed settings, not a hand 170% */ white-space: pre; } .uci-dialog div > del, .uci-dialog div > ins, .uci-dialog div > var { margin-bottom: var(--fs-space-0-5); border: var(--fs-hairline); line-height: var(--fs-leading-tight); /* one row of single-line code takes the tight step */ overflow: hidden; text-overflow: ellipsis; padding: var(--fs-space-0-5); position: relative; background-color: var(--fs-panel2); } .uci-dialog div > ins { background-color: var(--fs-good-fill); border-color: var(--fs-good-line); } .uci-dialog div > del { background-color: var(--fs-danger-fill); border-color: var(--fs-danger-line); } .uci-dialog var > ins { background-color: var(--fs-good-fill); } .uci-dialog var > del { background-color: var(--fs-danger-fill); } .uci-change-list > var > * { overflow: hidden; text-overflow: ellipsis; } .uci-dialog del, .uci-dialog ins, .uci-dialog var { text-decoration: none; font-family: var(--fs-font-mono); font-style: normal; color: var(--fs-text); display: block; } .uci-change-legend { display: flex; flex-wrap: wrap; } .uci-change-legend-label { flex-basis: 150px; margin: var(--fs-space-0-5); display: flex; align-items: center; } .uci-change-legend-label > ins, .uci-change-legend-label > del, .uci-change-legend-label > var { margin-inline-end: var(--fs-space-1); width: 16px; height: 16px; display: flex; } .uci-change-legend-label > * > * { flex-basis: 100%; padding: 1px; } #applyreboot-section { line-height: 300%; } @keyframes flash { 0% { opacity: 1; } 50% { opacity: .5; } 100% { opacity: 1; } } .flash { animation: flash var(--fs-dur-fade); } #view > div.spinning:first-child { display: table; margin: 15vh auto; } /* Rotate in CSS, never a SMIL inside the data-URI: that runs in the SVG's * own document, out of reach of the page's `prefers-reduced-motion`. In-flow glyph, not * absolutely positioned: docs/css.md "base/95-luci.css: the spinner glyph sits in the flow, on * purpose" (issues #15, #22). */ .spinning::before { --spinner-icon: url("data:image/svg+xml,"); display: inline-block; flex: 0 0 auto; vertical-align: middle; margin-inline-end: var(--fs-space-1); width: var(--fs-space-5); /* one reader, so the size lives here rather than behind a token */ height: var(--fs-space-5); content: " "; background: var(--fs-text); /* palette ink, not CanvasText: a foreign `:root{color-scheme}` flips that against the theme */ mask: var(--spinner-icon) center/cover no-repeat; -webkit-mask: var(--spinner-icon) center/cover no-repeat; /* two deliberate exceptions to the motion scale (02-tokens.css): a LOOP, not a state change * — 1s is a spinner period not one of the four durations, and `linear` avoids the stutter * `ease` gives an infinite rotation at each turn's seam. */ animation: fs-spin 1s linear infinite; } /* A button's glyph is its label ink, so it cannot match the fill under it (white on Save's pale * tint measures 1.22:1). A bare `.spinning` keeps the page ink above. */ :is(.btn, .cbi-button).spinning::before { background: currentColor; } /* One user (.spinning::before above) — the rail's poll glyph is deliberately STATIC * (theme/20-shell.css). Unscoped: keyframe names resolve document-wide, a luci-app-* may ride it. */ @keyframes fs-spin { to { transform: rotate(360deg); } } [data-tab-title] { height: 0; opacity: 0; overflow: hidden; } /* the revealed state of both collapsibles — tab pane and file browser — in one rule */ [data-tab-active="true"], .cbi-filebrowser.open { opacity: 1; height: auto; overflow: visible; transition: opacity var(--fs-dur-fade); } /* min(210px, 100%) floor: docs/css.md "base/95-luci.css: the file browser's floor, and the row * that is not text". */ .cbi-filebrowser { min-width: min(210px, 100%); max-width: 100%; border: var(--fs-hairline); /* width/style only; radius: theme/60-inputs.css — absorbed */ display: flex; flex-direction: column; opacity: 0; height: 0; overflow: hidden; } /* padding: theme/60-inputs.css — absorbed. Zero side padding here let a 36px themed button * touch this element's own clip box and lose its corners. */ .cbi-filebrowser > * { max-width: 100%; overflow: hidden; text-overflow: ellipsis; margin: .25em .25em 0 .25em; white-space: nowrap; border-bottom: var(--fs-hairline); } .cbi-filebrowser .cbi-button-positive { margin-inline-end: .25em; } /* .right is not one line of text (upload strip + Create/Cancel), so it opts out of the * nowrap+ellipsis assumption above: docs/css.md "base/95-luci.css: the file browser's floor, * and the row that is not text". */ .cbi-filebrowser > .right { white-space: normal; overflow: visible; } .cbi-filebrowser > div { border-bottom: none; } .cbi-filebrowser > ul > li { display: flex; } /* row hover surface + flex sizing: theme/60-inputs.css — absorbed (this file's old * name:10/actions:3 split fitted base's ~20px buttons, not the theme's 36px ones). */ .cbi-filebrowser > ul > li > div:first-child { overflow: hidden; text-overflow: ellipsis; } .cbi-filebrowser > ul > li > div:last-child { text-align: end; } .cbi-filebrowser > ul > li > div:last-child > button { padding: .125em .25em; } .cbi-filebrowser .upload { display: flex; flex-wrap: wrap; margin: 0 -.125em .25em -.125em; padding: 0 0 .125em 0; border-bottom: var(--fs-hairline); } .cbi-filebrowser .upload > * { margin: .125em; flex: 1; } .cbi-filebrowser .upload > .btn { flex-basis: 60px; } .cbi-filebrowser .upload > div { flex: 10; min-width: 150px; } /* the upload input's width: theme/60-inputs.css — absorbed with the generic box */ @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes fade-out { 0% { opacity: 1; } 100% { opacity: 0; } } /* `fade-in` stays although nothing here rides it: an upstream animation NAME a luci-app-* may * ride (coverage contract, docs/conventions.md). The theme's own `.fade-in` CLASS rule is gone * on purpose — luci-mod-status re-marks it on every poll tick, replaying anything hung there. * `fs-fade` is one-shot instead, played once by the rail flyout and the search overlay. * docs/css.md "Misc: fade-in animates nothing, on purpose". */ @keyframes fs-fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } } .fade-out { animation: fade-out var(--fs-dur-fade); } .assoclist .ifacebadge { display: flex; flex-direction: column; align-items: center; white-space: normal; text-align: center; } .assoclist .ifacebadge > img { margin: var(--fs-space-1); } .assoclist .td:nth-of-type(3), .assoclist .td:nth-of-type(5) { width: 25%; } /* PNGs on a white ground, inverted for dark mode: docs/css.md "base/95-luci.css: Statistics * graphs and Realtime SVGs are art drawn for a white ground" — 180deg is exact for the three * primaries, not tuned further. */ [data-darkmode="true"] :where(#view)[data-page="admin-statistics-graphs"] [data-plugin] img { filter: invert(100%) hue-rotate(180deg); } /* Realtime SVGs carry inline style= for a black ground; only an author !important beats it: * same doc section. */ #view div[style] > svg { background-color: var(--fs-panel); } #view div[style] > svg line[style] { stroke: var(--fs-text) !important; } /* child combinator, not descendant: reaches only the axis labels, not Channel Analysis's * per-BSSID curve labels nested inside a — same doc section. */ #view div[style] > svg > text[style] { fill: var(--fs-text) !important; text-shadow: none !important; font-weight: var(--fs-weight-bold); } /* Overview Hide/Show toggle pills: theme/35-alerts.css (.cbi-title .label), the chevron in * theme/45-misc.css, the 16px pill in pages/20-overview.css — absorbed. */ }