@layer theme { /* The realtime graphs size their drawing from #view, not from the box they draw into: * `width = document.querySelector('#view').offsetWidth - 2`, in every realtime view and in the * luci-app-*-status copies of them. That assumption holds only in a theme whose .cbi-section has * no gutter; ours is a card, 16px of padding plus a 1px border, so the canvas comes out 34px * narrower than the drawing — and the views draw NEWEST-first from the right, so the freshest * samples are the ones falling off the edge. 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 plus border, derived from --fs-card-pad rather than * restated, or narrowing the gutter would silently re-clip every graph. * * The `!important` fights the inline `width: 100%` the view writes on the box: nothing in a * cascade layer can outrank an inline declaration. */ #view .cbi-section > div[style]:has(> svg) { width: auto !important; margin-inline: calc(-1 * (var(--fs-card-pad) + 1px)); } /* …and the same box's frame, which the same inline style paints BLACK: every realtime view writes * `border:1px solid #000;background:#fff` on it. Black is a colour no palette of this theme * contains, so on a dark surface the graph reads as a framed cut-out, and the frame lands * exactly on the card's own border edge because of the bleed above (issue #32). * * Wider than the bleed rule on purpose: channel_analysis.js hangs off a bare [data-tab] div and * must not be bled, but its border is just as black, so this asks only "an inline-styled box * wrapping an ". * * Only the COLOUR moves. The 1px stays, being the graph's own edge, and `background: #fff` is * left alone: the is sized to the box's content edge, so the white never paints. Only an * author `!important` outranks an inline declaration, and `border-color` alone is enough — the * width and style already agree with what the theme would ask for. */ #view div[style]:has(> svg) { border-color: var(--fs-border) !important; } /* 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 / 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 `MAC: ac:1f:6b:…`: the strong * names the datum, the text after it IS the datum. The theme puts mono on those surfaces, the * strong inherits it, and also means weight 700 — so the label asks for a bold mono * face that exists for no other reason: 227 elements across seven pages, and 20 KB of a * 600-weight mono face fetched to draw the word "MAC:". * * It must SET the face, not merely be excluded from the mono rule: an excluded element still * INHERITS its parent's. Excluding it was the first attempt and changed nothing. * * pre/code/#syslog/textarea are exempt: there a is part of the monospaced CONTENT. */ :is(strong, b):not(pre *, code *, #syslog *, textarea *) { font-family: var(--fs-font-sans); } /* Same argument, two holdouts. `.ifacebadge` is a BADGE — chrome, not a value — sitting in a mono * cell, and `.ifacebadge-active` makes it bold; sans keeps the active/inactive weight cue (WCAG * 1.4.1, that distinction not being carried by colour) with faces the page already has. * ``/`
` hold a LITERAL, which does not inherit its container's emphasis.
	 *
	 * The second selector is not redundant: the mono rule in theme/60-inputs.css is (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 instead, at (0,2,0). */
	.ifacebadge,
	.cbi-value-field .ifacebadge { font-family: var(--fs-font-sans); }
	code, pre { font-weight: var(--fs-weight-normal); }

	/* ---- the poll's `fade-in` is not animated at all, and that is the whole rule ----
	 *
	 * `fade-in` is upstream's class and luci-mod-status re-marks with it on EVERY poll tick — each
	 * section, and the wrapper holding all of them — so anything this theme hangs on that class
	 * replays on every tick across the entire content column.
	 *
	 * A fade plus a 4px slide moved the page under the reader; the fade alone still pulsed the whole
	 * column on every tick, reported from an iPhone as shaking that happens WITHOUT SCROLLING AT
	 * ALL, which is what named it — a poll tick is the only thing that happens on a still page.
	 *
	 * So the theme animates nothing on it. The class stays in the markup, being upstream's, and the
	 * `fade-in` keyframes in base/95-luci.css stay for the coverage reason stated there. */
	.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), and a data table cards by MEASUREMENT
	 * (.fs-stacked, fs-select.js) at any width.
	 *
	 * Query the CONTENT width, not the viewport: the sidebar eats width, so a viewport breakpoint
	 * stacks 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-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 nearer of the two. */
	.cbi-map { container-type: inline-size; container-name: 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  — so every @container query
	 * above resolves to nothing in there and no table stacks: on a phone the Edit-interface dialog
	 * rendered its form at desktop widths and cut the inputs off. Carry both names so a dialog
	 * stacks exactly like the page, a third-party app's modal included. */
	.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). Stock LuCI hands h2 to a page title
	 * and h3/legend to a section title, and the heading-card rule below plus theme/30-tables.css
	 * repaint exactly those two, so most of the ramp is only ever rendered by a third-party
	 * luci-app-* — styled anyway, coverage being 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 it, and
	 * a member here would fight that at equal specificity, i.e. on source order. */
	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: var(--fs-hairline);
		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: var(--fs-hairline);
		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. Nothing gives it a bottom
	 * margin, so it sits flush against the first .cbi-section below and reads as part of that card
	 * instead of as a page-level action; the same 16px every .cbi-section uses.
	 *
	 * And a top margin for the same reason at the other edge, the row sitting directly under the
	 * page heading with nothing between them. Half the bottom gap, because a heading and its own
	 * action belong together. */
	.cbi-title-buttons { margin-block: var(--fs-space-2) var(--fs-card-gap); }

	/* The per-section Delete button (`.cbi-section-remove`, what luci-base renders for every NAMED
	 * section: SQM's queues, the firewall's zones, any app that lets you add and drop instances) sat
	 * flush against whatever follows it — usually that section's own tab bar. Reported upstream on
	 * SQM, but nothing here is about SQM: it is one class luci-base gives every such section, and
	 * the gap belongs to the pattern rather than to the page. */
	.cbi-section-remove { margin-bottom: var(--fs-space-2); }

	/* A top-level block in a map that is not a section still keeps the page's rhythm.
	 *
	 * A view may return a bare widget where a section is expected — `s.render = () => new
	 * ui.Textarea(...).render()` hands the map a plain `
` with no `.cbi-section` — so nothing * gives that block the gap every section carries and the next section starts against it. Stock * bootstrap measures the same 0px, but there a section is a stretch of page and here it is a * CARD, so the missing gap reads as two cards fused. * * Written against what a map CONTAINS rather than against one app: the exclusions are the * children luci-base itself puts there and which own their spacing already — the map heading and * its description, the action bar, a tabbed map's tab bar and pane, and sections. * * `:not(:empty)` because a map can hold a placeholder that renders nothing. */ .cbi-map > *:not(.cbi-section):not(.cbi-map-descr):not(.cbi-page-actions):not(h2):not(legend):not(.cbi-tabmenu):not(.cbi-map-tabbed):not(:empty) { margin-bottom: var(--fs-card-gap); } }