mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-11 02:44:57 +08:00
205 lines
11 KiB
CSS
205 lines
11 KiB
CSS
@layer theme {
|
|
/* ---- the chrome's floor: pin what a foreign rule can reach by INHERITANCE ----
|
|
*
|
|
* fs-sheets.js fences the chrome out of a third-party selector, so nothing foreign MATCHES a
|
|
* chrome element. Inheritance is the way in a fence cannot close: a rule on `html` or `body` is
|
|
* an ancestor, and excluding it is meaningless. Measured against a hostile flagged rule on
|
|
* `html`: font-style reached 166 of the menu's 169 elements, word-spacing and text-align 157,
|
|
* letter-spacing and text-transform 156, cursor 46 — while font-family, color, font-size,
|
|
* line-height and font-weight reached none,
|
|
* because the chrome already states those. The fix is to state the rest too.
|
|
*
|
|
* Inheritance is not a cascade competitor: it supplies a value only when no declaration matches
|
|
* the element, so any declaration of ours — unflagged, at zero specificity — beats an inherited
|
|
* `!important`. Hence no `!important` here.
|
|
*
|
|
* It keys off `[data-fs-chrome]`, the mark a chrome root carries in the markup (header.ut), not
|
|
* off `.fs-sidebar`: the chrome is not one element, and a pin naming the menu leaves the skip
|
|
* link and the search overlay inheriting straight from `html`. The fence (fs-sheets.js) reads
|
|
* the same mark, so the two halves of zone 1's defence cannot cover different sets of elements,
|
|
* and `npm run chrome-fence` derives the mark from the markup and holds both to it.
|
|
*
|
|
* It goes on the ROOT alone, and says so in the SELECTOR (`:not([data-fs-chrome] *)`) rather
|
|
* than trusting whoever adds the next marker to have read this: the markers live in a template
|
|
* with conditional blocks, so "is this one inside that one" is not a question a text scanner can
|
|
* answer, and CSS can. Pinning descendants too is worse than useless — a direct declaration
|
|
* beats an inherited one even when the inherited one is ours, so `.fs-label` loses the `nowrap`
|
|
* it inherits and `text-align` is forced from `start` to `left` on 302 elements, which alone
|
|
* breaks every RTL language LuCI ships. Hence `text-align: start`, never `left`.
|
|
*
|
|
* `:where()` keeps it at 0,0,0 so every existing chrome rule outranks it: the pin is a floor,
|
|
* never a ceiling. Only inherited properties belong here. */
|
|
:where([data-fs-chrome]:not([data-fs-chrome] *)) {
|
|
font-style: normal;
|
|
font-variant: normal;
|
|
letter-spacing: normal;
|
|
word-spacing: normal;
|
|
text-transform: none;
|
|
text-indent: 0;
|
|
text-align: start;
|
|
cursor: auto;
|
|
}
|
|
|
|
/* Shared chrome — brand, logo, wordmark, logout, indicators. Same in both layouts;
|
|
* only their PLACEMENT differs. Written twice once (here and under `.fs-topnav`) and
|
|
* the copies had drifted: 32 vs 30px logo, 11 vs 10px brand gap, 10 vs 8px indicator
|
|
* gap. The top-bar values won. A layout file may set placement on these (flex, order,
|
|
* the rail collapse) — never their look. */
|
|
|
|
.fs-brand {
|
|
display: flex; align-items: center; gap: var(--fs-space-2-5);
|
|
color: var(--fs-text); font-weight: var(--fs-weight-bold);
|
|
}
|
|
|
|
.fs-logo {
|
|
width: var(--fs-btn-size); height: var(--fs-btn-size); border-radius: var(--fs-radius);
|
|
/* No accent fill — a bordered box, same look as the square buttons in the right
|
|
* cluster (.fs-logout, .fs-themerow: 1px var(--fs-border)). The glyph is the OpenWrt
|
|
* favicon mark (brand.ut), baked-colour, so color/currentColor is unused here. */
|
|
border: var(--fs-hairline);
|
|
display: flex; align-items: center; justify-content: center;
|
|
flex: 0 0 auto;
|
|
}
|
|
.fs-logo svg { width: 20px; height: 20px; }
|
|
/* The OpenWrt ring flips with the theme mode like the SVG favicon does under
|
|
* prefers-color-scheme — dark ring on the light UI, light ring on dark. The cyan arcs
|
|
* stay baked (read on both). */
|
|
.fs-logo-ring { fill: #002b49; }
|
|
:root[data-darkmode="true"] .fs-logo-ring { fill: #e6f4fb; }
|
|
|
|
/* 700, not 800: the theme ships no faces, so 800 is whatever the reader's Manrope or system
|
|
* stack has — 700 for most of them, synthesised for the rest. It already resolved to 700 back
|
|
* when the 18 KB face was bundled and dropped for drawing six elements. Say so. */
|
|
.fs-wordmark { font-size: var(--fs-type-lg); font-weight: var(--fs-weight-bold); letter-spacing: -.01em; }
|
|
|
|
/* Skip link — the first tab stop: a keyboard/SR user jumps the whole menu instead of
|
|
* tabbing through every section on every page. Off-screen until focused, never
|
|
* display:none / visibility:hidden — that removes it from the tab order, which is the
|
|
* one thing it exists for. Sits above everything, incl. an open popover. */
|
|
.fs-skip {
|
|
position: fixed; top: 8px; inset-inline-start: 8px; z-index: var(--fs-z-dropdown);
|
|
padding: var(--fs-space-2-5) var(--fs-space-4); border-radius: var(--fs-radius);
|
|
background: var(--fs-accent); color: var(--fs-on-accent);
|
|
font-weight: var(--fs-weight-bold); font-size: var(--fs-type); text-decoration: none;
|
|
box-shadow: var(--fs-shadow-pop);
|
|
}
|
|
/* Clipped, not translated off-screen: --fs-shadow-pop is offset 12px / blur 34px, so a
|
|
* box parked at -68px still bled ~16px of DROP SHADOW into the viewport and smudged the
|
|
* top-left corner of every page. clip-path clips the shadow too. Never display:none /
|
|
* visibility:hidden — see above. */
|
|
.fs-skip:not(:focus) {
|
|
clip-path: inset(50%);
|
|
width: 1px; height: 1px;
|
|
padding: 0; overflow: hidden; white-space: nowrap;
|
|
}
|
|
.fs-skip:focus {
|
|
outline: 2px solid var(--fs-panel); outline-offset: 2px;
|
|
}
|
|
/* Visually hidden but PRESENT in the a11y tree — for the document <h1> and the live
|
|
* region. hidden/display:none/visibility:hidden take the element out of the a11y tree
|
|
* too: the h1 would fix no heading outline and the live region would never announce.
|
|
* Clipping leaves a node readable to assistive tech while occupying no space.
|
|
* nowrap: a long title clipped into a 1px column is read out line by line by some SRs. */
|
|
.fs-sr {
|
|
position: absolute;
|
|
clip-path: inset(50%);
|
|
width: 1px; height: 1px;
|
|
margin: -1px; padding: 0;
|
|
overflow: hidden; white-space: nowrap;
|
|
}
|
|
|
|
/* the skip link's target <main> carries tabindex="-1" so the browser actually MOVES
|
|
* focus there (Safari/Chrome refuse on a non-focusable target: they scroll but leave
|
|
* focus in the menu) — but it must not then ring the whole page. */
|
|
#maincontent:focus { outline: none; }
|
|
|
|
.fs-logout { display: flex; align-items: center; color: var(--fs-dim); }
|
|
.fs-logout:hover { color: var(--fs-danger); }
|
|
|
|
.fs-indicators { display: flex; align-items: center; gap: var(--fs-space-2); }
|
|
|
|
/* The 34px square icon button (.fs-themerow / .fs-logout collapsed) is NOT declared
|
|
* here: it only exists where the chrome is a bar or a rail, and those blocks
|
|
* (20-shell.css, 50-toplayout.css) already give it its border, surface and size — an
|
|
* unconditional copy here was a second definition they had to re-state anyway. */
|
|
|
|
/* Density steps for the main menu once it is a horizontal row —
|
|
* fs-chrome.js (fitTabStrips) adds .fs-dense1/2 when the row would wrap (and strips them
|
|
* in the vertical sidebar, so no media guard is needed). Padding is trimmed hard
|
|
* first, the font only at the d2 floor. */
|
|
.fs-sidebar ul.nav.fs-dense1 > li > a { padding: var(--fs-space-1) var(--fs-space-2); }
|
|
.fs-sidebar ul.nav.fs-dense2 > li > a { padding: var(--fs-space-1) var(--fs-space-1-5); font-size: var(--fs-type); }
|
|
|
|
/* Menu primitives, once for both layouts (both render the menu as `ul.nav > li > a`):
|
|
* the 19px line-height, `display: block` on the items and the `position: relative`
|
|
* every submenu popup anchors to — all three used to be inherited from a navbar rule
|
|
* set the theme no longer carries. */
|
|
ul.nav { position: relative; margin: 0; list-style: none; }
|
|
ul.nav > li { display: block; }
|
|
ul.nav a { line-height: 19px; text-decoration: none; }
|
|
|
|
/* `.dropdown-menu` is emitted by nobody here — not by this theme, whose menu is a plain nested
|
|
* `ul`, and not by luci-base. It stays styled under the coverage contract: it is theme
|
|
* vocabulary (stock luci-theme-bootstrap carries 14 rules for it), so a third-party luci-app-*
|
|
* is entitled to emit it and expect a themed result. Each item anchors its own nested popup,
|
|
* hence position: relative.
|
|
*
|
|
* This comment used to claim ui.js emits it for the split Save & Apply button. It does not —
|
|
* that button is a `.cbi-dropdown`, a different widget. */
|
|
.dropdown-menu li { display: block; position: relative; }
|
|
/* wins over `ul.nav a` above: the popup sets its own denser rhythm */
|
|
ul.nav .dropdown-menu a, .dropdown-menu a { line-height: 18px; }
|
|
|
|
/* Menu hover / active — one look, every layout. One markup (`ul.nav > li > a` + a
|
|
* nested `ul`) serves the main list, the submenu (accordion / bar dropdown / rail
|
|
* flyout), the mode menu and the Appearance row, so there are no per-layout copies to
|
|
* keep in step. */
|
|
.fs-sidebar ul.nav > li > a:hover,
|
|
.fs-sidebar ul.nav ul a:hover,
|
|
.fs-themerow:hover {
|
|
background: var(--fs-panel2); color: var(--fs-text); opacity: 1;
|
|
}
|
|
.fs-sidebar ul.nav > li > a.active,
|
|
.fs-sidebar ul.nav ul a.active,
|
|
.fs-themerow[aria-expanded="true"],
|
|
.fs-modemenu li.active a {
|
|
background: var(--fs-accent-soft); color: var(--fs-accent);
|
|
}
|
|
/* ---- the fill marks the PAGE, and a category is not a page ----
|
|
*
|
|
* `li.active` is set on the SECTION containing the current page as well as on the page's own
|
|
* row, so `li.active > a` hands the same accent plate to both — two identical plates, one above
|
|
* the other, on System -> System (issue #37): measured before this rule, the open category and
|
|
* the current leaf carried the same fill, the same ink and the same 13px/600, differing in 2px
|
|
* of padding.
|
|
*
|
|
* The markup already says which is which: menu-footstrap.js puts `aria-current="page"` on the
|
|
* LEAF alone, a section header being a disclosure control rather than a link to where you are.
|
|
* So the plate stays with the page and the ancestor keeps the accent INK, while an ancestor
|
|
* that is also the page is matched by `a.active` above and keeps the plate.
|
|
*
|
|
* Unscoped, like the rule above it: the accordion, the bar's dropdown and the rail's flyout are
|
|
* one markup. */
|
|
.fs-sidebar ul.nav > li.active > a:not(.active) { color: var(--fs-accent); }
|
|
|
|
/* Keyboard focus for the chrome controls, which otherwise have only the UA default.
|
|
* --fs-focus-ring-solo, not --fs-focus-ring: none of these flips a border on focus, so the ring
|
|
* is the entire indicator, and the tinted ring measures 1.15-1.29:1 against the sidebar — i.e.
|
|
* tabbing the whole menu shows nothing at 3:1 (the two-ring contract in 02-tokens.css). */
|
|
.fs-railtoggle:focus-visible,
|
|
.fs-themerow:focus-visible,
|
|
.fs-logout:focus-visible,
|
|
.fs-sidebar ul.nav a:focus-visible {
|
|
outline: none;
|
|
box-shadow: var(--fs-focus-ring-solo);
|
|
}
|
|
|
|
/* The page-wide accent signals no widget rule covers, all following the palette:
|
|
* ::selection the UA default (opaque blue) ignores palette and dark mode
|
|
* caret-color inherits from :root into every field, in one declaration
|
|
* accent-color the theme's own checkboxes are appearance:none drawings and never see it,
|
|
* but a third-party app's native checkbox/radio/progress does */
|
|
::selection { background: var(--fs-accent); color: var(--fs-on-accent); }
|
|
:root { caret-color: var(--fs-accent); accent-color: var(--fs-accent); }
|
|
}
|