@layer theme { /* FOOTSTRAP SHELL — layout + chrome over styles/base. * Markup: ucode/template/themes/footstrap/header.ut · spec: docs/chrome.md */ body { background-color: var(--fs-bg); font-family: var(--fs-font-sans); } /* One stroke for every chrome icon (header.ut, svgIcon()): 5 attributes per were 300 B of each admin page */ .fs-ico, .fs-chevron { fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; } /* placement only — the look lives in 10-chrome.css */ .fs-brand { flex: 1 1 auto; min-width: 0; } /* Hostname handling, two rules for two failures — chrome.md, "Hostname handling". */ .fs-wordmark { overflow-wrap: anywhere; min-width: 0; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; } /* collapse the sidebar to an icon rail. The toggle only EXISTS in the vertical sidebar, * so its `display` is declared there, not here, or it would just be shadowed. */ .fs-railtoggle { align-items: center; justify-content: center; width: calc(28px * var(--fs-density-box)); height: calc(28px * var(--fs-density-box)); padding: 0; flex: 0 0 auto; border: 1px solid transparent; border-radius: var(--fs-radius-sm); background: transparent; color: var(--fs-faint); cursor: pointer; transition: background var(--fs-dur), border-color var(--fs-dur), color var(--fs-dur); } .fs-railtoggle:hover { color: var(--fs-text); background: var(--fs-panel2); border-color: var(--fs-border); } .fs-railtoggle .fs-ico { width: 16px; height: 16px; transition: rotate var(--fs-dur-move); } /* base `ul` gives a 24px inline-start margin and a disc; padding is already 0 (reset) */ .fs-modemenu { list-style: none; margin: 0; } .fs-sidebar ul.nav > li > a .fs-label { flex: 1 1 auto; } /* collapse chevron for sections with children */ .fs-chevron { width: 14px; height: 14px; flex: 0 0 auto; color: var(--fs-dim); transition: rotate var(--fs-dur-move); } /* `rotate`, not `transform: rotate()`: individual transform properties compose across rules, * so the RTL mirror below sets `scale` without either rule restating the other's value. */ .fs-sidebar ul.nav > li.open > a .fs-chevron { rotate: 90deg; } /* The two chevrons are DIRECTION, not decoration, so with the document flipped (head.ut * stamps dir) they must be mirrored explicitly — logical properties flip the box sides * (margin, padding, inset, radius), but a glyph needs the transform. */ :root[dir="rtl"] .fs-chevron, :root[dir="rtl"] .fs-railtoggle .fs-ico { scale: -1 1; } .fs-sidebar ul.nav > li.has-sub > a { cursor: pointer; } /* .fs-logout shares the .fs-themerow button rule below (display, colour, box); only its * LAYOUT (square button vs full-width row) differs per layout block. */ /* The bar is the default chrome and the only copy of it, unguarded — chrome.md, "The bar: * fit steps, scroll model, a rail that must match its guard". The vertical sidebar below is * the single guarded override, winning on SPECIFICITY, never on source order. */ .fs-shell { display: flex; flex-direction: column; min-height: 100svh; width: 100%; /* svh not dvh — chrome.md, same heading */ } .fs-sidebar { display: flex; width: 100%; height: auto; position: sticky; top: 0; z-index: var(--fs-z-sticky); flex-direction: row; flex-wrap: wrap; align-items: center; gap: var(--fs-space-2-5); padding: var(--fs-space-2-5) var(--fs-space-4); background: var(--fs-bar-bg); border-inline-end: 0; border-bottom: var(--fs-hairline); /* the submenu popups anchor to this bar and must escape it */ overflow: visible; } /* brand may shrink (min-width: 0) but is capped at 30ch, tracking the text not the viewport; * the extra line grows the bar's own min-height (50-toplayout.css). */ .fs-brandrow { flex: 0 1 auto; order: 1; min-width: 0; max-width: min(100%, 30ch); padding: 0; } /* the bar has no section caption, no column to collapse, and its height follows its * content — so the flex filler has nothing to fill */ .fs-navlabel, .fs-spacer, .fs-railtoggle, .fs-modemenu { display: none; } /* the mode strip opts in at 2+ modes; empty (before renderModeMenu in fs-chrome.js runs: no gap, no brand * shift) or `.single` stays hidden by the line above. */ .fs-modemenu:not(:empty):not(.single) { display: flex; } /* the menu on its own full-width row beneath the brand. The DESKTOP bar lifts it back * onto the brand's row whenever it fits — MEASURED, not a breakpoint * (fs-chrome.js fitChrome adds .fs-bar-stack when it does NOT fit). */ .fs-sidebar ul.nav { display: flex; order: 3; flex: 1 1 100%; flex-direction: row; flex-wrap: wrap; align-items: center; gap: var(--fs-space-0-5); } /* anchors the dropdown to its OWN item, not the bar — issue #19, chrome.md, "A bar dropdown * anchors to its own
  • ". A phone never shows it: `hover: none` disables the rule. */ .fs-sidebar ul.nav > li.has-sub { position: relative; } .fs-sidebar ul.nav > li > a { display: flex; align-items: center; padding: var(--fs-space-1-5) var(--fs-space-3); border-radius: var(--fs-radius); color: var(--fs-dim); font-size: var(--fs-type); font-weight: var(--fs-weight); white-space: nowrap; } /* a bar item is its label: no icon, no chevron */ .fs-sidebar ul.nav > li > a .fs-ico, .fs-sidebar ul.nav > li > a .fs-chevron { display: none; } /* popup card under its own item, sized to content and capped to the viewport; clampDropdown * (fs-chrome.js) nudges one back inside near the inline-end edge, every bar state. */ .fs-sidebar ul.nav ul { display: none; list-style: none; position: absolute; inset-inline: 0 auto; top: calc(100% + 8px); z-index: var(--fs-z-flyout); width: max-content; min-width: 200px; max-width: calc(100vw - 32px); margin: 0; padding: var(--fs-space-2); background: var(--fs-panel); border: var(--fs-hairline); border-radius: var(--fs-radius-lg); box-shadow: var(--fs-shadow-pop); } /* an invisible bridge across the 8px gap, so travelling from the item down into the panel * does not drop the hover and close it. Unguarded: every state that can open a panel on * hover needs it, not just one layout. */ .fs-sidebar ul.nav > li.has-sub > ul::before { content: ""; position: absolute; inset-inline: 0; top: -10px; height: 10px; } /* hover only on a real pointer: a tap leaves :hover stuck on mobile browsers, which * would block tap-to-close */ @media (hover: hover) { .fs-sidebar ul.nav > li.has-sub:hover > ul { display: block; } } .fs-sidebar ul.nav > li.has-sub.open > ul { display: block; } /* a submenu link is the same link in every layout, the accordion included. */ .fs-sidebar ul.nav ul a { display: block; padding: var(--fs-space-2) var(--fs-space-3); border-radius: var(--fs-radius-sm); color: var(--fs-dim); font-size: var(--fs-type); font-weight: var(--fs-weight); } /* right cluster: the poll pill, then Search and Log out as square icon buttons. * LuCI's #indicators (poll "Refreshing", unsaved changes) collapses when empty, so it * only shows when there is something to report. */ .fs-sidebar #indicators { display: flex; flex-wrap: wrap; align-items: center; gap: var(--fs-space-1-5); order: 2; margin-inline-start: auto; padding: 0; } /* the square icon button both far-edge controls are; .fs-themerow adds only what a