mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-13 20:04:45 +08:00
199 lines
12 KiB
CSS
199 lines
12 KiB
CSS
@layer theme {
|
|
/* The realtime graphs (Status → Realtime *) size their drawing from #view — NOT from the box
|
|
* they draw into: `width = document.querySelector('#view').offsetWidth - 2`, in bandwidth.js,
|
|
* load.js, wireless.js and connections.js, and in the luci-app-*-status copies of them. That
|
|
* assumption only holds in a theme whose .cbi-section has no gutter (luci-theme-bootstrap's has
|
|
* none, which is why the page is whole there). Ours is a card: 16px padding + a 1px border, so
|
|
* the canvas came out 34px narrower than the drawing, and the views draw NEWEST-first from the
|
|
* right — the freshest samples were the ones falling off the edge. Measured on the router, on
|
|
* every realtime page and at every width: #view 1158 vs svg 1124. It reads as a phone bug only
|
|
* because the loss is absolute: 34px of a 1124px desktop plot is 3%, of a 322px phone plot 10%.
|
|
*
|
|
* So bleed the graph box back out to the card's border edge, making the canvas exactly
|
|
* #view - 2 again. The bleed is padding + border, and it is derived from --fs-card-pad rather
|
|
* than restated, or narrowing the gutter would silently re-clip every graph.
|
|
*
|
|
* Scoped by SHAPE, not by page: a graph is an inline-styled box wrapping an <svg>, one card
|
|
* deep. The luci-app-*-status graphs are third-party pages this theme has never seen, and they
|
|
* inherit the bug by copying the stock view. channel_analysis.js is deliberately NOT matched —
|
|
* it measures its OWN box, so it is already correct, and it hangs off a bare [data-tab] div
|
|
* rather than a .cbi-section. The !important fights the inline `width: 100%` on the box; nothing
|
|
* in a cascade layer can outrank that. */
|
|
#view .cbi-section > div[style]:has(> svg) {
|
|
width: auto !important;
|
|
margin-inline: calc(-1 * (var(--fs-card-pad) + 1px));
|
|
}
|
|
|
|
/* WCAG 2.2 SC 1.4.1 (Use of Color, A): inside a block of text a link may not be identified
|
|
* by COLOUR ALONE — it needs a non-colour cue or 3:1 against the surrounding text, and
|
|
* --fs-accent on --fs-text measures 1.4:1 in dark mode. So: underline links in prose.
|
|
* Scoped to prose deliberately — nav links, buttons, pills and table cells are identified
|
|
* by position and shape, and underlining them would be noise. */
|
|
p a, dd a,
|
|
.cbi-value-description a,
|
|
.alert-message a:not(.btn):not(.cbi-button) {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* ==================================================================
|
|
* A <strong>/<b> is a LABEL, and a label is UI text — so it takes the UI face even on a
|
|
* monospaced surface. LuCI builds every status readout as `<strong>MAC:</strong>
|
|
* ac:1f:6b:…`: the strong names the datum, the text after it IS the datum. The theme puts
|
|
* mono on those surfaces (.cbi-value-field, data-table cells, ifacebox bodies), the strong
|
|
* inherited it, and <strong> also means weight 700 — so the label asked for a BOLD MONO
|
|
* face that exists for no other reason. Measured on the live router: 227 elements across
|
|
* seven pages, 20 KB of jetbrains-mono-600 fetched to draw them — 30% of the theme's whole
|
|
* font payload, spent on the word "MAC:". Manrope 700 is already downloaded: zero bytes.
|
|
*
|
|
* It must SET the face, not merely be excluded from the mono rule — an excluded element
|
|
* still INHERITS its parent's. That was the first attempt and it changed nothing.
|
|
*
|
|
* pre/code/#syslog/textarea are exempt: there a <strong> is part of the monospaced CONTENT
|
|
* (a highlighted config line, a log level), not a label for it, and its bold resolves onto
|
|
* the mono 400 face, which is still shipped.
|
|
* ================================================================== */
|
|
:is(strong, b):not(pre *, code *, #syslog *, textarea *) {
|
|
font-family: var(--fs-font-sans);
|
|
}
|
|
|
|
/* Same argument, the last two holdouts — found by measuring:
|
|
* .ifacebadge is a BADGE — chrome, not a value. It sits in a mono cell and
|
|
* .ifacebadge-active makes it bold, so the four active firewall zone badges were the last
|
|
* thing on the router asking for a bold mono face. Sans keeps the active/inactive weight cue
|
|
* (WCAG 1.4.1 — that distinction is not carried by colour) with faces the page already has.
|
|
* <code>/<pre> hold a LITERAL, which does not inherit its container's emphasis:
|
|
* `<code>/etc/ethers</code>` inside a .cbi-value-title (600) was asking for bold mono.
|
|
*
|
|
* The second selector is not redundant: the mono rule in theme/60-inputs.css is
|
|
* `.cbi-value-field *:where(…)` = (0,1,0), a TIE with a bare `.ifacebadge` — and a tie is
|
|
* settled by SOURCE ORDER, which 60-inputs would win. Win on specificity (0,2,0) instead. */
|
|
.ifacebadge,
|
|
.cbi-value-field .ifacebadge { font-family: var(--fs-font-sans); }
|
|
code, pre { font-weight: var(--fs-weight-normal); }
|
|
|
|
/* fade-in used by overview poll */
|
|
.fade-in { animation: fs-fade var(--fs-dur-fade); }
|
|
@keyframes fs-fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
.cbi-title-ref { color: var(--fs-accent); }
|
|
.close { color: var(--fs-text); text-shadow: none; opacity: .6; }
|
|
.close:hover { opacity: 1; }
|
|
.ifacebox { background: var(--fs-panel); border-color: var(--fs-border); }
|
|
.ifacebox .ifacebox-head { background: var(--fs-panel2); color: var(--fs-text); }
|
|
/* The active-interface highlight lives in THIS layer, next to the head it overrides: the
|
|
* same rule in base was dead — a layer beats specificity, and the plain `.ifacebox-head`
|
|
* above repainted `.active` along with the rest. */
|
|
.ifacebox .ifacebox-head.active { background: var(--fs-accent); color: var(--fs-on-accent); }
|
|
.ifacebadge { background: var(--fs-panel2); border-color: var(--fs-border); }
|
|
.ifacebadge-active { border-color: var(--fs-accent); }
|
|
.zonebadge-empty { border-color: var(--fs-border); color: var(--fs-dim); }
|
|
|
|
/* ==================================================================
|
|
* The @container PLUMBING for the responsive tables — only WHICH boxes are query containers,
|
|
* under what names. The card rules are NOT here: a CBI section table cards under `@container
|
|
* fs-content (max-width: 960px)` (theme/65-dropdown.css), a data table cards by MEASUREMENT
|
|
* (.fs-stacked, fs-select.js) at any width — which is why no data-table threshold appears in
|
|
* this file.
|
|
*
|
|
* Query the CONTENT width, not the viewport: the sidebar eats width, so a viewport breakpoint
|
|
* would stack too late (or too early) depending on layout.
|
|
*
|
|
* NAMED, deliberately — an unnamed @container binds to the nearest ancestor that happens to
|
|
* have a container-type, so giving container-type to any new component would silently
|
|
* re-target every unnamed query underneath it.
|
|
* fs-map this element specifically (a CBI form's content column)
|
|
* fs-view the overview's grid (pages/20-overview.css, @container 800)
|
|
* fs-content "whatever content column I am in" — carried by BOTH .cbi-map and #view, so a
|
|
* query resolves to the NEAREST of the two. That is the point of sharing it.
|
|
* ================================================================== */
|
|
.cbi-map { container-type: inline-size; container-name: fs-map fs-content; }
|
|
#view { container-type: inline-size; container-name: fs-view fs-content; }
|
|
|
|
/* A modal is NOT inside #view — ui.js appends #modal_overlay to <body> — so every
|
|
* @container query above resolved to nothing in there and no table stacked: on a phone the
|
|
* Edit-interface dialog rendered its form at desktop widths and cut the inputs off at the
|
|
* dialog's right edge. Carry BOTH names so a dialog stacks its .cbi-section-table
|
|
* (fs-content) and its data tables (fs-view) exactly like the page — including any
|
|
* third-party app's modal. */
|
|
.modal { container-type: inline-size; container-name: fs-view fs-content; }
|
|
|
|
/* pager (« prev · text · next ») — generic, not page-scoped: any .pager gets an inline
|
|
* centred row with compact square buttons. */
|
|
.pager.center { display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center; justify-content: center; gap: var(--fs-space-2-5); padding: var(--fs-space-3-5) 0; }
|
|
.pager .text { flex: 0 0 auto; font-size: var(--fs-type); color: var(--fs-dim); }
|
|
|
|
/* The heading ramp — absorbed from base wholesale (docs/css.md, the "native elements and
|
|
* typography" category). Stock LuCI hands h2 to a page title and h3/legend to a section
|
|
* title, and the heading-card rule below + theme/30-tables.css repaint exactly those two —
|
|
* so most of the ramp is only ever rendered by a third-party luci-app-*; it is styled
|
|
* anyway, coverage is a contract. Every step is a scale token and the ramp is monotonic.
|
|
* `legend` is deliberately NOT in the group: theme/30-tables.css owns legend (and carries
|
|
* the ramp's ink), and a member here would fight it at equal specificity, i.e. on source
|
|
* order — the one thing a cascade must not rest on. */
|
|
h1, h2, h3, h4, h5, h6 {
|
|
font-weight: var(--fs-weight-bold);
|
|
color: var(--fs-text);
|
|
/* 1.5 is prose leading; a heading is a label and looks slack at it. */
|
|
line-height: var(--fs-leading-tight);
|
|
}
|
|
h1 { margin-bottom: var(--fs-space-4); font-size: var(--fs-type-2xl); }
|
|
h2 { font-size: var(--fs-type-xl); }
|
|
h3 { font-size: var(--fs-type-lg); }
|
|
h4 { font-size: var(--fs-type); }
|
|
h5 { font-size: var(--fs-type); }
|
|
h6 { font-size: var(--fs-type-xs); color: var(--fs-dim); text-transform: uppercase; }
|
|
|
|
/* Heading card (h2 + .cbi-map-descr): everything sitting directly on the page background
|
|
* gets a .cbi-section card's frame, so a page reads as a stack of panels instead of loose
|
|
* text floating over the (possibly patterned) background. */
|
|
.cbi-map > h2,
|
|
#view > h2 {
|
|
background: var(--fs-panel);
|
|
border: 1px solid var(--fs-border);
|
|
border-radius: var(--fs-radius-lg);
|
|
box-shadow: var(--fs-shadow);
|
|
margin: 0 0 var(--fs-card-gap);
|
|
padding: var(--fs-space-4);
|
|
font-size: var(--fs-type-xl);
|
|
font-weight: var(--fs-weight-bold);
|
|
color: var(--fs-text);
|
|
}
|
|
/* a heading followed by a description: split the card across the two siblings (no wrapper
|
|
* element exists to hold both) */
|
|
.cbi-map > h2:has(+ .cbi-map-descr),
|
|
#view > h2:has(+ .cbi-map-descr) {
|
|
border-bottom: 0;
|
|
border-radius: var(--fs-radius-lg) var(--fs-radius-lg) 0 0;
|
|
padding-bottom: var(--fs-space-1);
|
|
margin-bottom: 0;
|
|
box-shadow: none;
|
|
}
|
|
.cbi-map > h2 + .cbi-map-descr,
|
|
#view > h2 + .cbi-map-descr {
|
|
background: var(--fs-panel);
|
|
border: 1px solid var(--fs-border);
|
|
border-top: 0;
|
|
border-radius: 0 0 var(--fs-radius-lg) var(--fs-radius-lg);
|
|
box-shadow: var(--fs-shadow);
|
|
margin: 0 0 var(--fs-card-gap);
|
|
padding: 0 var(--fs-space-4) var(--fs-space-4);
|
|
color: var(--fs-dim);
|
|
font-size: var(--fs-type);
|
|
line-height: var(--fs-leading);
|
|
}
|
|
|
|
/* An action-button row (a `.right` wrapper holding Add/Save) sitting directly under a field
|
|
* had no gap above it — the apk "Repo Public Keys -> Add key" paste box, and every other
|
|
* paste-a-value-then-submit layout, pinned its button to the textarea. Scoped to a `.right`
|
|
* immediately following a form control, so it never touches right-aligned text or cells. */
|
|
#view :is(textarea, input[type="text"], input[type="password"], .cbi-dropdown) + .right {
|
|
margin-top: var(--fs-space-2-5);
|
|
}
|
|
|
|
/* A button row belonging to the PAGE TITLE rather than to any section (`.cbi-title-section >
|
|
* .cbi-title-buttons`; Status → Channel Analysis puts "Refresh Channels" there, a
|
|
* third-party app may put anything). Nothing gave it a bottom margin, so it sat flush
|
|
* against the first .cbi-section below and read as part of that card instead of as a
|
|
* page-level action. Same 16px every .cbi-section already uses. */
|
|
.cbi-title-buttons { margin-bottom: var(--fs-card-gap); }
|
|
}
|