@layer theme { /* FOOTSTRAP SHELL — layout + chrome over styles/base. * Markup: ucode/template/themes/footstrap/header.ut · spec: docs/chrome.md */ body { padding: 0; background-color: var(--fs-bg); font-family: var(--fs-font-sans); } /* 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: transform var(--fs-dur-move); } .fs-sidebar ul.nav, .fs-sidebar #modemenu { list-style: none; margin: 0; padding: 0; display: flex; } .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 do that for everything * else in the chrome, 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-sidebar ul.nav ul { list-style: none; } /* .fs-logout's display/align/colour are shared chrome (10-chrome.css) — do not restate * them; only its LAYOUT (square button vs full-width row) belongs to a 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 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 { 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 * (menu-footstrap.js) nudges one back inside near the right edge, every bar state. */ .fs-sidebar ul.nav ul, .fs-sidebar ul.nav > li.open > ul { display: 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 — only its PADDING differs (denser * inside a popup than in the accordion), which is all the vertical override restates. */ .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 Appearance 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; gap: var(--fs-space-1-5); order: 2; margin-inline-start: auto; padding: 0; } .fs-themerow { display: flex; align-items: center; justify-content: center; flex: 0 0 auto; order: 2; margin-inline-start: auto; /* see the :not(:empty) rule below — two auto margins would SPLIT the free space between * them instead of pushing one cluster to the bar's far edge */ width: var(--fs-btn-size); height: var(--fs-btn-size); padding: 0; border: var(--fs-hairline); border-radius: var(--fs-radius); background: transparent; color: var(--fs-dim); font: inherit; font-weight: var(--fs-weight); font-size: var(--fs-type); line-height: 1.1; text-align: start; cursor: pointer; transition: background var(--fs-dur), border-color var(--fs-dur), color var(--fs-dur); } /* .fs-ind-compact then .fs-bar-actrow, both by fitCluster (fs-chrome.js) — chrome.md, "Right * cluster: two fit steps". max-width releases with the flex basis, or the 30ch cap leaves * the brand short of the row it was just given. */ .fs-sidebar.fs-bar-actrow .fs-brandrow { flex: 1 1 100%; max-width: 100%; } /* the poll pill: label off, masked glyph on. Static green arrows, grey when the poll is paused — * the same --fs-icon-refresh as the rail, and the same reason it does not spin (a permanently * spinning glyph makes an idle router look busy, and a moving target cannot be clicked). */ .fs-sidebar.fs-ind-compact #indicators [data-indicator="poll-status"] { width: var(--fs-btn-size); padding: 0; font-size: 0; justify-content: center; position: relative; } .fs-sidebar.fs-ind-compact #indicators [data-indicator="poll-status"]::before { content: ""; width: var(--fs-icon-refresh-sz); height: var(--fs-icon-refresh-sz); background: var(--fs-good); /* @mirror poll-glyph/mask */ -webkit-mask: var(--fs-icon-refresh) center / var(--fs-icon-refresh-sz) no-repeat; mask: var(--fs-icon-refresh) center / var(--fs-icon-refresh-sz) no-repeat; /* @endmirror */ } .fs-sidebar.fs-ind-compact #indicators [data-indicator="poll-status"]:not([data-style="active"])::before { background: var(--fs-dim); } /* every OTHER indicator collapses to the same box and shows the badge fs-chrome.js lifted out of * its label — the count, or a dot when there is no number — exactly as the collapsed rail does. * min-width rather than width: three digits must not be clipped, and here there is room to grow. */ .fs-sidebar.fs-ind-compact #indicators [data-indicator]:not([data-indicator="poll-status"]) { min-width: var(--fs-btn-size); padding: 0 var(--fs-space-1); font-size: 0; justify-content: center; } .fs-sidebar.fs-ind-compact #indicators [data-indicator]:not([data-indicator="poll-status"])::after { /* @mirror ind-badge/paint */ content: attr(data-fs-badge); font-size: var(--fs-type-xs); font-weight: var(--fs-weight-bold); font-variant-numeric: tabular-nums; /* @endmirror */ } /* Exactly ONE member of the far-edge cluster may carry an auto inline-start margin, and it must be the * FIRST one actually rendered. #indicators is earlier in the DOM, so when the poll pill * shows IT opens the gap and .fs-themerow's auto must stand down — otherwise flexbox * SPLITS the free space between the two and the pill drifts away from the buttons. * .fs-themerow keeps its own auto for when the pill is empty and collapsed. */ .fs-sidebar #indicators:not(:empty) ~ .fs-themerow { margin-inline-start: 0; } /* …and there are TWO .fs-themerow buttons now (Search, then Appearance), so the same split * would happen between THEM whenever the pill is collapsed: the second one stands down too. */ .fs-themerow ~ .fs-themerow { margin-inline-start: 0; } .fs-logout { flex: 0 0 auto; order: 2; justify-content: center; width: var(--fs-btn-size); height: var(--fs-btn-size); padding: 0; border: var(--fs-hairline); border-radius: var(--fs-radius); background: transparent; } /* on a square button the BORDER carries the state — the fill and the ink come from the * shared hover/active groups in 10-chrome.css */ .fs-themerow:hover, .fs-themerow[aria-expanded="true"] { border-color: var(--fs-accent); } .fs-logout:hover { border-color: var(--fs-danger); } .fs-themerow .fs-label, .fs-logout .fs-label { display: none; } .fs-themerow .fs-ico, .fs-logout .fs-ico { width: 18px; height: 18px; flex: 0 0 auto; } .fs-sidebar ul.nav .fs-ico { flex: 0 0 auto; } /* The vertical sidebar: the single override, guarded POSITIVELY (never `:not([data-layout= * …])`); everything the bar declared above must be answered here. The `data-narrow` * measurement and the 521px JS-disabled floor: chrome.md, "`data-narrow`". */ @media (min-width: 521px) { /* Static, never sticky (issue #7) — chrome.md, "A static sidebar, never position: * sticky". Confined here; the top layout and the phone bar keep window scrolling. */ :root[data-layout="sidebar"]:not([data-narrow]) .fs-shell { flex-direction: row; height: 100svh; overflow: hidden; } :root[data-layout="sidebar"]:not([data-narrow]) .fs-main { height: 100svh; overflow-y: auto; /* Firefox-only overflow leak past the static sidebar (issue #12) — chrome.md, * "contain: paint … is the fix for issue #12". */ contain: paint; } :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar { width: var(--fs-sidebar-w); height: 100svh; flex: 0 0 auto; position: static; flex-direction: column; flex-wrap: nowrap; align-items: normal; gap: normal; padding: var(--fs-space-5) var(--fs-space-3-5); background: var(--fs-panel); border-inline-end: var(--fs-hairline); border-bottom: 0; box-shadow: none; /* `overflow: auto`, not overflow-y alone: the bar sets `overflow: visible` for its * popups, and a lone overflow-y here would leave overflow-x visible — which the * spec forces back to auto anyway. Say both. */ overflow: auto; z-index: auto; } /* the column is already 224px wide — that IS the cap, so the bar's 30ch one must go * or the brand is capped twice, at the wrong value */ :root[data-layout="sidebar"]:not([data-narrow]) .fs-brandrow { display: flex; align-items: center; gap: var(--fs-space-1-5); flex: 0 0 auto; order: 0; min-width: auto; max-width: none; padding: 0 var(--fs-space-1) var(--fs-space-5) var(--fs-space-2); } :root[data-layout="sidebar"]:not([data-narrow]) .fs-navlabel { display: block; color: var(--fs-dim); font-size: var(--fs-type-xs); font-weight: var(--fs-weight-bold); letter-spacing: .1em; text-transform: uppercase; padding: var(--fs-space-1-5) var(--fs-space-2-5) var(--fs-space-2); } :root[data-layout="sidebar"]:not([data-narrow]) .fs-spacer { display: block; flex: 1 1 auto; min-height: 12px; } :root[data-layout="sidebar"]:not([data-narrow]) .fs-railtoggle { display: inline-flex; } /* :not(:empty):not(.single) in the SHOW rule, not a separate hide pair: the bar's base * rule (top of this file) already hides .fs-modemenu, so an empty/single-mode strip * simply never opts in. */ :root[data-layout="sidebar"]:not([data-narrow]) .fs-modemenu:not(:empty):not(.single) { display: flex; margin-top: var(--fs-space-1); } /* vertical main nav */ :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav, :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar #modemenu { order: 0; flex: 0 1 auto; flex-direction: column; flex-wrap: nowrap; align-items: normal; gap: var(--fs-space-0-5); } :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav > li, :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav > li.has-sub { position: relative; } :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav > li > a { display: flex; align-items: center; gap: var(--fs-space-3); padding: var(--fs-space-2-5) var(--fs-space-3); /* Bold is the root level's own signal (--fs-weight-normal has no face of its own, so * lightening children reads as hierarchy instead); only the accordion needs it. */ font-size: var(--fs-type); font-weight: var(--fs-weight-bold); white-space: normal; line-height: 1.1; } :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav > li > a .fs-ico { display: block; } :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav > li > a .fs-chevron { display: block; } :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav .fs-ico, :root[data-layout="sidebar"]:not([data-narrow]) .fs-themerow .fs-ico, :root[data-layout="sidebar"]:not([data-narrow]) .fs-logout .fs-ico { width: 17px; height: 17px; } /* nested submenu -> an inline accordion, not a popup */ :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav ul { display: none; /* the bar anchors this panel under itself (absolute + top/left); an accordion is * just the next block in the column, so the offsets must go back to auto or they * linger in the computed style */ position: static; top: auto; inset-inline: auto; z-index: auto; width: auto; min-width: 0; max-width: none; flex-direction: column; gap: var(--fs-space-0-5); margin: 0 0 0 var(--fs-space-7); padding: 0; background: none; border: 0; border-radius: 0; box-shadow: none; overflow: hidden; } /* the bar's hover bridge does not follow: static here, its containing block would fall * back to the
  • and swallow clicks above it. The rail brings its own (::after). */ :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav > li.has-sub > ul::before { content: none; } /* An open section is a GROUP: an indent alone does not say where it ends, and after ten * children the next root category read as an eleventh (issue #37). No :not([data-rail]) * guard needed — the rail's own rule below restates these at higher specificity. */ :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav > li.open > ul { display: flex; margin: var(--fs-space-1) 0 var(--fs-space-3) var(--fs-space-7); padding-inline-start: var(--fs-space-1); border-inline-start: var(--fs-hairline); } :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav > li.has-sub.open > ul { display: flex; } :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav ul a { padding: var(--fs-space-2) var(--fs-space-3); } /* the Appearance trigger and Log out are full-width text rows down here */ :root[data-layout="sidebar"]:not([data-narrow]) .fs-themerow, :root[data-layout="sidebar"]:not([data-narrow]) .fs-logout { order: 0; margin-inline-start: 0; width: 100%; height: auto; justify-content: normal; gap: var(--fs-space-3); padding: var(--fs-space-2-5) var(--fs-space-3); border: 0; border-radius: var(--fs-radius); background: transparent; } :root[data-layout="sidebar"]:not([data-narrow]) .fs-themerow { text-align: start; } :root[data-layout="sidebar"]:not([data-narrow]) .fs-logout { font-weight: var(--fs-weight); font-size: var(--fs-type); } :root[data-layout="sidebar"]:not([data-narrow]) .fs-logout:hover { background: var(--fs-panel2); border-color: transparent; } :root[data-layout="sidebar"]:not([data-narrow]) .fs-themerow:hover, :root[data-layout="sidebar"]:not([data-narrow]) .fs-themerow[aria-expanded="true"] { border-color: transparent; } :root[data-layout="sidebar"]:not([data-narrow]) .fs-themerow .fs-label, :root[data-layout="sidebar"]:not([data-narrow]) .fs-logout .fs-label { display: block; flex: 1 1 auto; } /* the poll pill sits in the sidebar footer, above Appearance */ :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar #indicators { order: 0; margin-inline-start: 0; flex-wrap: wrap; gap: var(--fs-space-1-5); padding: 0 var(--fs-space-1-5) var(--fs-space-1); } /* In the column the indicator is one of a stack of ROWS, not a row of buttons — * Appearance and Log out below it run the sidebar's full width, so a pill at its own * content width read as a stray chip. */ :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar #indicators .label, :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar #indicators [data-indicator] { flex: 1 1 100%; justify-content: center; } } /* The Appearance controls used to live here (a popover on ). They are a PAGE now * (System -> Appearance): styles/pages/80-appearance.css. */ /* Collapsed sidebar (icon rail): , persisted in localStorage and * re-applied before paint by head.ut. Labels collapse and a section's submenu becomes a * flyout anchored right of its icon. The guard below must equal the vertical sidebar's * exactly — chrome.md, "The rail's guard must be the vertical sidebar's, exactly". */ @media (min-width: 521px) { :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar { width: var(--fs-rail-w); padding: var(--fs-space-5) var(--fs-space-2-5); /* the default `overflow-y: auto` computes overflow-x to `auto` as well, * which would clip the flyouts; the rail is short enough not to scroll */ overflow: visible; /* no z-index/sticky: static, like the vertical sidebar (issue #7) — its flyouts * still cover the content on their own --fs-z-flyout in the root stacking context. */ } :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-brandrow { flex-direction: column; gap: var(--fs-space-2-5); padding: 0 0 var(--fs-space-5); } :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-brand { flex: 0 0 auto; justify-content: center; } :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-railtoggle .fs-ico { rotate: 180deg; } /* only the rail's own labels collapse — a `.fs-label` also exists on every * submenu link, and those live inside the flyout where they must stay */ :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-wordmark, :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-navlabel, :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-modemenu, :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav > li > a > .fs-label, :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-themerow > .fs-label, :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-logout > .fs-label, /* a rail item has no accordion — its children fly out sideways — so the chevron says * nothing and crowds the icon. Selector kept this long because the vertical override * turns the chevron back on at (0,6,3), so a plainer form here would only be (0,6,0). */ :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav > li > a .fs-chevron { display: none; } :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav > li > a, :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-themerow, :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-logout { justify-content: center; gap: 0; padding: var(--fs-space-2-5) 0; } /* leaf sections have no flyout — show their name as a tooltip bubble */ :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav > li:not(.has-sub) > a::after { content: attr(data-label); position: absolute; inset-inline-start: 100%; top: 50%; transform: translateY(-50%); margin-inline-start: var(--fs-space-2-5); z-index: var(--fs-z-flyout); padding: var(--fs-space-1-5) var(--fs-space-2-5); border-radius: var(--fs-radius-sm); background: var(--fs-panel); border: var(--fs-hairline); box-shadow: var(--fs-shadow-pop); color: var(--fs-text); font-size: var(--fs-type); font-weight: var(--fs-weight); white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity var(--fs-dur); } :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav > li:not(.has-sub) > a:hover::after { opacity: 1; } /* the flyout: opened by hover on a mouse, or by .open which the menu JS toggles on tap * (exclusively — menu-footstrap.js flyoutMode). Hover gated on a real pointer: mobile * browsers leave :hover stuck on the tapped element, which would block tap-to-close. */ :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav > li.has-sub > ul { display: none; position: absolute; inset-inline-start: 100%; top: -8px; margin: 0; margin-inline-start: var(--fs-space-2-5); min-width: 196px; padding: var(--fs-space-2); z-index: var(--fs-z-flyout); background: var(--fs-panel); border: var(--fs-hairline); border-radius: var(--fs-radius-lg); box-shadow: var(--fs-shadow-pop); animation: fs-fade var(--fs-dur); } :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav > li.has-sub.open > ul { display: flex; } @media (hover: hover) { :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav > li.has-sub:hover > ul { display: flex; } } /* The flyout's heading reuses the bar's hover-bridge pseudo-element (issue #22) — * chrome.md, same heading as above. */ :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav > li > ul[data-title]::before { content: attr(data-title); position: static; inset: auto; height: auto; padding: var(--fs-space-1) var(--fs-space-3) var(--fs-space-2); color: var(--fs-eyebrow-color); font-size: var(--fs-type-xs); font-weight: var(--fs-eyebrow-weight); letter-spacing: var(--fs-eyebrow-tracking); text-transform: uppercase; } /* bridge the 10px gap so the pointer can travel icon -> flyout without a hover drop */ :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav > li.has-sub::after { content: ""; position: absolute; inset-inline-start: 100%; top: 0; width: 12px; height: 100%; } /* "Refreshing" -> a green masked refresh glyph. Needs the FULL guard, not [data-rail] * alone: data-rail stamps pre-paint, data-narrow lands later from an async module, so a * narrow phone would paint a bare glyph in the phone bar until the modules land. * --fs-ctl-h-sm follows the Density axis, one step under the bar's pill. */ :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar #indicators [data-indicator="poll-status"] { font-size: 0; width: var(--fs-ctl-h-sm); height: var(--fs-ctl-h-sm); padding: 0; border: 0; box-shadow: none; background: var(--fs-good); /* @mirror poll-glyph/mask */ -webkit-mask: var(--fs-icon-refresh) center / var(--fs-icon-refresh-sz) no-repeat; mask: var(--fs-icon-refresh) center / var(--fs-icon-refresh-sz) no-repeat; /* @endmirror */ /* Static, deliberately: this is a poll running quietly forever, not something being * waited on, and a permanently spinning glyph also broke the click that pauses it. */ } /* paused: the same glyph gone quiet — without it the rail kept saying GREEN while the * poll was stopped, the one thing the indicator exists to report. */ :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar #indicators [data-indicator="poll-status"]:not([data-style="active"]) { background: var(--fs-dim); } /* Badges its count instead of wrapping its prose (issue #14) — chrome.md, "A * collapsed-rail indicator badges its count". One rule per box: two would need an extra * attribute + :not() past the css-metrics specificity ratchet. `.fs-sidebar` is * deliberately absent: #indicators is an id. */ :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] #indicators [data-indicator]:not([data-indicator="poll-status"]) { /* min-width, not width: the count is unbounded (a big config can queue three digits) * and a fixed box would clip it. */ font-size: 0; min-width: var(--fs-ctl-h-sm); max-width: 100%; height: var(--fs-ctl-h-sm); box-sizing: border-box; padding: 0 var(--fs-space-1); overflow: hidden; display: inline-flex; align-items: center; justify-content: center; } /* .fs-ind-compact above paints the same badge (mirror-pinned) but cannot SHARE this * rule: it applies at every width, this one must stay inside the 521px floor. */ :root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] #indicators [data-indicator]:not([data-indicator="poll-status"])::after { /* @mirror ind-badge/paint */ content: attr(data-fs-badge); font-size: var(--fs-type-xs); font-weight: var(--fs-weight-bold); font-variant-numeric: tabular-nums; /* @endmirror */ } } /* ---------- main column ---------- */ .fs-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; background: var(--fs-bg); overflow-x: clip; } .fs-sidebar #indicators:empty { display: none; } .fs-indicators { flex-wrap: wrap; } .fs-content { padding: var(--fs-space-5) var(--fs-content-pad); flex: 1 1 auto; width: 100%; max-width: var(--fs-content-max); margin: 0 auto; } /* fs-router.js builds the incoming page in a second #view here, then moves its children into * the live one, so the visible page is never emptied while the next one loads. `visibility: * hidden` (not display: none) keeps the box so a self-sizing view sees no zero width; * `height: 0` stops the page moving; `overflow: clip` (not hidden) skips a second scrollport. */ .fs-staging { visibility: hidden; height: 0; overflow: clip; } /* Slow-navigation hairline, shown once a nav outlives 150ms (fs-router.js). Fixed, and a * child of , not .fs-main (a column flex container shrinks its 2px to zero) or * .fs-shell (contain: paint would re-base and clip a fixed descendant). transform/opacity * only, both composited, so drawing this never costs the render it reports on. */ #fs-nav-progress { position: fixed; inset-inline: 0; top: 0; height: 2px; background: var(--fs-accent); transform: scaleX(0); transform-origin: left center; opacity: 0; pointer-events: none; z-index: var(--fs-z-tooltip); } :root[dir="rtl"] #fs-nav-progress { transform-origin: right center; } #fs-nav-progress[data-state="active"] { opacity: 1; transform: scaleX(.8); transition: transform 2s cubic-bezier(.1, .7, .1, 1), opacity 120ms linear; } #fs-nav-progress[data-state="done"] { opacity: 0; transform: scaleX(1); transition: transform 120ms ease-out, opacity 300ms linear 100ms; } @media (prefers-reduced-motion: reduce) { #fs-nav-progress[data-state="active"], #fs-nav-progress[data-state="done"] { transition: none; transform: scaleX(1); } } /* ---------- footer ---------- * Bare `footer`, not `footer.fs-footer`: the theme owns footer paint outright, so a * third-party view emitting its own