/* ══════════════════════════════════════════════════════════════════════
   Nivasik Layout — TWA / PWA optimised
   Breakpoints:
     ≥ 900px  → full sidebar + topbar (desktop/large tablet)
     600-899px → sidebar hidden, topbar, bottom nav (small tablet / large phone)
     < 600px  → full mobile: bottom nav, collapsed topbar, single column
   ══════════════════════════════════════════════════════════════════════ */

/* ── App shell ───────────────────────────────────────────────────────────
   `overflow-x: clip` contains a wide table WITHOUT making this element a
   scroll container. With `overflow: hidden` here (and on .main) the sidebar
   and topbar were sticky in name only: measured at scrollTop 600 they sat at
   -600px, i.e. scrolled clean off the screen. clip → both measure 0. */
.app {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  min-height: 100dvh; /* dynamic viewport height: adjusts for browser chrome */
  min-height: 100vh;  /* fallback */
  width: 100%;
  /* NEVER put a plain `overflow: hidden` here.
     .app wraps the entire dashboard and is only min-height:100dvh, so
     `overflow:hidden` clipped everything past the first viewport AND gave no
     way to reach it: the page simply would not scroll. It appeared to "work
     on toggle" because opening the mobile drawer changes the layout enough to
     expose a different scroller.
     `clip` constrains the horizontal axis without turning this into a scroll
     container, so overflow-y stays truly visible and the document scrolls
     normally. `hidden` is the fallback for engines without `clip`; on those it
     computes overflow-y to auto, which is harmless here because the element
     has no fixed height and therefore grows instead of scrolling. */
  overflow-x: hidden;   /* fallback for engines without `clip` */
  overflow-x: clip;
}

/* ── Sidebar ─────────────────────────────────────────────────────────── */
.sidebar {
  /* --surface-chrome, not --surface: the frame must not be the same
     material as what it frames. */
  background: var(--surface-chrome);
  border-right: 1px solid var(--border);
  padding: var(--sp-3) 10px;
  padding-top: calc(var(--sp-3) + var(--safe-top));
  position: sticky;
  top: 0;
  height: 100dvh;
  height: 100vh;
  overflow-y: auto;
  overflow-x: hidden;
  display: flex;
  flex-direction: column;
  /* Smooth scroll on iOS */
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none; /* Firefox */
}
.sidebar::-webkit-scrollbar { display: none; }

.brand {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 6px 8px var(--sp-5) 8px;
  flex-shrink: 0;
}
.brand-mark {
  width: 28px; height: 28px;
  border-radius: 8px;
  /* --grad-brand, not a per-theme gradient of --primary + --accent. Those
     two are INK roles and both go dark in light mode, so the mark became a
     dark teal tile carrying near-black initials at 3.10:1. --grad-brand is
     identical in both themes and is the one fill that owns --on-accent. */
  background: var(--grad-brand);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--on-accent);
  font-size: var(--fs-caption);
  flex-shrink: 0;
}
.brand-name {
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 500;
  letter-spacing: -0.01em;
}

.nav-section { margin-bottom: var(--sp-3); }
/* ── Sidebar filter ──────────────────────────────────────────────────────
   Forty-three entries scan fine and scroll badly. This is the cheap fix: it
   costs nothing until it is used, unlike a shorter list, which costs a click
   on every visit.

   No transition anywhere in here on purpose. It runs on every keystroke, and
   animating a list that redraws as fast as someone types is the top row of the
   frequency table — the case for no motion at all. */
.nav-filter { padding: 0 2px var(--sp-2); }
.nav-filter-input {
  width: 100%;
  box-sizing: border-box;
  padding: 7px 10px;
  font: inherit;
  font-size: var(--fs-small);
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  -webkit-appearance: none;
  appearance: none;
}
.nav-filter-input::placeholder { color: var(--muted); }
.nav-filter-input:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
  border-color: var(--primary);
}
/* Safari draws its own clear affordance on type=search; it lands on the wrong
   ground colour here and Escape already clears. */
.nav-filter-input::-webkit-search-decoration,
.nav-filter-input::-webkit-search-cancel-button { -webkit-appearance: none; }

/* The section picker sits under the search box. Same surface and border as the
   input so the two read as one control, and NO transition on either: this whole
   block redraws on every keystroke, which is the top row of the frequency
   table. The arrow is drawn rather than left to the UA — a native select arrow
   is the one part of a themed sidebar that stays the OS's colour. */
.nav-filter-select {
  width: 100%;
  box-sizing: border-box;
  margin-top: 6px;
  padding: 7px 26px 7px 10px;
  font: inherit;
  font-size: var(--fs-small);
  color: var(--text);
  background-color: var(--surface-2);
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 12px center, right 7px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
}
.nav-filter-select:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
  border-color: var(--primary);
}
/* The popup list is drawn by the OS and inherits nothing, so the options need
   their own colours or a dark sidebar gets a white menu on Windows. */
.nav-filter-select option { background: var(--surface-2); color: var(--text); }

.nav-empty {
  padding: var(--sp-2) 10px;
  font-size: var(--fs-tiny);
  color: var(--muted);
}

.nav-section-title {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tr-micro);
  /* --muted-2 is the placeholder/hover-border token; as text it is 4.70:1
     on dark --surface and 3.81:1 on --surface-3. Never text. */
  color: var(--text-soft);
  padding: var(--sp-1) 10px 5px;
}
.nav-link {
  position: relative;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 10px;
  /* A nav link is a control read at a glance, so it takes --text.
     --text-soft is the body-copy role. Active state is carried by the
     tinted fill, the weight and the rail, not by ink strength. */
  color: var(--text);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  cursor: pointer;
  /* transform is in this list so the bottom-nav press has something to ease on
     in BOTH directions. This shorthand redeclares .nav-link's transition and
     would otherwise defeat the one in motion.css, giving 0ms in and 0ms out. */
  transition: background-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
  min-height: 40px;
  border: 1px solid transparent;
  text-decoration: none;
}
.nav-link:hover  { background: var(--surface-2); color: var(--text); }
.nav-link:active { background: var(--surface-3); }
.nav-link.active { background: var(--primary-soft); color: var(--text); font-weight: 500; }
/* Active rail — reads as "you are here" without boxing the row in */
.sidebar .nav-link.active::before {
  content: '';
  position: absolute;
  left: -10px; top: 50%;
  transform: translateY(-50%);
  width: 3px; height: 20px;
  border-radius: 0 2px 2px 0;
  background: var(--primary);
}
.nav-link .icon  { width: 16px; height: 16px; opacity: 0.8; flex-shrink: 0; }
.nav-link .icon svg { width: 100%; height: 100%; display: block; }
.nav-link.active .icon { opacity: 1; color: var(--primary); }
.nav-link .tier-lock {
  margin-left: auto;
  font-size: 10px;
  color: var(--text-soft);
  background: var(--surface-2);
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
}

/* ── Main area ───────────────────────────────────────────────────────── */
.main {
  display: flex;
  flex-direction: column;
  min-width: 0;
  max-width: 100%;
  overflow-x: hidden; /* fallback */
  overflow-x: clip;   /* keeps .topbar sticky — see .app note above */
}

/* ── Topbar ──────────────────────────────────────────────────────────── */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Inside the app the WebView runs edge to edge UNDER the status bar, and it
     has to: Android 15 (targetSdk 35) deprecated setStatusBarColor to a no-op,
     so the only way the bar matches the theme is for the page itself to paint
     behind it. --bg on .topbar is what makes that happen, and this inset is
     what keeps the clock off the property name.

     env() is 0 in every browser, so the web layout is byte-identical. */
  height: calc(var(--topbar-h) + var(--safe-top));
  /* LONGHAND on purpose. `padding: var(--safe-top) ... ` was silently dropped
     by the Android WebView — --safe-top resolves to a max() and the shorthand
     would not parse it, so the bar grew to the right height with zero padding
     and the controls stayed centred under the clock. The longhand parses. */
  padding: 0 var(--sp-5);
  padding-top: var(--safe-top);
  border-bottom: 1px solid var(--border);
  background: var(--bg);
  position: sticky;
  top: 0;
  z-index: 50;
  gap: var(--sp-2);
  flex-shrink: 0;
}
/* The topbar's frosting is ONE rule and it is at the bottom of this file, on
   `.sidebar, .topbar`, using the chrome material. A second copy used to sit
   here behind `@media (min-width: 901px)` and painted --scrim, the MODAL
   tint, rather than --chrome-glass, the chrome one — and the later rule beat
   it at every width, so the width guard and the tint were both inert. Two
   definitions of one surface decided by source order is how the next person
   reorders a file and moves the chrome. */
.topbar.has-ribbon { top: 24px; }
.topbar-title {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
  min-width: 0;
}
.topbar-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  position: relative;
  flex-shrink: 0;
}
.search-input {
  background: var(--surface);
  /* --border is decorative and measures 1.24:1 against the topbar ground.
     A control outline has to clear 3:1 — see .input, same pattern. */
  border: 1px solid var(--border-strong);
  padding: 6px 12px;
  border-radius: var(--radius-sm);
  width: 268px;   /* wide enough for its own placeholder — it was clipped mid-word at 200 */
  color: var(--text);
  font-size: var(--fs-sm);
  min-height: 34px;
  transition: border-color var(--dur-1) var(--ease-out), box-shadow var(--dur-1) var(--ease-out);
}
.search-input:hover { border-color: var(--muted-2); }
.search-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.user-chip {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  min-height: 34px;
  cursor: pointer;
  border: 1px solid transparent;
  transition: background-color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
}
.user-chip:hover { background: var(--surface-3); border-color: var(--border-strong); }
/* It opens the account menu, so it is a button and answers like one. */
.user-chip:active { transform: scale(0.97); transition-duration: 70ms; }
.user-avatar {
  width: 24px; height: 24px;
  border-radius: 50%;
  /* --primary is an ink role: in light it is #146B75, so near-black
     initials on it measured 3.10:1. --grad-brand is theme-stable and owns
     --on-accent. */
  background: var(--grad-brand);
  color: var(--on-accent);
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700;
  flex-shrink: 0;
}

/* Module toggle button — topbar, admin-only */
.topbar-modules-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  color: var(--text);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
  flex-shrink: 0;
}
.topbar-modules-btn:hover,
.topbar-modules-btn.active {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
}
.topbar-modules-btn:active { transform: scale(0.94); transition-duration: 70ms; }
/* A thumb is 44px. This used to live in components.css behind !important,
   under a comment saying nav.js wrote the size inline — nav.js has not done
   that for as long as git remembers. What the !important actually beat was
   the 34px rule eight lines above, in THIS file, which loads after
   components.css. Moving the rule here makes the cascade do it: same
   specificity, later in the same sheet. */
@media (pointer: coarse) {
  .topbar-modules-btn { width: 44px; height: 44px; }
}

/* ══ The shell's overlays — drawer, sheet, account menu ═══════════════════
   nav.js used to write every box below as an inline `style` attribute: 51 of
   them, plus fourteen `onmouseover="this.style.background=…"` pairs standing
   in for :hover. An inline style is the one declaration no stylesheet can
   answer, which is why two files had to reach for !important to get a 44px
   thumb target back, and why a hover state had to be written in JavaScript
   in a codebase that has a motion system with a rule about hover.

   These are the same values, in the cascade, on the scale. Three things
   changed on the way in and each is a rule this repo already has:
   · hover is gated behind (hover: hover) and (pointer: fine) — on touch a
     :hover latches after a tap, which is why it was JS before;
   · the module switch's knob moves on `transform`, not on `left`;
   · 18px and 10px became --sp-5 and --sp-3. The drawer head and its rows
     were 18px apart from each other by hand and now cannot drift.
   ════════════════════════════════════════════════════════════════════════ */

/* The sidebar's admin footer: how much of the product is switched on. */
.nav-modfoot {
  margin-top: auto;
  padding-top: var(--sp-3); padding-right: 2px; padding-left: 2px;
  padding-bottom: calc(var(--sp-3) + env(safe-area-inset-bottom, 0px));
}
.nav-modfoot-btn { width: 100%; background: var(--surface-2); border-color: var(--border); }
.nav-modfoot-n   { margin-left: auto; }

/* ── Module drawer ──────────────────────────────────────────────────────
   Opacity and transform only, and the open state is a class so the
   transition is interruptible — a second tap retargets instead of
   restarting. */
.mdrawer-veil {
  position: fixed; inset: 0; z-index: 900;
  background: var(--veil); opacity: 0;
  transition: opacity var(--dur-2) var(--ease-out);
}
.mdrawer-veil.is-in { opacity: 1; }
.mdrawer {
  position: fixed; top: 0; left: 0; bottom: 0; z-index: 901;
  width: 330px; max-width: 88vw;
  display: flex; flex-direction: column;
  background: var(--surface); border-right: 1px solid var(--border);
  box-shadow: var(--shadow-2);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  transform: translateX(-100%);
  transition: transform var(--dur-2) var(--ease-out);
}
.mdrawer.is-in { transform: translateX(0); }
/* LONGHANDS with --safe-top, always: the Android WebView silently drops a
   `padding:` shorthand whose first component is a max(), and the bar then
   grows to the right height with no padding at all. Already paid for once. */
.mdrawer-head {
  padding-top: calc(var(--sp-4) + var(--safe-top));
  padding-right: var(--sp-5); padding-bottom: var(--sp-3); padding-left: var(--sp-5);
  border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.mdrawer-body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; }
.mdrawer-titlerow { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); }
.mdrawer-titles { min-width: 0; }
.mdrawer-h {
  font: var(--fw-semi) var(--fs-subhead)/var(--lh-subhead) var(--font-display);
  letter-spacing: var(--tr-subhead); color: var(--text);
}
.mdrawer-sub { font-size: var(--fs-micro); line-height: var(--lh-micro); color: var(--muted); margin-top: var(--sp-1); }
.mdrawer-x {
  background: none; border: 0; color: var(--muted); cursor: pointer;
  padding: var(--sp-1); border-radius: var(--r-sm); flex-shrink: 0;
  transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
.mdrawer-x:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.mdrawer-group {
  padding: var(--sp-3) var(--sp-5) var(--sp-2);
  font: var(--fw-semi) var(--fs-micro)/var(--lh-micro) var(--font-body);
  letter-spacing: var(--tr-micro); text-transform: uppercase; color: var(--text-soft);
}
.mdrawer-note {
  padding: var(--sp-4) var(--sp-5) var(--sp-5);
  font-size: var(--fs-micro); line-height: var(--lh-micro); color: var(--text-soft);
}

/* A module's row in the drawer. */
.mrow {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-5);
  border-bottom: 1px solid var(--border);
  transition: background-color var(--dur-1) var(--ease-out);
}
.mrow-ico { width: 17px; height: 17px; flex-shrink: 0; margin-top: 1px; color: var(--muted-2); }
.mrow.is-on .mrow-ico { color: var(--accent); }
.mrow-main { flex: 1 1 0; min-width: 0; }
.mrow-name { display: block; color: var(--text); font-size: var(--fs-caption); font-weight: var(--fw-semi); }
.mrow-name.is-locked { color: var(--text-soft); }
.mrow-sub  { display: block; font-size: var(--fs-micro); line-height: var(--lh-micro); color: var(--muted); margin-top: 2px; }
.mrow-err  { display: none; font-size: var(--fs-micro); color: var(--danger-text); margin-top: var(--sp-1); }
.mrow-err.is-shown { display: block; }
.mrow-btn  { background: none; border: 0; padding: 0; cursor: pointer; display: flex; align-items: center; }
.mrow-btn:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: var(--r-sm); }
.mrow-state {
  font-size: var(--fs-micro); line-height: var(--lh-micro); color: var(--text-soft);
  letter-spacing: var(--tr-micro); text-transform: uppercase; flex-shrink: 0;
}

/* The on/off switch. The knob moves on transform: the old one animated
   `left`, which is a layout property and the one thing motion.css forbids. */
.mswitch {
  position: relative; width: 36px; height: 20px; flex-shrink: 0;
  border-radius: var(--r-pill);
  background: var(--surface-2); border: 1px solid var(--border-control);
  transition: background-color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
}
.mswitch::after {
  content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
  border-radius: 50%; background: var(--muted);
  transition: transform var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
}
.mswitch.is-on { background: var(--accent-fill); border-color: var(--accent); }
.mswitch.is-on::after { transform: translateX(16px); background: var(--on-accent); }
.mswitch.is-locked { opacity: .35; }

/* ── The More sheet (phone) ─────────────────────────────────────────────── */
.msheet-veil {
  position: fixed; inset: 0; z-index: 940;
  background: var(--veil); opacity: 0;
  transition: opacity var(--dur-2) var(--ease-out);
}
.msheet-veil.is-in { opacity: 1; }
.msheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 941;
  max-height: 82vh; display: flex; flex-direction: column;
  background: var(--surface); border-top: 1px solid var(--border);
  border-radius: var(--r-md) var(--r-md) 0 0;
  box-shadow: var(--shadow-2);
  transform: translateY(100%);
  transition: transform var(--dur-2) var(--ease-out);
}
.msheet.is-in { transform: translateY(0); }
.msheet .nav-filter  { padding: 0 var(--sp-3) var(--sp-2); }
.msheet .nav-empty   { padding: 0 var(--sp-3) var(--sp-3); }

/* ── The account menu ───────────────────────────────────────────────────
   Its box is .m-raised from ds.css — surface-2, shadow-2, no border in
   dark, because the shadow IS the edge of a thing that came from somewhere.
   What stays here is only where it hangs and how big it is. Its entrance
   lives in motion.css and its phone placement in the media query below;
   both of those had to fight an inline style until now, which is what the
   !important on the mobile block was for. */
.user-menu {
  position: absolute; top: 100%; right: 0; margin-top: var(--sp-2);
  padding: var(--sp-2); min-width: 190px; z-index: 200;
}
.user-menu-head {
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--border); margin-bottom: var(--sp-2);
}
.user-menu-name { font-size: var(--fs-micro); font-weight: var(--fw-semi); color: var(--text); }
.user-menu-role { font-size: var(--fs-micro); color: var(--text-soft); margin-top: 2px; }
.user-menu-item {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm);
  font-size: var(--fs-micro); color: var(--text); cursor: pointer; text-decoration: none;
  transition: background-color var(--dur-1) var(--ease-out);
}
.user-menu-item.is-danger { color: var(--danger-text); }
.user-menu-pick {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: 7px var(--sp-3);
}
.user-menu-pick > span:first-child { font-size: var(--fs-micro); color: var(--text-soft); }
.user-menu-rule { height: 1px; background: var(--border); margin: var(--sp-2) 10px; }
.user-menu-veil { position: fixed; inset: 0; z-index: 49; background: transparent; }
.user-chip-caret { width: 11px; height: 11px; opacity: .5; flex-shrink: 0; }
/* The language pair sits inside a row that is already spacing itself. */
.lang-switch { flex: none; }
.lang-switch .theme-opt { font-size: var(--fs-micro); min-width: 30px; }

/* ── Realtime dot ───────────────────────────────────────────────────────── */
#rtDot {
  width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; cursor: default;
  background: var(--warn);
  transition: background-color var(--dur-4) var(--ease-out);
}

/* ── The two sticky notices nav.js prepends to <body> ────────────────────
   --danger-soft is an ALPHA colour in dark (#F07C7C22, 13% opaque). As the
   sole background of a STICKY bar it would let the page scroll straight
   through the lockout notice, so the tint is layered over an opaque
   --surface. Longhands rather than the `background` shorthand: if a UA ever
   rejected the gradient the bar still keeps its opaque ground instead of
   losing it whole. */
.nv-lockbar {
  position: sticky; top: 0; z-index: 9999;
  background-color: var(--surface);
  background-image: linear-gradient(var(--danger-soft), var(--danger-soft));
  border-bottom: 2px solid var(--danger);
  color: var(--danger-text);
  padding: 10px var(--sp-4);
  font-size: var(--fs-micro); font-weight: var(--fw-semi); text-align: center;
}
.nv-modbar {
  position: sticky; top: 0; z-index: 9998;
  background: var(--surface-2); border-bottom: 1px solid var(--border);
  color: var(--text-soft);
  padding: 9px var(--sp-4);
  font-size: var(--fs-micro);
  display: flex; align-items: center; justify-content: center; gap: var(--sp-3);
  flex-wrap: wrap;
}
.nv-modbar-on {
  background: var(--accent-fill); border: 0; color: var(--on-accent);
  font-weight: var(--fw-semi); font-size: var(--fs-micro);
  padding: var(--sp-1) 10px; border-radius: var(--r-sm); cursor: pointer;
  transition: opacity var(--dur-1) var(--ease-out);
}

/* A choice in a list of choices reads left, not centred. */
.btn-left { justify-content: flex-start; text-align: left; }

/* Hover, once, in CSS. Every one of these was an onmouseover/onmouseout pair
   writing element.style — which latches on a touch device after a tap, and
   which is the reason this rule has to be gated. */
@media (hover: hover) and (pointer: fine) {
  .mrow:hover           { background: var(--surface-2); }
  .mdrawer-x:hover      { background: var(--surface-2); color: var(--text); }
  .user-menu-item:hover { background: var(--surface-2); }
  .user-menu-item.is-danger:hover { background: var(--danger-soft); }
  .nv-modbar-on:hover   { opacity: .85; }
}

/* ── Content area ────────────────────────────────────────────────────── */
.content {
  padding: var(--sp-5) var(--sp-6) var(--sp-8);
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  min-width: 0;
  overflow-x: hidden; /* fallback */
  overflow-x: clip;
}
.page-header {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  margin-bottom: var(--sp-5);
  gap: var(--sp-3);
  flex-wrap: wrap;
}
.page-header h1 { margin: 0; }
.page-header .sub { color: var(--text-soft); margin-top: 3px; font-size: var(--fs-sm); }

/* ── Grid ────────────────────────────────────────────────────────────── */
.grid   { display: grid; gap: var(--sp-4); }
/* Grid columns are minmax(0, 1fr), not 1fr: a 1fr column refuses to shrink
   below its content, so one button with long nowrap text (Settings, Vertical
   pack) pushed a whole column past a phone screen, where it was clipped. */
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid-aside { grid-template-columns: 1fr 340px; }
.grid-12 { display: grid; grid-template-columns: repeat(12, 1fr); gap: var(--sp-4); }

.span-3  { grid-column: span 3; }
.span-4  { grid-column: span 4; }
.span-6  { grid-column: span 6; }
.span-8  { grid-column: span 8; }
.span-9  { grid-column: span 9; }
.span-12 { grid-column: span 12; }

/* ── Bottom nav — hidden on desktop ─────────────────────────────────── */
.bottom-nav {
  display: none;
  position: fixed;
  bottom: 0; left: 0; right: 0;
  background: var(--surface);
  border-top: 1px solid var(--border);
  /* Safe area: home indicator on modern iOS/Android */
  padding: 4px 0 max(6px, env(safe-area-inset-bottom));
  z-index: 60;
  justify-content: space-around;
  align-items: stretch;
}
.bottom-nav .nav-link {
  flex-direction: column;
  gap: 3px;
  padding: 6px 4px;
  font-size: 11px;
  border-radius: 8px;
  border: none;
  flex: 1;
  min-height: 50px;
  justify-content: center;
  align-items: center;
  color: var(--text);
}
.bottom-nav .nav-link.active  { background: var(--primary-soft); border: none; color: var(--text); font-weight: 600; }
.bottom-nav .nav-link .icon   { width: 20px; height: 20px; }
/* Belt to the label's braces: even if a label ever gets long again, it may not
   wrap the bar taller. A two-line cell stretched the whole nav over the page. */
.bottom-nav .nav-link > span:not(.icon) {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
/* 0.96 and no local duration: this selector was declared TWICE, here at 80ms
   and again in motion.css at 70ms. layout.css loads last, so the motion.css
   copy was dead code that read as a landed fix. 80ms is under the 100-160ms
   press band and 0.94 under the 0.95-0.98 scale band, on the most-tapped
   surface in a phone-first app. It inherits --dur-1 from the base rule now. */
.bottom-nav .nav-link:active  { transform: scale(0.96); }

/* ── Auth pages ──────────────────────────────────────────────────────── */
.auth-shell {
  min-height: 100dvh;
  min-height: 100vh;
  display: grid;
  grid-template-columns: 1fr 1fr;
}
.auth-side {
  background: var(--panel-accent);
  padding: var(--sp-10) 48px;
  padding-top: calc(var(--sp-10) + var(--safe-top));
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  border-right: 1px solid var(--border);
}
.auth-form-side {
  /* Same edge-to-edge story as .topbar: inside the app this page runs under
     the status bar so its own --bg paints it, and this keeps the heading out
     from behind the clock. 0 in every browser. */
  padding: var(--sp-10) 48px;
  padding-top: calc(var(--sp-10) + var(--safe-top));
  display: flex;
  flex-direction: column;
  justify-content: center;
  max-width: 480px;
  margin: 0 auto;
  width: 100%;
}
.auth-form-side h1 { margin-bottom: var(--sp-2); }
.auth-form-side .sub { color: var(--text-soft); margin-bottom: 28px; }

/* ── Landing / public ────────────────────────────────────────────────── */
.landing { max-width: 1200px; margin: 0 auto; padding: 80px var(--sp-8); }
.hero { text-align: center; margin-bottom: 80px; }
.hero h1 { font-size: clamp(32px, 5vw, 64px); line-height: 1.05; margin-bottom: var(--sp-4); }
.hero .lede { font-size: 18px; color: var(--text-soft); max-width: 640px; margin: 0 auto var(--sp-8); }
.hero .cta-row { display: flex; gap: var(--sp-3); justify-content: center; flex-wrap: wrap; }
.feature-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-4); margin-top: 48px; }
.feature-card { padding: var(--sp-6); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }

/* ══════════════════════════════════════════════════════════════════════
   RESPONSIVE — 900px: sidebar collapses
   ══════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .app {
    grid-template-columns: 1fr;
    padding-bottom: calc(58px + max(6px, env(safe-area-inset-bottom)));
  }
  .sidebar { display: none; }
  .bottom-nav { display: flex; }

  .grid-4    { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-aside { grid-template-columns: 1fr; }
  .span-3, .span-4 { grid-column: span 6; }
  .span-8, .span-9 { grid-column: span 12; }
  .search-input { display: none; }

  /* Show module toggle in topbar — was sidebar-only */
  .topbar-modules-btn { display: flex; }
}

/* ══════════════════════════════════════════════════════════════════════
   RESPONSIVE — 600px: full mobile
   ══════════════════════════════════════════════════════════════════════ */
@media (max-width: 600px) {
  /* The topbar keeps its full 52px on phones (it used to shrink to 48):
     the account chip and the module button live in it and both have to
     clear 44px for a thumb. */

  .content { padding: var(--sp-3) var(--sp-3) var(--sp-5); }
  .page-header { margin-bottom: var(--sp-4); }
  .page-header { flex-direction: column; align-items: flex-start; gap: 10px; }

  /* Metadata is 12px on a 27" monitor and 13px in a dim back office */
  .tiny { font-size: 13px; }
  .label { font-size: var(--fs-tiny); }

  /* Filter chips scroll sideways instead of being sliced by the card edge —
     the pattern every shopping app uses. Scoped with :has() so only rows of
     buttons become scrollers; everything else keeps wrapping. */
  .card .row > .row:has(> .btn) {
    max-width: 100%;
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .card .row > .row:has(> .btn)::-webkit-scrollbar { display: none; }

  /* All grids → single column */
  .grid-2, .grid-3, .grid-4 { grid-template-columns: minmax(0, 1fr); }
  .span-3, .span-4, .span-6, .span-8, .span-9 { grid-column: span 12; }

  /* Auth */
  .auth-shell { grid-template-columns: 1fr; }
  .auth-side  { display: none; }
  .auth-form-side { padding: var(--sp-8) var(--sp-5); max-width: 100%; }

  /* Landing */
  .landing { padding: var(--sp-10) var(--sp-4); }
  .feature-grid { grid-template-columns: 1fr; }

  /* KPI: 2-up even on small phones */
  .grid.grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .kpi .value  { font-size: 22px; }
  .kpi { padding: var(--sp-3); min-height: 86px; }

  /* Card */
  .card { padding: var(--sp-3); }

  /* Topbar */
  /* Narrower side padding on a phone — but NOT a reset of the top inset.
     This rule was `padding: 0 var(--sp-3)`, which quietly undid
     .topbar's padding-top and put the theme toggle and the avatar back
     underneath the clock on every Android phone. Side longhands only. */
  .topbar { padding-left: var(--sp-3); padding-right: var(--sp-3); gap: 6px; }
  .topbar-title { font-size: var(--fs-body); }
  /* Under ~480px the title loses its fight with the theme switch, the module
     button and the account chip, and ellipsises mid-word — "Command C…",
     which reads as a bug rather than as a heading. The page's own <h1> and the
     bottom nav both name the page, so the third copy is the one to drop. */
  @media (max-width: 480px) {
    /* visibility, not display: .topbar-title is the flex:1 spacer that holds
       the controls against the right edge, and removing it from layout let
       them collapse into a left-hand huddle with dead space beside them. */
    .topbar-title { visibility: hidden; }
  }
  /* Account chip and module button are the only two controls up here —
     both have to be thumb-sized, not 34px. */
  .user-chip { min-height: 44px; padding: 4px 12px 4px 6px; }
  .user-avatar { width: 30px; height: 30px; font-size: 12px; }
  .topbar-modules-btn { width: 44px; height: 44px; }
  .user-chip .tiny { display: none; } /* hide name, show avatar only */
  /* Hide the tier badge in the topbar only. This used to be an unscoped
     rule, so it also blanked the category and "normal" priority badges in
     every ticket row on a phone — the two columns staff scan first. */
  .topbar .badge:not(.badge-dot) { display: none; }

  /* Tables: one line per row, swipe sideways for the rest of the columns.
     At 460px the cells wrapped to three lines each and a ticket list stopped
     being scannable; nowrap + ellipsis keeps rows countable at a glance. */
  .card table, .table-wrap, .card > table.table, .card > div > table.table {
    display: block;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }
  /* The rule above makes the TABLE ELEMENT itself the scroll container, so a
     min-width here widens the SCROLLPORT, not the content. Measured at 375px:
     port 560px inside a 351px card whose inline style is `overflow:hidden`
     (tickets.html), so the card clipped the right 209px and Status + SLA were
     unreachable at every scroll position — on tickets, staff and sla alike.
     The port has to stay at the card's width; the rows are what scroll.
     A min-width is still correct when a WRAPPER is the scroller. */
  table.table { min-width: 0; }
  .table-wrap > table.table { min-width: 560px; }
  .table th, .table td { white-space: nowrap; }
  .table td { max-width: 210px; overflow: hidden; text-overflow: ellipsis; }

  /* Modals: full-width slide from bottom */
  .modal-backdrop { align-items: flex-end; padding: 0; }
  .modal {
    width: 100vw !important;
    max-width: 100vw !important;
    padding: var(--sp-5) var(--sp-4) max(20px, calc(16px + env(safe-area-inset-bottom)));
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    margin: 0;
    max-height: 92dvh;
    max-height: 92vh;
    animation: nvSheetIn var(--dur-4) var(--ease-out);
  }
  /* A full translateY(100%), matching nvSheetOut. The comment above says
     "full-width slide from bottom", but this rose 14px and faded while the
     EXIT slid the element's whole height — two different physics for one
     object. A percentage translate moves by the sheet's own height whatever
     its size. Opacity is dropped deliberately: it starts entirely below the
     viewport, so fading it is wasted compositing. The backdrop still fades. */
  @keyframes nvSheetIn {
    from { transform: translateY(100%); }
    to   { transform: none; }
  }
  .modal-actions { flex-wrap: wrap; gap: var(--sp-2); }
  .modal-actions .btn { flex: 1; min-width: 100px; justify-content: center; }
  .modal .row.gap-12 { flex-direction: column; }
  .modal .row.gap-12 .grow { width: 100%; }

  /* Inputs: 16px prevents iOS auto-zoom */
  .input, .select, .textarea, input, select, textarea { font-size: 16px !important; }

  /* Touch targets */
  .btn { min-height: 44px; }
  .btn-sm { min-height: 44px; font-size: var(--fs-tiny); padding: 10px 14px; }
  .nav-link { min-height: 44px; }
  .tab { min-height: 44px; display: flex; align-items: center; }

  /* Page-header buttons: full width */
  .page-header > .row { width: 100%; flex-wrap: wrap; }
  .page-header > .row > .btn,
  .page-header > button.btn { flex: 1; justify-content: center; min-width: 120px; }

  /* Bottom nav */
  .bottom-nav .nav-link { min-height: 52px; }

  /* User menu: under the chip that opens it.
     ────────────────────────────────────────────────────────────────────────
     This was pinned to the BOTTOM of the screen, roughly 600pt from the avatar
     tapped to open it. Two independent QA passes, one on Android and one on
     iOS, both concluded the control was dead — "tapping the avatar never opens
     the menu", "Sign out is unreachable" — because nothing appeared anywhere
     they were looking. A menu that opens at the opposite end of the screen
     from its trigger is, in practice, a menu that did not open. */
  /* No !important any more: the base rule is in this same file, above, and
     this one is later. It used to be beating an inline style attribute. */
  .user-menu {
    position: fixed;
    top: calc(var(--topbar-h, 56px) + var(--safe-top, env(safe-area-inset-top, 0px)) + 4px);
    bottom: auto;
    right: 10px;
    left: auto;
    min-width: 220px;
    max-height: calc(100vh - var(--topbar-h, 56px) - 120px);
    overflow-y: auto;
  }

  /* Toast: above bottom nav, edge-to-edge */
  .toast-stack { bottom: calc(66px + env(safe-area-inset-bottom)); right: 10px; left: 10px; align-items: stretch; }
  .toast { min-width: unset; max-width: unset; width: 100%; }

  /* SVG/charts: fill width */
  svg { max-width: 100%; height: auto; }

  /* Tabs: scrollable row */
  .tabs { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .tab  { flex-shrink: 0; }

  /* Prevent overflow in main */
  .main, .content { max-width: 100vw; }
}
@media (max-width: 359px) {
  .bottom-nav .nav-link span:not(.icon) { display: none; }
  .bottom-nav .nav-link { min-height: 44px; }
}

/* ══════════════════════════════════════════════════════════════════════
   "More" sheet — the phone's route to every module
   There is no sidebar under 900px, and the bottom bar holds four links, so
   without this sheet the other thirty-odd modules have no route on a phone.
   Tokens only, and it inherits the app's light/dark like any other surface.
   ══════════════════════════════════════════════════════════════════════ */
#moreSheet .ms-head {
  flex-shrink: 0;
  padding: 8px 16px 10px;
  border-bottom: 1px solid var(--border);
}
#moreSheet .ms-grip {
  display: block; width: 34px; height: 4px; margin: 0 auto 10px;
  border-radius: 2px; background: var(--border-strong);
}
#moreSheet .ms-head-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
#moreSheet .ms-title {
  font-family: var(--font-display); font-size: var(--fs-subhead);
  font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 8px;
}
#moreSheet .ms-count {
  font-family: var(--font-body); font-size: var(--fs-micro); font-weight: 600;
  color: var(--text-soft); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px;
}
#moreSheet .ms-close {
  background: none; border: none; color: var(--muted); cursor: pointer;
  padding: 8px; margin: -8px; border-radius: var(--radius-sm);
  display: flex; align-items: center;
}
#moreSheet .ms-close:hover { background: var(--surface-2); color: var(--text); }
#moreSheet .ms-body {
  flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch;
  /* The sheet sits at bottom:0 and covers the bar it was opened from, so only
     the home indicator needs clearing — leaving room for the bar as well put
     76px of dead space under the last module. */
  padding-bottom: calc(12px + max(6px, env(safe-area-inset-bottom)));
  overscroll-behavior: contain;
}
#moreSheet .ms-group {
  padding: 14px 16px 6px; font-size: 10px; font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--tr-micro); color: var(--text-soft);
}
#moreSheet .ms-row {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 16px; min-height: 46px;      /* thumb-sized, per the bottom bar */
  color: var(--text); text-decoration: none; font-size: var(--fs-caption);
}
#moreSheet .ms-row:active { background: var(--surface-2); }
#moreSheet .ms-row.active { background: var(--primary-soft); font-weight: 600; }
#moreSheet .ms-ico { width: 18px; height: 18px; flex-shrink: 0; color: var(--muted); display: flex; }
#moreSheet .ms-row.active .ms-ico { color: var(--text); }
#moreSheet .ms-ico svg { width: 100%; height: 100%; }
#moreSheet .ms-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (prefers-reduced-motion: reduce) {
  /* The sheet's translateY(100%) is travel and goes. The veil's opacity is not:
     --veil is a heavy scrim, and a full-viewport dim arriving in one frame is a
     luminance jolt — the thing reduced motion is most often turned on to avoid.
     200ms because that is what closeMoreSheet waits before removing it. */
  #moreSheet     { transition: none !important; }
  #moreSheetVeil { transition: opacity var(--dur-2) var(--ease-out) !important; }
}

/* ══════════════════════════════════════════════════════════════════════
   Module drawer overlay (shared by sidebar button + topbar toggle)
   ══════════════════════════════════════════════════════════════════════ */
#moduleDrawer {
  /* Ensure it covers the full viewport height including safe areas */
  padding-top: max(0px, env(safe-area-inset-top));
  padding-bottom: max(0px, env(safe-area-inset-bottom));
}

/* ══════════════════════════════════════════════════════════════════════
   Reduced motion — one switch for the whole product.
   Loaded last on every page, so it also neutralises inline transitions
   set from JS. Spinners keep turning (a frozen spinner reads as a hang);
   shimmer stops, because a skeleton already reads as "not here yet".

   GENTLER, NOT ZERO. This block used to collapse transitions to 0.01ms as
   well, which left a reduced-motion user with a product where nothing eased
   at all: hover colours snapped, a tab's underline teleported, a row's
   background flicked. Colour and opacity carry no vestibular risk and they
   are what makes a state change legible, so they keep a --dur-1 transition.
   Movement is the part that has to go, and it goes by transform simply not
   being in the transition-property list below.

   Animations stay at 0.01ms and that is deliberate, not sloppy: utils.js
   waits on `animationend` to remove a toast and to dismiss a modal, and at
   that duration the event still fires, so nothing is stranded. Nothing in
   this repo waits on `transitionend` — checked — so softening the
   transition half strands no handler.
   ══════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
  *, *::before, *::after {
    transition-property: opacity, color, background-color, border-color, fill, stroke, box-shadow !important;
    transition-duration: var(--dur-1) !important;
  }
  .btn.is-loading::after,
  .btn[aria-busy="true"]::after {
    animation-duration: 900ms !important;
    animation-iteration-count: infinite !important;
  }
  .skeleton::after { animation: none !important; }
  /* Opacity is not movement. A class selector outranks `*`, so these win the
     !important contest on specificity and keep their crossfade while every
     transform-based exit above still collapses. nvFade and nvFadeOut are
     opacity-only, so nothing travels. */
  .modal-backdrop, .toast { transition-property: opacity !important;
                            transition-duration: var(--dur-2) !important; }
  .btn:active, .nav-link:active, .bottom-nav .nav-link:active,
  .topbar-modules-btn:active, .theme-switch button:active,
  .user-chip:active { transform: none !important; }
}

/* ── Topbar search ──────────────────────────────────────────────────────────
   The input existed and was wired to NOTHING — it had a placeholder, a focus
   ring and no handler, which is worse than no search at all: it invites the
   one thing it cannot do. The panel is positioned relative to a wrapper rather
   than the topbar, because .topbar is position:sticky with a z-index and
   therefore its own stacking context (see the account-menu note below). */
.topsearch { position: relative; display: flex; }
.topsearch-panel {
  position: absolute; top: calc(100% + 6px); right: 0; left: 0;
  min-width: 320px; max-height: min(60vh, 460px); overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-2);
  padding: 6px; z-index: 80;
}
.topsearch-group {
  font-size: var(--fs-micro); letter-spacing: var(--tr-micro); text-transform: uppercase;
  color: var(--muted); font-weight: 600; padding: 8px 10px 4px;
}
.topsearch-item {
  display: flex; align-items: baseline; gap: 8px;
  padding: 7px 10px; border-radius: 7px; cursor: pointer;
  font-size: 13px; color: var(--text); text-decoration: none;
}
.topsearch-item small { color: var(--text-soft); font-size: var(--fs-micro); }
.topsearch-item[aria-selected="true"] { background: var(--surface-2); }
@media (hover: hover) and (pointer: fine) {
  .topsearch-item:hover { background: var(--surface-2); }
}
.topsearch-empty { padding: 12px 10px; font-size: var(--fs-micro); color: var(--text-soft); }

/* ── Grid and flex children may shrink ───────────────────────────────────
   A grid or flex item's default `min-width: auto` means it refuses to go
   narrower than its own content — so a wide child pushes its parent past
   the viewport even when the column rule says one column. `.grid-aside`
   already collapses to `1fr` on mobile and `assets.html` still rendered its
   card 745px wide inside a 390px window, because the card was never allowed
   to be smaller than what was in it.
   `.gx-split` had worked this out and wrote `minmax(0, …)`; this applies the
   same permission everywhere rather than one page at a time. ──────────── */
.grid > *, .grid-2 > *, .grid-3 > *, .grid-4 > *, .grid-aside > *, .grid-12 > * { min-width: 0; }
.card { min-width: 0; }

/* A JSON blob in a mono span has no break opportunity, so it sets its own
   width and takes the card with it. `anywhere` breaks only when there is no
   other option, so a room number or a rupee figure is untouched. */
.mono { overflow-wrap: anywhere; }

/* ── Chrome is a material — .m-glass in ds.css, by its tokens ────────────
   The sidebar and topbar are translucent over the ground and the content
   scrolls under them; a hard opaque wall was the single biggest "template"
   tell on every dashboard. The blur is --blur-glass rather than a repeated
   `saturate(160%) blur(14px)`, so this surface and the one ds.css declares
   cannot drift apart.

   A backdrop-filter ancestor becomes the containing block for its
   `position: fixed` descendants, and the account menu is fixed on a phone
   and lives inside .topbar. An earlier version of this file guarded against
   that with a min-width: 901px query, which a later rule then beat at every
   width. MEASURED at 390px before removing the dead guard: the topbar's own
   top is 0 in the product, so `top: calc(var(--topbar-h) + var(--safe-top)
   + 4px)` resolves to the same 56px either way. It differs only under the
   demo ribbon, which pushes the topbar down — and there the menu hangs 12px
   under the bar instead of crossing it, which is the better of the two. */
.sidebar, .topbar {
  background: var(--chrome-glass);
  -webkit-backdrop-filter: var(--blur-glass, saturate(160%) blur(14px));
  backdrop-filter: var(--blur-glass, saturate(160%) blur(14px));
}
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .sidebar, .topbar { background: var(--surface-chrome); -webkit-backdrop-filter: none; backdrop-filter: none; }
}

/* ══════════════════════════════════════════════════════════════════════
   Hubs — seven places in the sidebar, a tab strip on the page, a + in the
   topbar. docs/design/DIRECTION.md, "Information architecture".

   The registry is still 43 entries; a hub is how they are SHOWN. The active
   hub is open with its modules indented under it; every other hub is one
   row with a count of what is on. Tokens only. Movement is transform and
   opacity; hover is gated; the press answers in 70ms.
   ══════════════════════════════════════════════════════════════════════ */
.nav-hub { margin-bottom: 2px; }
.nav-hub-row { display: flex; align-items: center; gap: 2px; }
.nav-hub-row .nav-hub-link { flex: 1; min-width: 0; }
.nav-hub-link > span:nth-child(2) {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The hub this page belongs to: weight and the primary hue on the glyph, no
   fill — the fill and the rail belong to the module row underneath it. */
.nav-hub.is-current .nav-hub-link { font-weight: var(--fw-medium); }
.nav-hub.is-current .nav-hub-link .icon { opacity: 1; color: var(--primary); }
.nav-hub-count {
  margin-left: auto; flex: none;
  min-width: 18px; padding: 0 5px; line-height: 17px; text-align: center;
  font-size: 11px; font-variant-numeric: tabular-nums;
  color: var(--muted); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: var(--r-pill);
}
.nav-hub.is-open .nav-hub-count { display: none; }

/* The disclosure. Opens the list in place; the row's link is what navigates. */
.nav-hub-toggle {
  flex: none; width: 28px; height: 28px; margin-right: 2px;
  display: flex; align-items: center; justify-content: center;
  background: none; border: 0; border-radius: var(--r-sm);
  color: var(--muted); cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
}
.nav-hub-toggle svg { width: 14px; height: 14px; transition: transform var(--dur-2) var(--ease-out); }
.nav-hub.is-open .nav-hub-toggle svg { transform: rotate(90deg); }
@media (hover: hover) and (pointer: fine) {
  .nav-hub-toggle:hover { background: var(--surface-2); color: var(--text); }
}
.nav-hub-toggle:active { transform: scale(0.94); transition-duration: 70ms; }
.nav-hub-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

.nav-hub-list { display: none; padding: 2px 0 4px; }
.nav-hub.is-open .nav-hub-list,
#navList.is-filtering .nav-hub-list { display: block; }
/* While the Jump box has a query every matching hub is shown open, and the
   disclosure and the count have nothing to say. */
#navList.is-filtering .nav-hub-toggle,
#navList.is-filtering .nav-hub-count { display: none; }

/* A module row under its hub: indented to the hub label's text edge
   (10px pad + 16px glyph + 9px gap), one step quieter, the fill and rail on
   the active one exactly as before. */
.nav-sub {
  padding: 7px 10px 7px 35px; min-height: 36px;
  font-size: var(--fs-caption); color: var(--text-soft);
}
.nav-sub:hover, .nav-sub.active { color: var(--text); }
.sidebar .nav-sub.active::before { height: 16px; }
.nav-actions .nav-sub { padding-left: 10px; }
/* Rows rise in only when a PERSON opens a hub. A page load holds still.
   The index is :nth-child, not a custom property. This rule used to read
   `calc(var(--nv-i, 0) * 20ms)`, and --nv-i is written by nvMotion.enter()
   in utils.js onto the children of #root — never onto a nav link. Nothing
   in the sidebar has ever carried it, so every row resolved to 0ms and the
   hub arrived in one block: a stagger that cost a declaration and did
   nothing. The longest hub is Stays at nine modules, so nine steps covers
   the list and a tenth row simply arrives with the ninth. */
.nav-hub.is-revealing .nav-hub-list .nav-link {
  animation: nvRise var(--dur-2) var(--ease-out) both;
  animation-delay: 160ms;
}
.nav-hub.is-revealing .nav-hub-list .nav-link:nth-child(1) { animation-delay:   0ms; }
.nav-hub.is-revealing .nav-hub-list .nav-link:nth-child(2) { animation-delay:  20ms; }
.nav-hub.is-revealing .nav-hub-list .nav-link:nth-child(3) { animation-delay:  40ms; }
.nav-hub.is-revealing .nav-hub-list .nav-link:nth-child(4) { animation-delay:  60ms; }
.nav-hub.is-revealing .nav-hub-list .nav-link:nth-child(5) { animation-delay:  80ms; }
.nav-hub.is-revealing .nav-hub-list .nav-link:nth-child(6) { animation-delay: 100ms; }
.nav-hub.is-revealing .nav-hub-list .nav-link:nth-child(7) { animation-delay: 120ms; }
.nav-hub.is-revealing .nav-hub-list .nav-link:nth-child(8) { animation-delay: 140ms; }

/* ── Hub tab strip — under the topbar, above the page's own header ────────
   The same box as .content so the tabs sit on the page's left edge. It is its
   own horizontal scroller: on a phone the pill slides inside the strip and the
   page never scrolls sideways. Real links styled as the segmented control. */
.hub-strip {
  position: relative;
  display: flex; align-items: center; gap: var(--sp-3);
  width: 100%; max-width: var(--maxw); margin: 0 auto;
  box-sizing: border-box;
  padding: var(--sp-3) var(--sp-6) 0;
  overflow-x: auto; overflow-y: hidden;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  flex-shrink: 0;
}
.hub-strip::-webkit-scrollbar { display: none; }
.hub-strip-name {
  display: inline-flex; align-items: center; gap: 7px; flex: none;
  font-size: var(--fs-caption); font-weight: var(--fw-medium);
  color: var(--text-soft); white-space: nowrap;
}
.hub-strip-name .icon { width: 15px; height: 15px; display: flex; color: var(--primary); }
.hub-strip-name .icon svg { width: 100%; height: 100%; }
.hub-seg { flex: none; }
.hub-tab {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 13px; border-radius: var(--r-pill);
  font-size: var(--fs-caption); font-weight: var(--fw-medium); line-height: 1.2;
  color: var(--muted); text-decoration: none; white-space: nowrap;
  transition: color var(--dur-1) var(--ease-out),
              background-color var(--dur-1) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .hub-tab:hover { color: var(--text); text-decoration: none; }
}
.hub-tab[aria-current="page"] {
  background: var(--surface); color: var(--text);
  box-shadow: var(--shadow-1); font-weight: var(--fw-semi);
}
.hub-tab:active { transform: scale(0.97); transition-duration: 70ms; }
.hub-tab:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.hub-tab .tier-lock {
  font-size: 10px; line-height: 1.4; color: var(--text-soft);
  background: var(--surface-2); padding: 0 6px;
  border-radius: var(--r-pill); border: 1px solid var(--border);
}
@media (max-width: 600px) {
  .hub-strip { padding: var(--sp-2) var(--sp-3) 0; gap: var(--sp-2); }
  .hub-strip-name { display: none; }
  .hub-tab { min-height: 36px; }
}

/* ── The + : quick actions on every dashboard ─────────────────────────────
   Tinted, not filled: --accent is ink-safe in both themes, and a filled
   green in the chrome would compete with the page's own primary button. */
.qa { position: relative; display: flex; flex-shrink: 0; }
.topbar-plus {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: var(--r-sm);
  background: var(--accent-soft); border: 1px solid var(--accent);
  color: var(--accent); cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
}
.topbar-plus svg { width: 16px; height: 16px; }
@media (hover: hover) and (pointer: fine) {
  .topbar-plus:hover { background: var(--accent-fill); color: var(--on-accent); }
}
.topbar-plus[aria-expanded="true"] { background: var(--accent-fill); color: var(--on-accent); }
.topbar-plus:active { transform: scale(0.94); transition-duration: 70ms; }
.topbar-plus:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* The menu springs from the button: raised surface, --shadow-2, the
   translucent chrome material where the browser can blur. Anchored top-right
   so the scale reads as growing out of the +. */
.qa-menu {
  position: absolute; top: calc(100% + 6px); right: 0;
  min-width: 240px; padding: 6px; z-index: 80;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r-md); box-shadow: var(--shadow-2);
  transform-origin: top right;
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .qa-menu { background: var(--chrome-glass);
             -webkit-backdrop-filter: saturate(160%) blur(14px); backdrop-filter: saturate(160%) blur(14px); }
}
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .qa-menu { background: var(--surface-2); -webkit-backdrop-filter: none; backdrop-filter: none; }
}
.qa-menu[hidden] { display: none; }
.qa-menu.is-open { animation: nvMenuIn var(--dur-3) var(--ease-out) both; }
@keyframes nvMenuIn { from { opacity: 0; transform: translateY(-6px) scale(0.96); } }
.qa-item {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 10px; border-radius: var(--r-sm);
  color: var(--text); font-size: var(--fs-caption); font-weight: var(--fw-medium);
  text-decoration: none; cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-out), transform var(--dur-1) var(--ease-out);
}
.qa-item .icon { width: 16px; height: 16px; display: flex; flex: none; color: var(--accent); }
.qa-item .icon svg { width: 100%; height: 100%; }
.qa-item .qa-label { flex: 1; min-width: 0; }
.qa-item small { color: var(--muted); font-size: var(--fs-micro); font-weight: var(--fw-regular); white-space: nowrap; }
@media (hover: hover) and (pointer: fine) {
  .qa-item:hover { background: var(--surface-3); text-decoration: none; }
}
.qa-item:focus-visible { outline: none; background: var(--surface-3); }
.qa-item:active { transform: scale(0.98); transition-duration: 70ms; }
@media (max-width: 900px) {
  /* Fixed under the bar that opened it, like the account menu — the topbar
     below 900px has no backdrop-filter, so fixed still means the viewport. */
  .qa-menu {
    position: fixed;
    top: calc(var(--topbar-h, 52px) + var(--safe-top, env(safe-area-inset-top, 0px)) + 4px);
    right: 10px; left: auto; min-width: 250px;
  }
}
@media (max-width: 600px) {
  .topbar-plus { width: 44px; height: 44px; }
}

@media (prefers-reduced-motion: reduce) {
  .nav-hub-toggle:active, .hub-tab:active, .topbar-plus:active, .qa-item:active { transform: none !important; }
  .nav-hub-toggle svg { transition: none !important; }
  /* The rotation is state, not motion — it still has to land. */
  .nav-hub.is-open .nav-hub-toggle svg { transform: rotate(90deg); }
}
