/* ============================================================
   NetVault shared theme - SOURCE OF RECORD
   Owner: NVT-NXS-01 (THEME-1). LEO and Fax REQUEST changes; do not edit a copy.
   Published: nexus-shared-assets.git

   RECONCILED 2026-08-05 across all three copies.
     normalised md5 fcc03c5c66457f255ce40c9687ce772d  23764 B  266 braces
     The whole divergence was ONE rule: .pager-label, present only on NVT-FAX01
     and used live three times there. It was ADDED here, not removed there.
     There was NO drift inside shared rules - that finding was an artefact of
     subtracting the wrong rule, and is withdrawn.
     .cover-wrap is shared by all three copies and never was the difference.

   ⚠️ Had this been reconciled by DELETION - which the block count and the
      first localisation both pointed at - three labels would have rendered
      unstyled while the count reported convergence.

   ADDED 2026-08-06: .pill.info (requested by LEO-01 for the scheduled/pending
   state). Their proposed light ink was adopted unchanged; the DARK override was
   added here — see the note at the rule. NVT-FAX01 and LEO must pull this.

   ADDED 2026-08-06: .rail-links — the foot-of-rail container for outbound
   product links (Nexus -> LEO / Fax), sibling of .nav and .rail-user. It is
   STANDALONE, so bare use is correct. Available to LEO and Fax for their
   Return-to-Nexus item, which sits in the same position.

   40 of the 137 classes here are CONTEXT-ONLY - styled only inside a parent or
   beside a co-class. Using one bare renders unstyled with NO console error.
   .ok / .bad / .warn / .active / .info look general and are not. Full list:
   theme-context-classes.json in the same repo. The dangerous case is a class
   already working correctly twenty lines away - the reader's evidence is real
   but does not bear on the question.
   .pager-label is STANDALONE, so bare use of it is correct.
   ============================================================ */
/* ============================================================
   NetVault shared design system — theme.css
   COPIED VERBATIM from the Fax2Email portal (/opt/faxportal/theme.css
   on NVT-FAX01) on 2026-08-01. Keep the two in sync: if you change a
   token or component here, mirror it there, and vice versa. Do not
   fork the design system per-app.
   ------------------------------------------------------------
   Originally: NetVault Fax2Email Portal — theme.css
   Design system derived from NetVault Brand Guide v1.0
   Vault Charcoal #1D252D · Sky Blue #009CDE · Kanit + Public Sans
   Single source of truth for colour + type across the SPA.
   ============================================================ */

/* ---- self-hosted fonts (TTF; swap to WOFF2 later if desired) ---- */
@font-face{font-family:'Kanit'; font-style:normal; font-weight:500; font-display:swap;
  src:url('/assets/fonts/Kanit-Medium.ttf') format('truetype')}
@font-face{font-family:'Kanit'; font-style:normal; font-weight:600; font-display:swap;
  src:url('/assets/fonts/Kanit-SemiBold.ttf') format('truetype')}
@font-face{font-family:'Kanit'; font-style:normal; font-weight:700; font-display:swap;
  src:url('/assets/fonts/Kanit-Bold.ttf') format('truetype')}
@font-face{font-family:'Public Sans'; font-style:normal; font-weight:400; font-display:swap;
  src:url('/assets/fonts/PublicSans-Regular.ttf') format('truetype')}
@font-face{font-family:'Public Sans'; font-style:normal; font-weight:500; font-display:swap;
  src:url('/assets/fonts/PublicSans-Medium.ttf') format('truetype')}
@font-face{font-family:'Public Sans'; font-style:normal; font-weight:600; font-display:swap;
  src:url('/assets/fonts/PublicSans-SemiBold.ttf') format('truetype')}
@font-face{font-family:'Public Sans'; font-style:normal; font-weight:700; font-display:swap;
  src:url('/assets/fonts/PublicSans-Bold.ttf') format('truetype')}
@font-face{font-family:'Public Sans'; font-style:italic; font-weight:400; font-display:swap;
  src:url('/assets/fonts/PublicSans-Italic.ttf') format('truetype')}

/* ---- LIGHT (default) ---- */
:root{
  --nv-blue:#009CDE; --nv-blue-600:#0089c4; --nv-blue-700:#0076ab; --nv-blue-tint:#e7f5fc;
  --charcoal:#1D252D;
  --canvas:#eef3f6; --paper:#ffffff; --input-bg:#ffffff; --input-alt:#fafcfd;
  --line:#dce4e9; --line-strong:#c6d1d8;
  --ink:#1D252D; --muted:#5e6e79; --muted-2:#8a97a0;
  --ok:#1E9E6A; --ok-tint:#DDF3EA; --warn:#E0A100; --warn-tint:#FBF0D2; --bad:#D0342C; --bad-tint:#FBE3E1;
  --rail-top:#28323b; --rail-bot:#1D252D; --topbar-bg:rgba(255,255,255,.85);
  --shadow:0 1px 2px rgba(29,37,45,.06), 0 8px 26px -12px rgba(29,37,45,.22);
  --shadow-lg:0 1px 2px rgba(29,37,45,.06), 0 24px 60px -20px rgba(29,37,45,.35);
  --r:6px; --r-sm:4px; --rail-w:248px;
  --font-display:'Kanit',system-ui,-apple-system,sans-serif;
  --font-body:'Public Sans',system-ui,-apple-system,sans-serif;
}
/* ---- DARK ---- */
[data-theme="dark"]{
  --nv-blue-tint:rgba(0,156,222,.16);
  --canvas:#12171c; --paper:#1b232a; --input-bg:#232d35; --input-alt:#202932;
  --line:#2a343d; --line-strong:#3a4650;
  --ink:#e9eef2; --muted:#9aa8b2; --muted-2:#6f7d87;
  --ok-tint:rgba(30,158,106,.18); --warn-tint:rgba(224,161,0,.20); --bad-tint:rgba(208,52,44,.20);
  --rail-top:#141b21; --rail-bot:#0d1216; --topbar-bg:rgba(18,23,28,.85);
  --shadow:0 1px 2px rgba(29,37,45,.4), 0 8px 26px -14px rgba(29,37,45,.7);
  --shadow-lg:0 24px 60px -20px rgba(29,37,45,.75);
}

*{box-sizing:border-box}
html,body{margin:0; height:100%}
body{font-family:var(--font-body); color:var(--ink); background:var(--canvas);
  font-size:14px; line-height:1.5; -webkit-font-smoothing:antialiased;
  font-variant-numeric:tabular-nums; transition:background .2s, color .2s}
h1,h2,h3,h4{font-family:var(--font-display); font-weight:600; margin:0; letter-spacing:-.01em}
button{font-family:var(--font-body); cursor:pointer}
a{color:var(--nv-blue)}
:focus-visible{outline:2px solid var(--nv-blue); outline-offset:2px; border-radius:4px}
.mono{font-variant-numeric:tabular-nums; letter-spacing:.01em}
.hide{display:none !important}

/* logo: two provided colourways. .logo-white forces white file; .logo-auto swaps by theme */
img.logo{display:block; width:100%; height:auto}

/* ---------- controls ---------- */
.label{display:block; font-size:11.5px; font-weight:600; letter-spacing:.04em; text-transform:uppercase; color:var(--muted); margin:0 0 7px}
.field{width:100%; height:44px; padding:0 14px; border:1px solid var(--line-strong); border-radius:var(--r-sm);
  background:var(--input-bg); font-size:14.5px; color:var(--ink); font-family:var(--font-body); transition:border-color .15s, box-shadow .15s}
.field::placeholder{color:var(--muted-2)}
.field:focus{outline:none; border-color:var(--nv-blue); box-shadow:0 0 0 3px var(--nv-blue-tint)}
textarea.field{height:auto; padding:12px 14px; resize:vertical; min-height:110px; line-height:1.55}
select.field{appearance:none; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%235e6e79' d='M6 8 0 0h12z'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position:right 14px center; padding-right:34px}

.btn{height:44px; padding:0 20px; border:1px solid transparent; border-radius:var(--r-sm); font-size:14px; font-weight:600;
  display:inline-flex; align-items:center; gap:9px; justify-content:center; transition:background .15s, box-shadow .15s, transform .05s}
.btn:active{transform:translateY(1px)}
.btn svg{width:17px; height:17px}
.btn-primary{background:var(--nv-blue); color:#fff; box-shadow:0 6px 16px -8px rgba(0,156,222,.8)}
.btn-primary:hover{background:var(--nv-blue-600)}
.btn-danger{background:var(--bad); color:#fff; box-shadow:0 6px 16px -8px rgba(208,52,44,.8)}
.btn-danger:hover{background:#b8302b}

.health-pill{display:inline-flex; align-items:center; gap:8px; background:none; border:none; cursor:pointer; padding:6px 10px; border-radius:20px; font-size:13px; font-weight:600; color:var(--muted); font-family:inherit}
button.health-pill{cursor:pointer}
.health-pill:not(button){cursor:default}
.health-dot{width:8px; height:8px; border-radius:50%; background:var(--muted); flex-shrink:0}
.health-dot.ok{background:var(--ok)}
.health-dot.bad{background:var(--bad)}
.health-label{white-space:nowrap}
.health-pill-wrap{position:relative}
.health-detail{position:absolute; top:100%; right:0; margin-top:8px; background:var(--paper); border:1px solid var(--line); border-radius:var(--r); box-shadow:var(--shadow); padding:12px 16px; z-index:50; min-width:240px}
.health-row{display:flex; align-items:center; gap:8px; padding:5px 0; font-size:13px}
.health-sub{color:var(--muted); font-size:11px; margin-left:auto; white-space:nowrap}
.btn-ghost{background:var(--input-bg); color:var(--ink); border-color:var(--line-strong)}
.btn-ghost:hover{border-color:var(--muted-2)}
.btn:disabled{opacity:.55; cursor:not-allowed}
.btn-sm{height:34px; padding:0 13px; font-size:13px; border-radius:7px}

.pill{display:inline-flex; align-items:center; gap:6px; height:24px; padding:0 10px 0 8px; border-radius:999px; font-size:12px; font-weight:600}
.pill .dot{width:7px; height:7px; border-radius:50%}
.pill.ok{background:var(--ok-tint); color:#1c7742} .pill.ok .dot{background:var(--ok)}
.pill.warn{background:var(--warn-tint); color:#975E09} .pill.warn .dot{background:var(--warn)}
.pill.bad{background:var(--bad-tint); color:#ab3733} .pill.bad .dot{background:var(--bad)}
.pill.info{background:var(--nv-blue-tint); color:#0a6a92} .pill.info .dot{background:var(--nv-blue)}
[data-theme="dark"] .pill.ok{color:#6fd79a}
[data-theme="dark"] .pill.warn{color:#f0b45a}
[data-theme="dark"] .pill.bad{color:#f08a85}
/* ⚠️ REQUIRED, not decorative. --nv-blue-tint is TRANSLUCENT in dark mode
   (rgba(0,156,222,.16)), so it composites to #173647 over --paper. The light
   ink #0a6a92 on that is 2.11:1 — below even the 3:1 large-text floor, while
   the other three tiers sit at 5.46-7.03. #80c7ea gives 6.81:1.
   Derived by applying the mean toward-white fraction of the ok/warn/bad dark
   inks to --nv-blue, not chosen by eye. A new tier added WITHOUT its dark
   override looks correct in light mode and is unreadable in dark. */
[data-theme="dark"] .pill.info{color:#80c7ea}

.err{background:var(--bad-tint); color:#ab3733; border:1px solid rgba(208,52,44,.3); border-radius:var(--r-sm);
  padding:11px 13px; font-size:13px; font-weight:500; margin:0 0 16px}
[data-theme="dark"] .err{color:#f08a85}
.notice-ok{background:var(--ok-tint); color:#1c7742; border:1px solid rgba(30,158,106,.3); border-radius:var(--r-sm);
  padding:11px 13px; font-size:13px; font-weight:500; margin:0 0 16px}
[data-theme="dark"] .notice-ok{color:#6fd79a}
.notice-warn{background:var(--warn-tint); color:#975E09; border:1px solid rgba(224,161,0,.3); border-radius:var(--r-sm);
  padding:11px 13px; font-size:13px; font-weight:500; margin:0 0 16px}
[data-theme="dark"] .notice-warn{color:#f0b45a}
.spinner{width:18px; height:18px; border:2.5px solid rgba(255,255,255,.35); border-top-color:#fff; border-radius:50%; animation:spin .7s linear infinite; display:inline-block}
@keyframes spin{to{transform:rotate(360deg)}}
.center-load{min-height:100vh; display:grid; place-items:center; color:var(--muted)}

/* ---------- AUTH ---------- */
.auth{position:fixed; inset:0; display:grid; place-items:center; padding:24px; overflow:hidden;
  background:radial-gradient(1100px 700px at 82% -12%, color-mix(in srgb, var(--nv-blue) 8%, var(--canvas)) 0%, rgba(0,0,0,0) 60%), var(--canvas)}
.auth::before{content:""; position:absolute; width:780px; height:780px; right:-260px; bottom:-320px; border-radius:50%;
  border:1.5px solid rgba(0,156,222,.16); box-shadow:inset 0 0 0 34px rgba(0,156,222,.05); pointer-events:none}
.auth::after{content:""; position:absolute; width:440px; height:440px; left:-180px; top:-200px; border-radius:50%;
  border:1.5px dashed rgba(120,140,155,.14); pointer-events:none}
.auth-card{position:relative; width:100%; max-width:404px; background:var(--paper); border:1px solid var(--line);
  border-radius:16px; box-shadow:var(--shadow-lg); padding:36px 34px 30px}
.auth-logo{margin:0 auto 26px; display:flex; justify-content:center}
.auth-logo img.logo{width:88%}
.auth-eyebrow{font-size:11.5px; font-weight:600; letter-spacing:.14em; text-transform:uppercase; color:var(--nv-blue); margin:0 0 6px}
.auth-title{font-size:23px; color:var(--ink); margin:0 0 4px}
.auth-sub{color:var(--muted); font-size:13.5px; margin:0 0 24px}
.auth-card .field + .label{margin-top:16px}
.auth-actions{margin-top:22px} .auth-actions .btn{width:100%}
.auth-foot{margin-top:20px; padding-top:16px; border-top:1px solid var(--line); display:flex; justify-content:space-between; align-items:center; font-size:12.5px; color:var(--muted-2)}
.auth-foot a{color:var(--muted); text-decoration:none} .auth-foot a:hover{color:var(--nv-blue)}
.tagline{color:var(--muted-2); font-size:11px; letter-spacing:.16em; text-transform:uppercase}
.totp-row{display:flex; gap:9px}
.totp-row input{flex:1; width:100%; height:54px; text-align:center; font-size:22px; font-weight:600; font-family:var(--font-display);
  border:1px solid var(--line-strong); border-radius:var(--r-sm); background:var(--input-bg); color:var(--ink)}
.totp-row input:focus{outline:none; border-color:var(--nv-blue); box-shadow:0 0 0 3px var(--nv-blue-tint)}
.back-link{background:none; border:none; color:var(--muted); font-size:13px; font-weight:600; padding:0; margin-top:16px; display:inline-flex; align-items:center; gap:6px}
.back-link:hover{color:var(--nv-blue)}
.enroll-secret{font-family:var(--font-body); font-size:15px; font-weight:600; letter-spacing:.08em; background:var(--input-alt);
  border:1px solid var(--line); border-radius:var(--r-sm); padding:11px 13px; word-break:break-all; margin:4px 0 8px}
.enroll-secret-small{font-family:var(--font-body); font-size:11px; font-weight:600; letter-spacing:.06em; color:var(--muted);
  text-align:center; word-break:break-all; margin:10px 0 4px}
.qr-code{display:flex; justify-content:center; margin:10px 0}
.qr-code svg{width:180px; height:180px; background:#fff; border-radius:var(--r-sm); padding:10px; border:1px solid var(--line)}
.codes{display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 4px}
.codes span{font-family:var(--font-body); font-size:13.5px; font-weight:600; background:var(--input-alt); border:1px solid var(--line); border-radius:6px; padding:8px 10px; text-align:center}

/* ---------- SHELL ---------- */
.app{display:grid; grid-template-columns:var(--rail-w) 1fr; min-height:100vh}
.rail{background:linear-gradient(180deg, var(--rail-top) 0%, var(--rail-bot) 62%); color:#cfd8de; display:flex; flex-direction:column; position:sticky; top:0; height:100vh}
.rail-logo{padding:22px 0 18px; display:flex; justify-content:center}
.rail-logo img.logo{width:86%}
.nav{padding:6px 12px; flex:1; overflow:auto}
.nav-item{display:flex; align-items:center; gap:12px; padding:10px 12px; border-radius:9px; color:#c2ccd3; font-size:14px; font-weight:500;
  text-decoration:none; margin:2px 0; border:1px solid transparent; cursor:pointer; transition:background .13s, color .13s; width:100%; background:none; text-align:left}
.nav-item svg{width:18px; height:18px; flex:none; opacity:.85}
.nav-item:hover{background:rgba(255,255,255,.06); color:#fff}
.nav-item.active{background:rgba(0,156,222,.16); color:#fff; border-color:rgba(0,156,222,.4)}
.nav-item.active svg{opacity:1; color:var(--nv-blue)}
.nav-section{padding:16px 12px 6px; font-size:11px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#5f6b74}
.nav-section:first-child{padding-top:6px}
/* Outbound product links, pinned at the foot of the rail above the account
   block. Its children are .nav-item, which is styled by CLASS and not scoped
   to .nav — so only .nav's horizontal padding needed restoring here.
   These must sit OUTSIDE <nav>: .nav is flex:1 and fills the rail, so anything
   inside it renders at the TOP of that space, not the bottom. */
.rail-links{padding:8px 12px; border-top:1px solid rgba(255,255,255,.08)}
/* ⚠️ flex-direction:column; align-items:flex-start, RECONCILED FROM CANONICAL
   2026-09-03 (tasks/leo/2026-09-03-reconcile-rail-layout-to-shared-rule.md,
   nexus-shared-assets.git theme.css @ 66e79ab). Was display:flex with no
   direction (a row, align-items:center), pushing .rail-actions (below) to
   the far right beside the avatar/name block - lclarke asked for the icon
   row below the name instead, left-aligned. Applied first as an app.js
   inline override (this file's own publish pipeline was blocked at the
   time); now the real rule, once canonical unblocked and the inline
   override was dropped from app.js. gap:11px is UNCHANGED - the inline
   override had it at 10px, which was never a considered choice, so removing
   the override reverts to this rule's own existing value rather than
   picking a new one. */
.rail-user{border-top:1px solid rgba(255,255,255,.08); padding:14px 16px; display:flex; flex-direction:column; align-items:flex-start; gap:11px}
.rail-user-profile{display:flex; align-items:center; gap:11px; min-width:0; flex:1; border-radius:9px; padding:4px 6px; margin:-4px -6px}
.rail-user-profile:hover{background:rgba(255,255,255,.06)}
.avatar{width:36px; height:36px; border-radius:9px; background:var(--nv-blue); color:#fff; font-family:var(--font-display); font-weight:600; display:grid; place-items:center; font-size:15px; flex:none}
.rail-user .who{min-width:0} .rail-user .who b{display:block; color:#fff; font-size:13.5px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
/* ⚠️ TRUNCATES LIKE THE NAME ABOVE IT. Line two now carries a CATEGORY plus an
   ORGANISATION NAME, and organisation names are unbounded. Without this a long
   one wraps and pushes the rail's account block out of shape.
   The category is written FIRST so the ellipsis eats the org name, never the
   category: the category is short, fixed and always meaningful, while the user
   already knows which organisation they are in. A title attribute carries the
   untruncated text. */
.rail-user .who span{font-size:12px; color:#8695a0; display:block;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
/* ⚠️ margin-left:0, RECONCILED FROM CANONICAL 2026-09-03 (same task/commit as
   .rail-user above) - was margin-left:auto, which only made sense pushing
   this row to the right of the avatar/name block in a ROW layout; now that
   .rail-user is a column (above), auto here would center this row instead
   of left-aligning it under the avatar. */
.rail-actions{margin-left:0; display:flex; align-items:center; gap:2px}
.icon-btn{background:none; border:none; color:#8695a0; padding:6px; border-radius:7px}
.icon-btn svg{width:18px; height:18px; display:block}
.icon-btn:hover{color:#fff; background:rgba(255,255,255,.08)}

.main{min-width:0; display:flex; flex-direction:column}
.topbar{height:66px; background:var(--topbar-bg); backdrop-filter:blur(8px); border-bottom:1px solid var(--line); display:flex; align-items:center; gap:16px; padding:0 30px; position:sticky; top:0; z-index:5}
.topbar h1{font-size:20px}
.topbar .crumb{font-size:12px; color:var(--muted-2); font-weight:600; letter-spacing:.04em; text-transform:uppercase; margin-bottom:1px}
.top-right{margin-left:auto; display:flex; align-items:center; gap:12px}
.content{padding:30px; max-width:1240px; width:100%}
.view{animation:rise .3s ease both}
@keyframes rise{from{opacity:0; transform:translateY(8px)} to{opacity:1; transform:none}}

.card{background:var(--paper); border:1px solid var(--line); border-radius:var(--r); box-shadow:var(--shadow)}
.card-h{padding:18px 22px; border-bottom:1px solid var(--line); display:flex; align-items:center; gap:12px}
/* ⚠️ ADDED 2026-08-05 on LEO-01's report, and it is a bug fix rather than a
   preference. The icons in the shared `I` map carry no width/height attributes:
   EVERY size in this file comes from a rule scoped to a parent (.nav-item svg,
   .icon-btn svg, .stat .si svg, .act .ai svg, .btn svg, .drop .di svg,
   .toast .ti svg). .card-h had none, so an icon dropped into a card header has
   nothing bounding it and expands to fill the flex row — LEO-01 rendered a pin
   and a bell several hundred pixels tall and it reached lclarke.
   ⚠️ The failure mode is ENORMOUS, not unstyled, which is worse: unstyled looks
   like a missing class, enormous looks like a broken component.
   18px + flex:none matches the .nav-item / .icon-btn precedent exactly. */
.card-h svg{width:18px; height:18px; flex:none}
.card-h .chevron{font-size:18px; color:var(--muted); line-height:1}
.card-h h3{font-size:16px}
.card-h .sub{font-size:12.5px; color:var(--muted); font-weight:400; font-family:var(--font-body)}
.card-h .more{margin-left:auto; font-size:13px; font-weight:600; color:var(--nv-blue); background:none; border:none}
.card-b{padding:22px}

/* dashboard */
.hero{display:flex; align-items:flex-end; justify-content:space-between; gap:16px; margin-bottom:24px}
.hero h2{font-size:26px; font-weight:600} .hero p{color:var(--muted); margin:5px 0 0; font-size:14px}
.stats{display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-bottom:22px}
.stat{background:var(--paper); border:1px solid var(--line); border-radius:var(--r); padding:17px 19px; box-shadow:var(--shadow); position:relative}
.stat .sl{font-size:11.5px; font-weight:600; letter-spacing:.03em; text-transform:uppercase; color:var(--muted)}
.stat .sv{font-family:var(--font-display); font-size:30px; font-weight:600; margin-top:7px; line-height:1}
.stat .sh{font-size:12.5px; color:var(--muted); margin-top:6px}
.stat .si{position:absolute; top:16px; right:16px; width:32px; height:32px; border-radius:9px; display:grid; place-items:center; background:var(--nv-blue-tint); color:var(--nv-blue)}
.stat .si svg{width:17px; height:17px}
.stat.alert .sv{color:var(--bad)} .stat.alert .si{background:var(--bad-tint); color:var(--bad)}
/* Amber tier for stat cards, matching the .alert shape exactly. .stat had ONE
   variant and it was red, while .pill has all three - so a stat card could be
   red or nothing. LEO-01 was setting --warn inline on .sv and .si, duplicating
   a pattern the file already expresses one selector away. */
.stat.warn .sv{color:var(--warn)} .stat.warn .si{background:var(--warn-tint); color:var(--warn)}
/* ⚠️ .stat-link, PART G (2026-08-26, dashboard tile deep-links) - purely
   additive, `.stat`/`.stat.alert`/`.stat.warn` above are untouched. Some
   dashboard tiles now render as <button class="stat stat-link ..."> instead
   of <div class="stat ...">, so a tile stays visually IDENTICAL to before
   except for the new hover affordance - a bare <button> carries its own
   browser-default chrome (shrink-to-fit width, centered text, its own font)
   that a <div> never had, the same reset .health-pill already does for its
   own button (background/border/font-family, above). display/width/
   text-align are restated here to match a <div>'s own default rendering;
   background/border are restated to the exact values `.stat` already gives
   it, rather than left to fall back to the browser's own button styling.
   Hover follows the same var(--input-alt) convention already used by
   .ga-table tbody tr:hover/.ga-a-row/.modal-h button, just above.

   ⚠️ color:var(--ink) ADDED, lclarke 2026-08-26 - missing from the first
   pass, and it mattered: a bare button's own default text colour does not
   inherit from the page the way a div's does, so the .sv value inside a
   stat-link tile rendered in the browser's own default button colour
   instead of following the theme - invisible in light mode by coincidence
   (that default is already dark), wrong in dark mode (it should have been
   light). Every other button-based component in this file already states
   its own colour explicitly for the same reason (.btn, .ga-rail-item, the
   .burger, and others) - this rule should have from the start too. */
.stat-link{display:block; width:100%; text-align:left; background:var(--paper); border:1px solid var(--line);
  color:var(--ink); font:inherit; cursor:pointer; transition:background .15s}
.stat-link:hover{background:var(--input-alt)}
.dash-grid{display:grid; grid-template-columns:1.5fr 1fr; gap:22px; align-items:start}
.act{display:flex; align-items:center; gap:13px; padding:13px 22px; border-bottom:1px solid var(--line)}
.act:last-child{border-bottom:none}
.act .ai{width:34px; height:34px; border-radius:9px; display:grid; place-items:center; flex:none}
.act .ai.out{background:var(--nv-blue-tint); color:var(--nv-blue)} .act .ai.in{background:var(--ok-tint); color:var(--ok)}
/* ⚠️ STATUS tiers for .act .ai, added because their absence caused a real
   defect rather than an inconvenience. The set was `out` (blue) and `in`
   (green) with no red and no amber, so LEO-01 used `out` for a fault and
   every offline row showed a BLUE triangle. The class was defined, so the
   CSS gate passed and nothing errored - the .card-h svg failure again.

   ⚠️ AND THE NAMES ARE THE CAUSE, but only HERE. On .dir, `in`/`out` are
   genuinely directional - inbound versus outbound fax - where blue and green
   are CATEGORY colours and the names are correct. On .act .ai the same two
   names carry STATUS, so "out" reads as a category and renders as a decision.
   The defect is .act .ai reusing direction names for status, not the names
   being bad everywhere. Renaming .dir would break a correct thing.

   NOT renaming .ai.in/.ai.out: Nexus uses neither, LEO-01 offered to take the
   churn, but NVT-FAX01's usage is unestablished and its session is
   intermittent. A rename is a removal plus an addition, and the removal
   silently unstyles any host still on the old name - which is the exact
   failure mode this whole class of bug belongs to. Extending is additive and
   safe; the rename wants fax's usage measured first. */
.act .ai.bad{background:var(--bad-tint); color:var(--bad)} .act .ai.warn{background:var(--warn-tint); color:var(--warn)}
.act .ai svg{width:16px; height:16px}
.act .am{min-width:0; flex:1} .act .am b{font-size:13.5px; font-weight:600; display:block} .act .am span{font-size:12.5px; color:var(--muted)}
.act .aw{font-size:12.5px; color:var(--muted-2); white-space:nowrap}
.svc{padding:16px 22px; border-bottom:1px solid var(--line)} .svc:last-child{border-bottom:none}
.svc .sn{font-size:14px; font-weight:600; display:flex; align-items:center; justify-content:space-between; gap:10px}
.svc .sd{font-size:13px; color:var(--muted); margin-top:4px}
.svc .meta{display:flex; gap:16px; margin-top:11px; font-size:12.5px; color:var(--muted); flex-wrap:wrap}
.svc .meta b{color:var(--ink); font-weight:600}

/* send fax */
.compose{display:grid; grid-template-columns:1.35fr 1fr; gap:22px; align-items:start}
.row2{display:grid; grid-template-columns:1fr 1fr; gap:16px}
.field-note{font-size:12px; color:var(--muted); margin:7px 0 0}
.drop{border:1.5px dashed var(--line-strong); border-radius:var(--r-sm); background:var(--input-alt); padding:26px 20px; text-align:center; transition:border-color .15s, background .15s; cursor:pointer}
.drop:hover,.drop.over{border-color:var(--nv-blue); background:var(--nv-blue-tint)}
.drop .di{width:44px; height:44px; border-radius:11px; background:var(--nv-blue-tint); color:var(--nv-blue); display:grid; place-items:center; margin:0 auto 12px}
.drop .di svg{width:22px; height:22px}
.drop b{font-weight:600; font-size:14px} .drop span{display:block; color:var(--muted); font-size:12.5px; margin-top:3px}
.file-chip{display:flex; align-items:center; gap:11px; padding:11px 13px; border:1px solid var(--line); border-radius:var(--r-sm); margin-top:11px; background:var(--input-bg)}
.file-chip .fi{width:34px; height:34px; border-radius:7px; background:var(--bad-tint); color:var(--bad); display:grid; place-items:center; font-size:11px; font-weight:700; flex:none}
.file-chip .fn{font-size:13.5px; font-weight:600} .file-chip .fm{font-size:12px; color:var(--muted)}
.file-chip .rm{margin-left:auto; color:var(--muted-2); background:none; border:none; padding:6px; border-radius:6px}
.file-chip .rm svg{width:16px; height:16px; display:block}
.file-chip .rm:hover{color:var(--bad); background:var(--bad-tint)}
.toggle-row{display:flex; align-items:center; gap:13px; padding:15px 0 0}
.switch{position:relative; width:44px; height:26px; flex:none}
.switch input{opacity:0; width:0; height:0; position:absolute}
.slider{position:absolute; inset:0; background:var(--line-strong); border-radius:999px; transition:background .18s; cursor:pointer}
.slider::before{content:""; position:absolute; width:20px; height:20px; border-radius:50%; background:#fff; top:3px; left:3px; transition:transform .18s; box-shadow:0 1px 3px rgba(29,37,45,.25)}
.switch input:checked + .slider{background:var(--nv-blue)}
.switch input:checked + .slider::before{transform:translateX(18px)}
.toggle-row .tt b{font-size:14px; font-weight:600} .toggle-row .tt span{display:block; font-size:12.5px; color:var(--muted)}
.cover-wrap{margin-top:16px}
.rte{border:1px solid var(--line-strong); border-radius:var(--r-sm); overflow:hidden}
.rte-bar{display:flex; gap:2px; padding:6px; border-bottom:1px solid var(--line); background:var(--input-alt)}
.rte-bar button{width:32px; height:30px; border:none; background:none; border-radius:6px; color:var(--muted); font-size:14px; font-weight:700}
.rte-bar button:hover{background:var(--nv-blue-tint); color:var(--nv-blue)}
.rte-area{padding:13px 14px; min-height:96px; font-size:14px; outline:none; background:var(--input-bg)}
.rte-area:empty::before{content:attr(data-ph); color:var(--muted-2)}
.send-aside .card-b{display:flex; flex-direction:column; gap:14px}
.summ{display:flex; justify-content:space-between; gap:12px; font-size:13.5px; padding:9px 0; border-bottom:1px solid var(--line)}
.summ:last-of-type{border-bottom:none}
.summ .k{color:var(--muted)} .summ .v{font-weight:600; text-align:right; word-break:break-word}

/* table */
.table-wrap{overflow-x:auto}
table{width:100%; border-collapse:collapse}
thead th{text-align:left; font-size:11px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:var(--muted); padding:12px 22px; border-bottom:1px solid var(--line); white-space:nowrap}
tbody td{padding:14px 22px; border-bottom:1px solid var(--line); font-size:13.5px; vertical-align:middle}
tbody tr:last-child td{border-bottom:none}
tbody tr:hover{background:var(--input-alt)}
.dir{display:inline-flex; align-items:center; gap:7px; font-weight:600; font-size:13px}
.dir svg{width:15px; height:15px}
.dir.in{color:var(--ok)} .dir.out{color:var(--nv-blue)}
.num{font-weight:600}
.dl{color:var(--nv-blue); background:none; border:1px solid var(--line); border-radius:7px; height:32px; padding:0 11px; font-size:12.5px; font-weight:600; display:inline-flex; align-items:center; gap:6px; text-decoration:none}
.dl:hover{background:var(--nv-blue-tint); border-color:var(--nv-blue)}
.dl:disabled{color:var(--muted-2); border-color:var(--line); cursor:not-allowed}
.dl svg{width:14px; height:14px}
.dl.danger{color:var(--bad); border-color:rgba(208,52,44,.35)}
.dl.danger:hover{background:var(--bad-tint); border-color:var(--bad)}
.dl.danger:disabled{color:var(--muted-2); border-color:var(--line)}
.filters{display:flex; gap:9px; margin-left:auto}
.seg{display:flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; background:var(--paper)}
/* ⚠️ .ga-tabstrip, ROUND 4 ITEM 1 (2026-08-26) - the individual service
   page's tab bar (LineDetail's role="tablist") reuses .seg for its own
   button/border/background styling, same as everywhere else .seg appears,
   but needs the ONE thing .seg's own overflow:hidden actively prevents:
   scrolling past a narrow viewport instead of clipping. Scoped to this
   second class (a compound selector, .seg.ga-tabstrip, genuinely more
   specific than bare .seg regardless of source order) rather than changing
   .seg itself, which is also the Table/Cards view toggle elsewhere
   (ServiceLines/KitList) - a short, 2-button control that should stay
   compact and never scroll; touching .seg directly would have changed that
   control's behaviour too, for no reason. flex-wrap stays nowrap (the
   default) rather than wrapping to a second row: a wrapped STICKY bar would
   shift its own height, and everything sticky below it, every time the
   active tab or the viewport changes - horizontal scroll keeps the bar's
   own height constant regardless of tab count. */
.seg.ga-tabstrip{overflow-x:auto; overflow-y:hidden; -webkit-overflow-scrolling:touch}
.seg.ga-tabstrip button{flex:none; white-space:nowrap}
.seg button{height:32px; padding:0 13px; border:none; background:none; font-size:13px; font-weight:600; color:var(--muted)}
.seg button.on{background:var(--charcoal); color:#fff}
[data-theme="dark"] .seg button.on{background:var(--nv-blue)}
.empty{padding:44px 22px; text-align:center; color:var(--muted)}
.empty b{display:block; font-size:15px; color:var(--ink); font-weight:600; margin-bottom:4px}

/* ---------- pagination ---------- */
.pager{display:flex; align-items:center; gap:18px; padding:14px 22px; border-top:1px solid var(--line); flex-wrap:wrap}
.pager-info{font-size:12.5px; color:var(--muted); margin-right:auto}
.pager-size{display:flex; align-items:center; gap:8px; font-size:12.5px; color:var(--muted)}
.pager-select{height:32px; padding:0 26px 0 10px; border:1px solid var(--line-strong); border-radius:7px;
  background:var(--input-bg) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%235e6e79' d='M6 8 0 0h12z'/%3E%3C/svg%3E") no-repeat right 10px center;
  appearance:none; font-size:13px; color:var(--ink); font-family:var(--font-body)}
.pager-nav{display:flex; align-items:center; gap:10px}
.pager-page{font-size:12.5px; color:var(--muted); white-space:nowrap}
/* Added 2026-08-03 on NVT-FAX01, adopted into the shared file 2026-08-05.
   Used 3x in the fax portal (Show / Tenant / Viewing labels) where it had no
   rule, so those labels rendered with default browser styling beside
   correctly-styled siblings. Matches the .pager-* family.
   ⚠️ THIS COMMENT IS THE REASON THE RULE EXISTS. The header above records that
   it is used three times, which is the reconciliation fact; this records why —
   which is the evidence against deleting it as purposeless. Body is currently
   byte-identical to .pager-page; they are kept SEPARATE deliberately, because
   merging would encode "these are the same thing" and the next divergence
   would have to un-encode it. (FAX01's text, carried by its request.) */
.pager-label{font-size:12.5px; color:var(--muted); white-space:nowrap}

/* ---------- themed searchable select (ThemedSelect, app.js) ----------
   Placed here rather than appended with the rest of this component's CSS
   further down (Ground Station round 6, near the end of this file) - on
   purpose, for discoverability beside the class its closed state visually
   matches. A native <select>'s CLOSED state can already be styled freely
   - .pager-select, directly above, does exactly that - but its OPEN popup
   cannot be styled cross-browser at all: that popup's own border, radius,
   colour and spacing come from the OS/browser chrome on every engine this
   app ships to, not from this stylesheet. That is the actual, sole reason
   a new component exists rather than just a styled <select> - everything
   below renders that popup as ordinary DOM instead, so it can carry this
   file's own tokens like any other panel here (compare .health-detail,
   above, the same "panel anchored under its own trigger" shape).
   .ga-ts-btn is used TOGETHER with .pager-select on the same element (see
   every ThemedSelect call site in app.js), not as a replacement for it -
   height/padding/border/radius/font and even the caret background-image
   are that class's own exact declarations, not a close copy of them. */
.ga-ts-wrap{position:relative; display:inline-block; min-width:0}
.ga-ts-btn{display:inline-flex; align-items:center; width:100%; min-width:0}
.ga-ts-btn-label{overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0}
/* ⚠️ .ga-ts-wrap-field - lclarke, 2026-08-27 (RuleModal's five selects
   converted to ThemedSelect this round, app.js). .ga-ts-wrap's own default,
   above, is display:inline-block with no width set - correct for every
   TOOLBAR use of ThemedSelect (it should only ever be as wide as its own
   label), wrong for a select standing in a plain vertical FORM next to
   .field text inputs, which are all width:100% (.field, above) - left at
   the default there, a ThemedSelect shrinks to fit only its own selected
   option's text, next to full-width siblings above and below it. This is
   a second, OPT-IN class rather than a change to .ga-ts-wrap itself, so
   every existing toolbar call site (ServiceLines/KitList, above) is
   completely unaffected - only a call site that passes it as its own
   `class` prop gets display:block;width:100% on top of .ga-ts-wrap's other
   rules. Deliberately does NOT touch .ga-ts-btn's own height (still
   .pager-select's 32px, not .field's 44px) - ThemedSelect's closed state is
   meant to look like ONE control everywhere it is used (see its own doc
   comment above: ".pager-select... not inventing a second visual
   language"), so this only ever widens it to match its new context, never
   grows it to imitate .field's own taller box. */
.ga-ts-wrap-field{display:block; width:100%}
/* ⚠️ POSITION SET INLINE BY JS, NOT HERE - lclarke, 2026-08-26 (AssignModal's
   org picker getting clipped by .modal-b's own overflow:auto, below - a
   position:absolute descendant is still clipped by an overflow:auto
   ancestor no matter where it visually appears to "float"). This popup now
   mounts through the same document.body portal ModalPortal already uses for
   AssignModal/RuleModal/NicknameModal (see ModalPortal's own doc comment in
   app.js) instead of as a plain nested child of .ga-ts-wrap, so a modal's
   own overflow can no longer clip it. position/top/left/min-width are
   computed per-open from the trigger button's real getBoundingClientRect()
   and set as an inline style in app.js instead of declared here - only this
   rule's VISUAL properties stay. z-index raised from 50 to sit above
   .modal-overlay's own 2000 (see that rule's own comment for why 2000 is
   what it is): the popup needs to render above the modal it can now open
   inside, not just above ordinary page content the way 50 was originally
   enough for. */
.ga-ts-pop{min-width:100%; width:max-content; max-width:280px;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-sm); box-shadow:var(--shadow-lg);
  z-index:2001; padding:6px; display:flex; flex-direction:column; gap:4px}
.ga-ts-search{height:32px; padding:0 10px; border:1px solid var(--line-strong); border-radius:7px;
  background:var(--input-bg); font-size:13px; color:var(--ink); font-family:var(--font-body)}
.ga-ts-list{max-height:240px; overflow-y:auto; display:flex; flex-direction:column; gap:1px}
.ga-ts-opt{padding:8px 10px; border-radius:6px; font-size:13px; color:var(--ink); cursor:pointer;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
.ga-ts-opt.ga-ts-hi{background:var(--input-alt)}
.ga-ts-opt.ga-ts-active{font-weight:600; color:var(--nv-blue)}
.ga-ts-empty{padding:8px 10px; font-size:13px; color:var(--muted)}

/* ---------- modal (rejection headers) ---------- */
/* ⚠️ z-index:2000, WAS 60, lclarke 2026-08-26 (direct report: the Fleet Map
   did not dim/blur behind the new nickname modal, and the modal did not
   look centered - both traced to the same real cause here, not two
   separate bugs). The vendored leaflet.css gives Leaflet's own panes and
   controls z-index values up to 1000 (.leaflet-top/.leaflet-bottom, its
   zoom control container) - any page with a Fleet Map open behind a modal
   had that control escaping the dim/blur and remaining clickable THROUGH
   the modal. Confirmed directly in a real headless-Chromium session before
   fixing: Playwright's own click-actionability check on the zoom-in button
   reported it reachable (not blocked) at z-index:60, and reported "modal-
   overlay subtree intercepts pointer events" (correctly blocked) once
   raised past Leaflet's own maximum. 2000 leaves headroom above every
   z-index value in the vendored Leaflet CSS, not just today's highest one. */
.modal-overlay{position:fixed; inset:0; background:rgba(10,14,18,.55); backdrop-filter:blur(2px); z-index:2000; display:flex; align-items:center; justify-content:center; padding:24px}
.modal{background:var(--paper); border:1px solid var(--line); border-radius:var(--r); box-shadow:var(--shadow-lg); width:100%; max-width:560px; max-height:80vh; display:flex; flex-direction:column}
.modal-h{padding:16px 20px; border-bottom:1px solid var(--line); display:flex; align-items:center; gap:12px}
.modal-h h3{font-size:15px}
.modal-h button{margin-left:auto; background:none; border:none; color:var(--muted); padding:6px; border-radius:7px}
.modal-h button:hover{color:var(--ink); background:var(--input-alt)}
.modal-b{padding:18px 20px; overflow:auto}
.hdrs{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:12.5px; line-height:1.6; white-space:pre-wrap; word-break:break-word; background:var(--input-alt); border:1px solid var(--line); border-radius:var(--r-sm); padding:14px; color:var(--ink)}

/* ---------- received / rejections ---------- */
.act .aw.col{display:flex; flex-direction:column; align-items:flex-end; gap:6px}
/* toast */
.toast{position:fixed; right:26px; bottom:26px; background:var(--charcoal); color:#fff; z-index:50; border-radius:var(--r);
  padding:14px 16px 14px 15px; display:flex; align-items:center; gap:13px; box-shadow:var(--shadow-lg);
  transform:translateY(140%); transition:transform .35s cubic-bezier(.2,.9,.3,1); max-width:360px}
.toast.show{transform:none}
[data-theme="dark"] .toast{background:#000; border:1px solid var(--line)}
.toast .ti{width:34px; height:34px; border-radius:9px; background:rgba(0,156,222,.2); color:var(--nv-blue); display:grid; place-items:center; flex:none}
.toast .ti svg{width:18px; height:18px}
.toast.bad .ti{background:var(--bad-tint); color:var(--bad)}
.toast b{font-size:14px; display:block} .toast span{font-size:12.5px; color:#aeb9c1}

/* responsive */
.burger{display:none; width:38px; height:38px; border:1px solid var(--line); border-radius:8px; background:var(--paper); color:var(--ink); align-items:center; justify-content:center}
.burger svg{width:20px; height:20px}
@media (max-width:960px){ .stats{grid-template-columns:repeat(2,1fr)} .dash-grid{grid-template-columns:1fr} }
@media (max-width:900px){
  .app{grid-template-columns:1fr}
  .rail{position:fixed; left:0; top:0; z-index:40; transform:translateX(-100%); transition:transform .25s; box-shadow:var(--shadow-lg)}
  .rail.open{transform:none}
  .burger{display:inline-flex}
  .compose{grid-template-columns:1fr} .content{padding:20px}
}
@media (max-width:560px){ .row2{grid-template-columns:1fr} .stats{grid-template-columns:1fr} .hero{flex-direction:column; align-items:flex-start} }
@media (prefers-reduced-motion:reduce){ *{animation:none !important; transition:none !important} }

/* ============================================================
   Ground Station (/dev-alpha/ only) - ADDITIVE ONLY
   Owner (of this block): LEO-01, review-concept track. See
   nexus-ux-tasks.git tasks/leo/2026-08-24-dev-alpha-redesign.md.

   Every selector below is new and every one is prefixed `ga-`
   (Ground Station), on purpose: the prototype this was built from reuses
   several bare names already defined ABOVE in this file with a different
   look - .pill, .card, .rail, .switch, .table-wrap, .seg, .health-dot among
   them - and those are load-bearing for the Profile page and the
   individual service-detail page on every portal this file serves, not
   just /dev-alpha/. Nothing above this banner is modified, reordered or
   removed; this block only ever ADDS rules and, in the second :root block
   further down, ADDS custom properties that do not already exist above.
   Modifier classes (.ga-on, .ga-warn, .ga-crit, .ga-online, .ga-offline,
   .ga-suspended) are ga-prefixed too, for the same reason - so a bare
   `.on`/`.warn`/`.online` etc. already meaningful above (e.g. .seg
   button.on, .pill.warn) is never coincidentally reused here.
   ============================================================ */
:root{
  /* Fixed dark "instrument panel" palette for the fleet-coverage radar -
     deliberately NOT overridden under [data-theme="dark"]: the panel is a
     constant dark surface regardless of the portal's own light/dark theme,
     same as the prototype. Every other new rule below reuses the EXISTING
     tokens from the first :root block above (--nv-blue, --ok/--warn/--bad
     and their -tint pairs, --ink, --muted, --muted-2, --line, --line-strong,
     --paper, --input-bg, --input-alt, --charcoal, --rail-top/--rail-bot,
     --shadow/--shadow-lg, --font-display/--font-body) rather than adding a
     parallel set. --r-lg is the one other genuinely new token: the existing
     --r (11px) and --r-sm (8px) have no larger sibling, and the prototype's
     hero/ground/attention/table panels all use an 18px radius neither
     covers. */
  --void:#080b0e; --void-line:#182028; --void-ink:#e6edf1; --void-muted:#8a97a0;
  --r-lg:18px;
}

/* ---- shell chrome (Shell only) ---- */
/* ⚠️ .card-h-actions, ADDED 2026-09-03 (tasks/leo/2026-09-03-dedicated-class-
   fleet-map-card-header.md, following tasks/leo/done/2026-09-03-reconcile-
   rail-layout-to-shared-rule.md). A right-aligned row of controls inside a
   .card-h (theme.css, shared section, above) - built for SvgFleetMap's own
   card header (app.js): the online/warning/offline counts plus zoom
   in/out/fit buttons, pushed to the right of the "Fleet map" title.
   Previously this reused .rail-actions purely for its flex-row shape
   (display:flex;align-items:center;gap:2px), with its own inline
   style="margin-left:auto" carrying the actual alignment - which worked
   right up until the 2026-09-03 rail-footer reconciliation changed
   .rail-actions' own canonical default from margin-left:auto to
   margin-left:0, leaving this card header's alignment surviving on nothing
   but an inline override and a comment. A dedicated class removes that
   structurally instead of relying on the comment to keep being read: this
   markup (Fleet map / ServiceMap) is LEO-only, so this is a pure LEO-local
   addition - nothing here is published to nexus-shared-assets.git, nothing
   for the fleet-wide drift check to track. */
.card-h-actions{display:flex; align-items:center; gap:2px; margin-left:auto}
.ga-app{display:grid; grid-template-columns:76px 1fr; min-height:100vh}
/* ⚠️ padding:18px 0 REMOVED, lclarke 2026-09-03 (nexus-ux-tasks.git
   tasks/leo/2026-09-03-mobile-rail-zindex-and-padding.md) - direct
   instruction, not a value judgment left open here. This was the only
   vertical breathing room between the rail's own top/bottom edges and its
   logo/footer content (.rail-logo/.ga-rail-foot each carry their own
   separate padding already) - checked live at 390px after removal (see
   LEO STATUS, same date): logo/footer sit closer to the rail edges but not
   flush against them, so no compensating rule was added. */
.ga-rail{background:linear-gradient(180deg, var(--rail-top) 0%, var(--rail-bot) 62%); color:#cfd8de;
  display:flex; flex-direction:column; align-items:stretch; position:sticky; top:0; height:100vh}
.ga-rail-mark{width:38px; height:38px; margin-bottom:22px; flex:none}
.ga-rail-nav{display:flex; flex-direction:column; gap:6px; flex:1; width:100%; align-items:stretch; padding:0 14px}
.ga-rail-item{width:48px; height:48px; border-radius:12px; display:flex; flex-direction:column; align-items:center;
  justify-content:center; gap:3px; color:#8f9aa3; background:none; border:1px solid transparent; position:relative;
  text-decoration:none}
.ga-rail-item svg{width:19px; height:19px}
.ga-rail-item span{font-size:9px; font-weight:600; letter-spacing:.02em}
.ga-rail-item:hover{background:rgba(255,255,255,.07); color:#e4e9ec}
.ga-rail-item.ga-on{background:rgba(0,156,222,.18); color:#fff; border-color:rgba(0,156,222,.4)}
.ga-rail-item.ga-on::before{content:""; position:absolute; left:-14px; top:50%; transform:translateY(-50%);
  width:3px; height:20px; background:var(--nv-blue); border-radius:0 3px 3px 0}
.ga-rail-foot{margin-top:auto; display:flex; flex-direction:column; align-items:stretch; gap:8px; padding:0 14px}
.ga-rail-avatar{width:36px; height:36px; border-radius:9px; background:var(--nv-blue); color:#fff;
  font-family:var(--font-display); font-weight:600; display:grid; place-items:center; font-size:14px;
  border:none; cursor:pointer}
.ga-main{min-width:0; display:flex; flex-direction:column}
.ga-topbar{height:66px; display:flex; align-items:center; gap:16px; padding:6px 32px; border-bottom:1px solid var(--line);
  background:var(--topbar-bg); backdrop-filter:blur(8px); position:sticky; top:0; z-index:5}
/* Mobile-only drawer opener - hidden above 768px (see the max-width:768px
   block below), reusing the existing .icon-btn look. */
.ga-burger-btn{display:none}
/* ⚠️ display:none HERE TOO, lclarke via mobile audit verifier (2026-08-26) -
   real, reproduced defect: this element had NO rule outside the
   max-width:768px block below, so opening the drawer on a narrow viewport
   and then resizing/rotating to desktop width WHILE it was still open
   (railOpen is JS state - nothing resets it on a breakpoint crossing) left
   it at its default position:static, occupying a real block in normal
   document flow. At a wide viewport that meant it filled the .ga-app grid's
   own first column track, pushing the actual rail into the second column
   and shoving the topbar/content out of view entirely - confirmed directly
   via computed style and a real screenshot, not a hypothetical. Same fix
   shape as .ga-burger-btn just above: inert by default, only switched on
   inside the one media query that gives it meaning. */
.ga-rail-backdrop{display:none}
.ga-topbar h1{font-size:18px}
.ga-crumb{font-size:11px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:var(--muted-2)}
.ga-top-right{margin-left:auto; display:flex; align-items:center; gap:14px}
.ga-theme-seg{display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden}
.ga-theme-seg button{font-size:12px; font-weight:600; color:var(--muted); background:var(--paper); border:none; padding:6px 12px}
.ga-theme-seg button.ga-on{background:var(--nv-blue); color:#fff}
.ga-content{padding:28px 32px 60px}
/* ⚠️ fill-mode:both REMOVED, lclarke 2026-08-26 - see ga-rise's own comment,
   just above, for the real mechanism (a position:fixed modal opened on a
   scrolled page centring on .ga-view's own box instead of the true
   window). At rest (animation finished, fill-mode default of none), this
   element's own un-animated style already gives opacity:1 and no transform
   - byte-for-byte the same appearance the old to{} keyframe held, just no
   longer held BY an animation that is still "current". */
.ga-view{animation:ga-rise .35s ease}
/* ⚠️ ga-rise's OWN comment did not need to change - "none" in the
   keyframe already meant the animation LOOKS finished. The bug was that
   .ga-view's animation shorthand carried fill-mode:both, which keeps this
   animation "current" indefinitely after it plays once, and an element
   with a current transform animation is a containing block for position:
   fixed descendants regardless of what the animated value settles to -
   confirmed directly (a real browser reported .ga-view's own computed
   transform as a literal matrix, not the keyword none, for as long as
   fill-mode:both held it). Removing fill-mode (see .ga-view below) lets the
   animation stop being "current" once it finishes, at which point the
   element's own un-animated computed style - opacity:1, transform:none,
   the correct at-rest values regardless - takes back over on its own; no
   change was needed here. */
@keyframes ga-rise{from{opacity:0; transform:translateY(6px)} to{opacity:1; transform:none}}

/* Mobile off-canvas rail drawer - moved below the "collapsible rail" banner,
   see that block's own trailing comment for why. */

/* ---- dashboard: hero stats + vitals ---- */
.ga-dash-top{display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:16px}
.ga-hero-stat{background:var(--paper); border:1px solid var(--line); border-radius:var(--r-lg); padding:22px 24px;
  box-shadow:var(--shadow); position:relative; overflow:hidden}
.ga-hero-stat::after{content:""; position:absolute; right:-30px; top:-30px; width:120px; height:120px; border-radius:50%;
  background:radial-gradient(circle, var(--nv-blue-tint) 0%, transparent 70%); opacity:.7; pointer-events:none}
.ga-hero-stat.ga-warn::after{background:radial-gradient(circle, var(--warn-tint) 0%, transparent 70%)}
.ga-hero-stat .ga-hl{font-size:11.5px; font-weight:600; letter-spacing:.05em; text-transform:uppercase; color:var(--muted); position:relative}
.ga-hero-stat .ga-hv{font-family:var(--font-display); font-size:44px; font-weight:600; letter-spacing:-.02em; line-height:1;
  margin-top:8px; position:relative}
.ga-hero-stat.ga-warn .ga-hv{color:var(--warn)}
.ga-hero-stat .ga-hs{font-size:12.5px; color:var(--muted); margin-top:8px; position:relative; display:flex; align-items:center; gap:6px}
.ga-vitals{display:flex; gap:0; background:var(--paper); border:1px solid var(--line); border-radius:var(--r);
  box-shadow:var(--shadow); margin-bottom:22px; overflow:hidden; flex-wrap:wrap}
.ga-vital{flex:1; padding:14px 20px; display:flex; flex-direction:column; gap:3px; position:relative; min-width:150px}
.ga-vital + .ga-vital::before{content:""; position:absolute; left:0; top:14px; bottom:14px; width:1px; background:var(--line)}
.ga-vital .ga-vl{font-size:10.5px; font-weight:600; letter-spacing:.05em; text-transform:uppercase; color:var(--muted-2)}
.ga-vital .ga-vv{font-family:var(--font-display); font-size:20px; font-weight:600}
.ga-vital .ga-vv small{font-family:var(--font-body); font-size:12px; font-weight:500; color:var(--muted)}

/* ---- dashboard: fleet coverage radar + needs-attention feed ---- */
.ga-ops-grid{display:grid; grid-template-columns:1.55fr 1fr; gap:18px; align-items:start}
.ga-ground{background:var(--void); border:1px solid var(--void-line); border-radius:var(--r-lg); box-shadow:var(--shadow-lg);
  overflow:hidden; position:relative}
.ga-ground-head{display:flex; align-items:center; justify-content:space-between; padding:16px 20px;
  border-bottom:1px solid var(--void-line); position:relative; z-index:2}
.ga-ground-head h3{color:var(--void-ink); font-size:14px}
.ga-live{display:flex; align-items:center; gap:7px; font-size:11.5px; font-weight:600; color:var(--ok); letter-spacing:.03em}
.ga-live .ga-dot{width:6px; height:6px; border-radius:50%; background:var(--ok); box-shadow:0 0 0 3px var(--ok-tint)}
.ga-radar{position:relative; height:380px; display:grid; place-items:center; overflow:hidden}
.ga-radar svg{width:100%; height:100%}
.ga-ring{fill:none; stroke:var(--void-line); stroke-width:1}
.ga-blip circle{stroke:var(--void); stroke-width:2}
.ga-blip.ga-online circle{fill:var(--ok)}
.ga-blip.ga-offline circle{fill:var(--bad)}
.ga-blip.ga-warn circle{fill:var(--warn)}
.ga-blip.ga-suspended circle{fill:var(--nv-blue)}
.ga-radar-legend{position:absolute; left:20px; bottom:16px; display:flex; gap:16px; font-size:11px; color:var(--void-muted);
  z-index:2; flex-wrap:wrap}
.ga-radar-legend span{display:inline-flex; align-items:center; gap:6px}
.ga-radar-legend i{width:7px; height:7px; border-radius:50%; display:inline-block}
.ga-radar-legend .ga-online i{background:var(--ok)}
.ga-radar-legend .ga-warn i{background:var(--warn)}
.ga-radar-legend .ga-suspended i{background:var(--nv-blue)}
.ga-radar-legend .ga-offline i{background:var(--bad)}
.ga-attention{background:var(--paper); border:1px solid var(--line); border-radius:var(--r-lg); box-shadow:var(--shadow); overflow:hidden}
.ga-attention-head{padding:16px 20px; border-bottom:1px solid var(--line); display:flex; align-items:center; justify-content:space-between}
.ga-attention-head h3{font-size:14px}
.ga-count{font-family:var(--font-display); font-size:12px; font-weight:600; color:var(--bad); background:var(--bad-tint);
  padding:2px 9px; border-radius:999px}
.ga-sev-group{padding:10px 10px 4px}
.ga-sev-label{font-size:10.5px; font-weight:700; letter-spacing:.07em; text-transform:uppercase; color:var(--muted-2); padding:8px 10px 6px}
.ga-sev-label.ga-crit{color:var(--bad)}
.ga-a-row{display:flex; align-items:center; gap:11px; padding:9px 10px; border-radius:9px}
.ga-a-row:hover{background:var(--input-alt)}
.ga-a-row .ga-stripe{width:3px; align-self:stretch; border-radius:2px; background:var(--bad); flex:none}
.ga-a-row.ga-warn .ga-stripe{background:var(--warn)}
.ga-a-row .ga-am{min-width:0; flex:1; cursor:pointer}
.ga-a-row .ga-am b{font-size:12.8px; font-weight:600; display:block}
.ga-a-row .ga-am span{font-size:11.5px; color:var(--muted)}
.ga-a-row .ga-aw{font-size:11px; color:var(--muted-2); white-space:nowrap; font-family:var(--font-display)}

/* ---- services / kits: dense table toolbar + table ---- */
/* ⚠️ EDITED IN PLACE, MOBILE AUDIT ITEM 5 (2026-08-26) - see the matching
   banner near the end of this file for the full new rule set this pairs
   with (.ga-tb-filters/.ga-tb-view/.ga-tb-divider/.row-pair and the
   @container/@media rules). Was a single flex row of up to 8 direct
   children with its own align-items:center/flex-wrap; is now a two-row flex
   COLUMN whose two rows are its own new immediate children
   (.ga-tb-filters, .ga-tb-view) - align-items:center and flex-wrap moved
   onto those two rules instead, since this element no longer lays out
   controls directly. container-type:inline-size is new and is the only
   container-query root this file adds for this feature - every descendant
   query below (inside both new child rules) resolves against THIS
   container, not a second one. */
.ga-table-toolbar{display:flex; flex-direction:column; gap:12px; margin-bottom:16px; container-type:inline-size}
.ga-seg2{display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; background:var(--paper)}
.ga-seg2 button{height:34px; padding:0 14px; border:none; background:none; font-size:12.5px; font-weight:600; color:var(--muted)}
.ga-seg2 button.ga-on{background:var(--ink); color:#fff}
[data-theme="dark"] .ga-seg2 button.ga-on{background:var(--nv-blue)}
.ga-search{flex:1; min-width:200px; height:34px; padding:0 13px; border:1px solid var(--line-strong); border-radius:8px;
  background:var(--input-bg); font-size:13px; color:var(--ink); font-family:var(--font-body)}
.ga-table-wrap{background:var(--paper); border:1px solid var(--line); border-radius:var(--r-lg); box-shadow:var(--shadow);
  overflow-x:auto}
.ga-table{width:100%; min-width:560px; border-collapse:collapse}
.ga-table thead th{text-align:left; font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
  color:var(--muted); padding:12px 18px; border-bottom:1px solid var(--line); background:var(--input-alt)}
.ga-table tbody td{padding:13px 18px; border-bottom:1px solid var(--line); font-size:13px; vertical-align:middle}
.ga-table tbody tr:last-child td{border-bottom:none}
.ga-table tbody tr:hover{background:var(--input-alt); cursor:default}
.ga-id-cell{display:flex; align-items:center; gap:9px}
.ga-id-cell b{font-family:var(--font-display); font-weight:600; font-size:13px}
.ga-org-tag{font-size:11.5px; color:var(--muted)}
.ga-usage-cell{display:flex; align-items:center; gap:10px}
.ga-kit-model{font-size:11.5px; color:var(--muted)}

/* ---- Ground Station responsive (mirrors the prototype's own breakpoint) ---- */
@media (max-width:640px){ .ga-dash-top{grid-template-columns:1fr} }
@media (max-width:1080px){ .ga-ops-grid{grid-template-columns:1fr} }

/* ============================================================
   Ground Station round 2 (/dev-alpha/ only) - ADDITIVE ONLY
   Owner (of this block): LEO-01, review-concept track. See
   nexus-ux-tasks.git tasks/leo/2026-08-24-dev-alpha-redesign.md, round-2
   feedback (items 1&2, 3&4, 8). SEPARATE from the round-1 "Ground Station"
   banner directly above this one - that block is untouched, this is a
   second, later addition underneath it, per the same append-only rule.
   Every selector below is new; every new one is prefixed `ga-`, for the
   same reason the round-1 banner gives for its own rules: several bare
   names above (.pill, .card, .seg among them) are load-bearing elsewhere
   in this shared file and must never be coincidentally reused here.
   `.pill`, `.pill.info`, `.card`, `.seg`, `.row2`, `.meta`, `.svc`, `.sn`,
   `.sd` and `--rail-w` are all EXISTING, unchanged, reused as-is - see the
   round-2 task doc's own instruction not to invent ga-prefixed duplicates
   of something that already does the job.
   ============================================================ */

/* ---- collapsible rail (item 1&2) ---- */
/* Reuses the EXISTING --rail-w custom property (defined in the first :root
   block, top of this file) for the expanded width - no new width value
   anywhere in this banner, only a class toggle on markup already in
   Shell/app.js. */
.ga-app.ga-rail-expanded{grid-template-columns:var(--rail-w) 1fr}
.ga-rail.ga-rail-expanded .ga-rail-item{width:100%; height:auto; flex-direction:row; justify-content:flex-start;
  padding:11px 12px; gap:12px}
.ga-rail.ga-rail-expanded .ga-rail-item span{font-size:13px; font-weight:500; letter-spacing:normal; white-space:nowrap}
/* Chevron mirrors direction instead of a second, near-identical icon - see
   Shell's own note on reusing I.right rather than adding a left-pointing
   twin to the icon set. */
.ga-rail-toggle svg{transition:transform .15s ease}
.ga-rail-toggle.ga-on svg{transform:rotate(180deg)}

/* ⚠️ MOVED HERE FROM RIGHT AFTER @keyframes ga-rise, ROUND 4 MOBILE AUDIT
   FOLLOW-UP (2026-08-26) - real, reproduced bug, found independently while
   verifying a LATER, unrelated task (the toolbar restructure), not this
   drawer's own original build/verify pass, which is exactly why it was
   missed there: that pass proved the drawer OPENS, CLOSES, and STAYS
   OFF-SCREEN correctly (all true, still true) but never measured what
   .ga-main actually ends up WIDE ENOUGH to be at a real mobile viewport -
   the one thing the whole feature exists to deliver.

   The bug: .ga-app.ga-rail-expanded{grid-template-columns:var(--rail-w) 1fr}
   (below) is a TWO-class selector, specificity (0,2,0). This block's own
   .ga-app{grid-template-columns:1fr} override, sitting in its ORIGINAL spot
   right after @keyframes ga-rise, was a ONE-class selector, (0,1,0) -
   lower specificity, so it lost to the rule below UNCONDITIONALLY,
   regardless of viewport width or which one appeared later in the file. A
   media query gates WHETHER a rule applies; it adds nothing to specificity
   once it does. Since .ga-app ALWAYS carries both classes in the real
   markup (round 3 made ga-rail-expanded permanent, see Shell's own
   comment), the grid never actually collapsed to one column below 768px -
   confirmed live in a real browser at a real 390px viewport: .ga-main's
   own content column measured ~184px wide, not the ~390px it should have
   had once the rail left normal flow. The drawer's own open/close/off-
   screen behaviour was never wrong; the payoff (room for content) never
   actually arrived.

   Fixed with BOTH halves of the same coin, not just one: the selector
   below now matches .ga-app.ga-rail-expanded (equal specificity to the
   rule above it, not lower), AND this whole block now sits AFTER that
   rule in the file, so an equal-specificity tie also resolves the same
   way source order already does - correct for two independent reasons,
   not fragile on either one alone. */
@media (max-width:768px){
  .ga-app.ga-rail-expanded{grid-template-columns:1fr}
  /* ⚠️ z-index 45->1100 / 44->1099, lclarke 2026-09-03 (nexus-ux-tasks.git
     tasks/leo/2026-09-03-mobile-rail-zindex-and-padding.md) - real,
     reproduced bug: neither .card nor .leaflet-container (LeafletFleetMap,
     app.js) establishes its own CSS stacking context, so Leaflet's own
     internal z-index scale (.leaflet-top/.leaflet-bottom{z-index:1000},
     .leaflet-control{z-index:800}, leaflet.css) competed directly against
     this drawer's in the page's ROOT stacking context - and 1000/800 beat
     the old 45/44, painting the zoom control and map panes over the open
     rail and its backdrop. Confirmed live (real leaflet.js+css+theme.css,
     390px viewport) before and after. 1100/1099 clears every leaflet value
     with room to spare, and stays well under .modal-overlay's 2000 /
     .ga-ts-pop's 2001 (theme.css, above) - a modal opened while this drawer
     is also open still correctly renders on top of it, unchanged. */
  .ga-rail{position:fixed; left:0; top:0; z-index:1100; transform:translateX(-100%); transition:transform .25s; box-shadow:var(--shadow-lg)}
  .ga-rail.ga-rail-open{transform:none}
  .ga-rail-backdrop{display:block; position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:1099}
  .ga-burger-btn{display:inline-flex}
}

/* ---- "coming soon" placeholder pages (item 3&4) ---- */
.ga-page-head{display:flex; align-items:flex-start; justify-content:space-between; gap:16px; margin-bottom:20px}
.ga-page-head__title{font-family:var(--font-display); font-weight:600; font-size:1.5rem; color:var(--ink); margin:0 0 4px}
.ga-page-head__dek{font-family:var(--font-body); font-size:0.875rem; color:var(--muted); margin:0; max-width:56ch}
.ga-coming-soon{padding:28px 30px; max-width:none}
.ga-coming-soon__body{font-family:var(--font-body); font-size:0.9375rem; line-height:1.6; color:var(--muted); max-width:54ch; margin:0 0 24px}
.ga-coming-soon__stats{display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin:0 0 24px}
.ga-coming-soon__stat{background:var(--canvas); border:1px solid var(--line); border-radius:var(--r-sm); padding:14px 16px}
.ga-coming-soon__stat-label{font-family:var(--font-body); font-size:11px; font-weight:600; letter-spacing:.04em; text-transform:uppercase; color:var(--muted-2); margin-bottom:8px}
.ga-coming-soon__stat-value{font-family:var(--font-display); font-size:1.375rem; font-weight:600; color:var(--muted-2); line-height:1; margin-bottom:6px}
.ga-coming-soon__stat-hint{font-family:var(--font-body); font-size:12px; color:var(--muted-2)}
.ga-coming-soon__link{display:inline-flex; align-items:center; gap:6px; font-family:var(--font-body); font-size:0.875rem; font-weight:600; color:var(--nv-blue-700); text-decoration:none}
.ga-coming-soon__link:hover{color:var(--nv-blue-600); text-decoration:underline}
.ga-coming-soon__link:focus-visible{outline:2px solid var(--nv-blue); outline-offset:3px; border-radius:var(--r-sm)}
.ga-coming-soon__link span{transition:transform 0.15s ease}
.ga-coming-soon__link:hover span{transform:translateX(2px)}
@media (max-width:640px){ .ga-coming-soon{padding:20px; max-width:none} .ga-coming-soon__stats{grid-template-columns:1fr} .ga-page-head{flex-direction:column} }

/* ============================================================
   Ground Station polish round (/dev-alpha/ only) - ADDITIVE ONLY
   (lclarke, 2026-08-24, item 5). SEPARATE from the round-1/round-2 banners
   above - those blocks are untouched, this is a third, later addition
   underneath them, per the same append-only rule.

   Services table usage column (item 5): `UsageBar`'s own root element is a
   bare, unstyled `<div>` shared by both table mode (wrapped in the
   EXISTING `.ga-usage-cell`, added round 1) and card mode (a plain
   `margin-top:8px` wrapper with no class at all). This rule is scoped
   under `.ga-usage-cell` specifically so it can ONLY ever match the
   Services table's usage cell - card mode, and every other caller of
   `UsageBar` (LineDetail's own usage tab included), has no `.ga-usage-cell`
   ancestor and is untouched. `.ga-usage-cell` is itself already ga-prefixed,
   so this descendant selector needs no new class or ga- prefix of its own. */
.ga-usage-cell > div{width:300px; flex:none}

/* ---- Ground Station topbar padding (2026-08-24, lclarke direct feedback) - ADDITIVE ONLY ----
   "the words are hard up to the top of the screen and the bottom of the bar" - .ga-topbar
   went from min-height:60px/padding:10px to a FIXED height:66px/padding:0 earlier today
   (a deliberate, separate fix for a LineDetail scroll-stacking bug - that file's own
   hardcoded top:65px sticky offset needs the topbar's rendered height to be exactly
   66px, so growing this rule's total height is not an option). The three stacked lines
   (.ga-crumb + h1 + .field-note) plus zero padding left no visible breathing room.
   Restored a small 6px top/bottom padding (see the .ga-topbar rule itself, edited in
   place - not duplicated here) and tightened .field-note's own top margin specifically
   inside this context so the three lines plus the new padding still fit the same fixed
   66px - the shared .field-note rule used elsewhere in the app is untouched. */
.ga-topbar .field-note{margin-top:3px}

/* ---- Ground Station rail overflow fix (2026-08-24, lclarke direct feedback) - MODIFIES
   TWO EXISTING, DEV-ALPHA-EXCLUSIVE RULES (both ga- prefixed, confirmed absent from
   dev-src/live's own app.js - same safety class as the earlier .ga-rail-nav fix, not the
   "never touch a shared rule" class) ----
   "the profile area is expanding beyond the sidebar, it needs to be hard limited to the
   sidebar width." Root cause: .ga-rail and .ga-rail-foot both still had align-items:center
   (the same pattern already fixed once for .ga-rail-nav earlier today, just not extended
   to these two). The ported original-site footer markup (.rail-user/.rail-user-profile/
   .who, added earlier today) has no explicit width of its own - it relies on its ORIGINAL
   parent (.rail, live/dev) having no align-items override at all, which defaults to
   stretch. With .ga-rail/.ga-rail-foot centering instead of stretching, that ported markup
   shrink-wrapped to its own unconstrained content width and could grow past the sidebar's
   actual 248px instead of being clipped by it - which is also why the ALREADY-PRESENT
   ellipsis rules on .who b/.who span (shared, unchanged, live on /dev/ today) never
   visibly took effect: text-overflow:ellipsis only fires on a width-CONSTRAINED box, and
   this one was never actually constrained. Changed both to align-items:stretch, matching
   the original .rail's real (default, unset) behavior - the .rail-logo, .ga-rail-item, and
   Nexus-link elements inside either container already self-center or explicitly set their
   own width:100% (checked directly), so this is not expected to visibly change anything
   about them, only to finally constrain the one element that needed it. Added matching
   14px side padding on .ga-rail-foot (mirroring .ga-rail-nav's own existing 14px) so the
   now-full-width footer content lines up with the nav items above it, not flush to the
   rail's outer edge. */


/* ============================================================
   Ground Station round 4 (/dev-alpha/ only) - ADDITIVE ONLY
   Owner (of this block): LEO-01, review-concept track. See
   nexus-ux-tasks.git tasks/leo/2026-08-24-dev-alpha-redesign.md, round-4
   feedback (item 6 - Coming Soon page). SEPARATE from the round-1/round-2/
   polish-round banners above - those blocks are untouched, this is a
   fourth, later addition underneath them, per the same append-only rule.

   "a little fancier, but still minimalist, just text saying coming soon"
   (lclarke). Content stays exactly what round 3 stripped it down to - the
   "{title} Coming Soon" heading, nothing else - this is PRESENTATION only,
   see app.js's own ComingSoon() comment for the content-side half of this.
   Direction given, followed rather than reinvented: reuse the Dashboard's
   own dark "instrument panel" language (`.ga-ground` above - the same
   --void/--void-line/--void-ink tokens, deliberately fixed-dark in both
   page themes, see the round-1 :root block's own comment above; no
   [data-theme="dark"] override is added here either, for the same reason)
   and the same corner radial-glow technique `.ga-hero-stat::after` above
   already uses (a content:"" pseudo-element off one corner, a radial-
   gradient using --nv-blue-tint, fading to transparent). No new colour
   value anywhere in this block - every value below is an existing token or
   a plain layout number.

   `.ga-coming-soon` is the EXISTING outer-container class from round 2
   (its own `padding`/`max-width` rule, above, untouched) - this is a
   SECOND rule for that same selector, adding only properties the first one
   never set (background/border/radius/shadow/layout), the same technique
   the round-1 banner's own second `:root` block already established for
   "ADDS ... that do not already exist above". `.ga-coming-soon__title` is
   new. Neither collides with anything: `.ga-coming-soon__title` was not
   previously defined, and every property added to `.ga-coming-soon` here
   is one its round-2 rule never touched. */
.ga-coming-soon{background:var(--void); border:1px solid var(--void-line); border-radius:var(--r-lg);
  box-shadow:var(--shadow-lg); position:relative; overflow:hidden;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  min-height:calc(100svh - 126px); text-align:center}
.ga-coming-soon::after{content:""; position:absolute; right:-30px; top:-30px; width:160px; height:160px;
  border-radius:50%; background:radial-gradient(circle, var(--nv-blue-tint) 0%, transparent 70%);
  opacity:.7; pointer-events:none}
.ga-coming-soon__title{font-family:var(--font-display); font-weight:700; font-size:2rem; letter-spacing:.02em;
  color:var(--void-ink); position:relative; margin:0}
/* ⚠️ ROUND 5 (lclarke, 2026-09-02) - CTA LINE + VIEWPORT CENTERING, item 2.
   `.ga-coming-soon__cta` is NEW: same display family as `__title`, a lighter
   weight/size (it is a subhead, not competing with the heading) and
   `position:relative` so it sits ABOVE the `::after` glow the same way
   `__title` already does (both need it - anything without it renders
   BELOW the glow, since the glow is the only other positioned descendant
   and stacking context follows document order once `z-index` is not
   involved). `margin-top` for spacing from the heading now that
   `flex-direction:column` (below) stacks them instead of placing them
   side-by-side (`.ga-coming-soon` never had a second child before this
   round, so its default row direction was never visible as a bug).
   `min-height` REPLACES the fixed 240px/180px card heights above with a
   viewport-relative one: `.topbar{height:66px}` (line ~247) plus
   `.content{padding:30px}` (line ~251, box-sizing:border-box means that
   30px is on EACH side, so 60px total vertical) = 126px of chrome outside
   this block on every page that mounts it - `100svh` (not `100vh`, so
   mobile browser address-bar chrome cannot push the block off-screen)
   minus that 126px puts `.ga-coming-soon` at the full height of the
   visible content area below the header, so its own `align-items:center;
   justify-content:center` genuinely centers in the user's visible screen
   area rather than a fixed 240px card. */
.ga-coming-soon__cta{font-family:var(--font-display); font-weight:500; font-size:1.05rem;
  color:var(--void-ink); position:relative; margin:10px 0 0}
@media (max-width:640px){ .ga-coming-soon__title{font-size:1.5rem} .ga-coming-soon__cta{font-size:.95rem} }

/* ============================================================
   Ground Station round 5 (/dev-alpha/ + shared toolbar) - MOBILE
   FRIENDLINESS AUDIT, ITEM 5 (2026-08-26). ADDITIVE ONLY, pairs with the
   .ga-table-toolbar rule edited IN PLACE further up this file (own comment
   there explains that edit) - every rule below this banner is new.

   MECHANISM: .ga-table-toolbar (edited above) is the one container-query
   root - container-type:inline-size lives there, nowhere else, so every
   @container rule below (regardless of which of its two new child rows the
   selector targets) resolves against that same container. A plain @media
   fallback sits beside every @container rule at the identical pixel value,
   per the task's own instruction - defensive only, for a context where
   this toolbar's container width might not vary independently of the
   viewport; @container is the real, primary mechanism, used deliberately
   instead of the existing matchMedia/useIsSmallScreen() hook (app.js) since
   that hook answers a different question (viewport width, for the
   unrelated Cards-enforcement feature) and this one needs the toolbar's
   own available width.

   TWO THRESHOLDS, for two different reasons:
     - 868px (mid) - the toolbar's rendered box is a constant 312px
       narrower than the viewport in this app's real layout (248px
       expanded rail + 32px+32px .ga-content padding), confirmed directly
       across real viewport widths, so the container width at the viewport
       real breakage was confirmed (1150px) is 1150-312 = 838px. 868px adds
       ~30px of headroom ABOVE that confirmed-broken number - triggering
       the degrade a little earlier than strictly necessary, which is the
       safer of the two rounding directions available, given the
       alternative is shipping the exact width already proven broken.
       Re-confirmed directly with a real browser at this value (see
       BUILD-NOTES) before this rule was finalised, not derived on paper
       alone.
     - 390px (narrow) - given directly by the task's own brief, not derived
       here; a representative narrow-phone container width, and the point
       past which a single row genuinely has nowhere left to shrink and
       needs to become several full-width rows instead. Also re-confirmed
       directly.

   Nothing below needs a [data-theme="dark"] counterpart - every value used
   is layout only (display/flex/grid/order/width) except the one new
   colour reference (.ga-tb-divider, below), which reuses the EXISTING
   --line-strong custom property (already theme-aware in the first :root
   block, top of this file) rather than a new value - checked directly
   rather than assumed, per the task's own instruction.

   The Usage/Condition seg-vs-select swap (868px block) and both .row-pair
   wrappers apply ONLY to ServiceLines' own toolbar in practice - the
   selectors below simply match nothing on KitList's smaller toolbar, which
   has no .ga-tb-usage-seg/.ga-tb-cond-seg/.ga-tb-pair-usage/etc anywhere in
   its own markup (see KitList's own app.js comment for why it needs no mid-
   breakpoint behaviour of its own). One shared stylesheet correctly serving
   two different-sized toolbars this way needs no KitList-specific guard -
   there is nothing here that CAN match its markup.
   ============================================================ */
.ga-tb-filters{display:flex; align-items:center; gap:12px; flex-wrap:wrap}
.ga-tb-view{display:flex; align-items:center; gap:12px; flex-wrap:wrap}
/* Vertical rule between Group A (state filters) and Group B (account/
   search) at every width down to 390px, where it is hidden outright (a
   stacked, one-control-per-row layout has nothing left to divide). */
.ga-tb-divider{width:1px; align-self:stretch; background:var(--line-strong); flex:none}
/* .row-pair - display:contents by default, so its two/four children behave
   as ordinary flex items of .ga-tb-filters at every width down to the
   narrow breakpoint (this wrapper is invisible to those two widths' own
   layout) - and becomes a real 2-column grid only at 390px, the one width
   that actually needs two controls sharing a row. Name/shape are exactly
   what the task's own brief suggested; checked this name did not already
   exist in this file before adding it. */
.row-pair{display:contents}
/* Usage/Condition's <select> variants start hidden - the 868px rule below
   is the only thing that ever shows one, always alongside hiding its .seg
   sibling. display:inline-block matches a bare <select>'s own real default,
   restored explicitly only where something needs un-hiding TO it. */
.ga-tb-usage-sel, .ga-tb-cond-sel{display:none}
/* The narrow-only duplicate Table/Cards toggle (ServiceLines only - see its
   own app.js comment) starts hidden the same way. display:contents when
   shown (390px block, below) rather than display:flex/block, so its own
   child - the real <${Seg}>'s own ".seg" box, already display:flex - is
   what actually occupies the .row-pair grid cell, not an extra wrapping
   box with no styling purpose of its own. */
.ga-tb-toggle-dup{display:none}
.ga-tb-filters .ga-search{max-width:340px}

@container (max-width:868px){
  .ga-tb-usage-seg, .ga-tb-cond-seg{display:none}
  .ga-tb-usage-sel, .ga-tb-cond-sel{display:inline-block}
  .ga-tb-filters .ga-search{min-width:180px}
}
@media (max-width:868px){
  .ga-tb-usage-seg, .ga-tb-cond-seg{display:none}
  .ga-tb-usage-sel, .ga-tb-cond-sel{display:inline-block}
  .ga-tb-filters .ga-search{min-width:180px}
}

@container (max-width:390px){
  .ga-tb-filters{flex-direction:column; align-items:stretch}
  /* Status only - the direct-child combinator deliberately excludes the
     (hidden, at this width) Usage/Condition .seg variants nested one level
     deeper inside .row-pair, which need none of this. */
  .ga-tb-filters > .ga-seg2{width:100%}
  /* min-width:0 overrides a flex item's own default min-width:auto, which
     otherwise refuses to shrink a button below its own text's intrinsic
     width and pushes .ga-seg2 wider than its 100% parent instead - the
     classic flexbox overflow gotcha, not a browser bug. white-space/
     overflow/text-overflow keep "Suspended" (the longest label) a clean
     ellipsis rather than a wrapped second line or a visible spill past the
     button's own edge if a container ever ends up narrower than this
     breakpoint's own real-world target (confirmed happening TODAY, for an
     unrelated, pre-existing reason - .ga-app's own grid-template-columns
     stays var(--rail-w) 1fr, not the mobile drawer's 1fr, below the 768px
     viewport breakpoint, because .ga-app.ga-rail-expanded's two-class
     selector outweighs the drawer rule's plain one-class selector; flagged,
     not fixed, here - out of THIS task's own scope - but its real effect is
     a toolbar container narrower than 390px on an actual phone today, so
     this rule earns its keep regardless of when or whether that gets its
     own fix). */
  .ga-tb-filters > .ga-seg2 button{flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
  .ga-tb-divider{display:none}
  /* minmax(0,1fr), NOT bare 1fr - the same shrink-below-content-size fix as
     .ga-seg2 button's own min-width:0 above, applied to a grid track this
     time: a bare 1fr column still refuses to size below its own content's
     min-content width (a <select> showing "Ending soon" or similar), which
     pushed this row past its own 100%-width parent by the same real amount
     .ga-seg2 did - confirmed by measuring both before adding either fix,
     not assumed. */
  .row-pair{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:8px}
  .ga-tb-filters .ga-search{width:100%; max-width:none; min-width:0; order:2}
  .ga-tb-pair-usage{order:3}
  .ga-tb-pair-acct{order:4}
  .ga-tb-toggle-dup{display:contents}
  /* ServiceLines-only (see .ga-tb-c-restack's own app.js comment) - its
     toggle hides here because a second instance (immediately above, inside
     .ga-tb-filters' own Account row-pair) takes its place; KitList's own
     .ga-tb-view carries no .ga-tb-c-restack class, so neither rule below
     ever matches it and its Group C stays exactly as it already was, at
     every width, per its own app.js comment. */
  .ga-tb-view.ga-tb-c-restack{flex-direction:column; align-items:stretch}
  .ga-tb-view.ga-tb-c-restack .ga-tb-toggle-primary{display:none}
}
@media (max-width:390px){
  /* Mirrors the @container block above line for line, including its
     min-width:0/minmax(0,1fr) overflow fixes - see that block's own
     comments for why each one is there. */
  .ga-tb-filters{flex-direction:column; align-items:stretch}
  .ga-tb-filters > .ga-seg2{width:100%}
  .ga-tb-filters > .ga-seg2 button{flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
  .ga-tb-divider{display:none}
  .row-pair{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:8px}
  .ga-tb-filters .ga-search{width:100%; max-width:none; min-width:0; order:2}
  .ga-tb-pair-usage{order:3}
  .ga-tb-pair-acct{order:4}
  .ga-tb-toggle-dup{display:contents}
  .ga-tb-view.ga-tb-c-restack{flex-direction:column; align-items:stretch}
  .ga-tb-view.ga-tb-c-restack .ga-tb-toggle-primary{display:none}
}

/* ============================================================
   Ground Station round 6 (/dev-alpha/ + shared toolbar) - STATUS
   SELECT-DEGRADE REVERSAL (lclarke, 2026-08-26). ADDITIVE ONLY, pairs with
   round 5's own banner directly above (unmodified) and with the matching
   app.js comments on ServiceLines' and KitList's own toolbars, which carry
   the full reasoning for the reversal - not reproduced here.

   Round 5 gave Usage/Condition (ServiceLines) a select degrade at the
   same two thresholds and deliberately left Status (both ServiceLines'
   LINE_FILTERS and KitList's own KIT_FILTERS) as a full .ga-seg2 at every
   width, "the single highest-frequency filter here" - lclarke's own
   later, explicit instruction reverses exactly that call, so Status now
   gets the identical treatment, in both places. Separate rules from round
   5's own .ga-tb-usage-seg/.ga-tb-usage-sel etc. below, not edits to
   them - this file's own additive convention - and the two sets are
   byte-identical in shape by construction (both toggle the same display
   values at the same thresholds for a seg/select pair), not copy-paste
   drift.

   .ga-tb-line-seg/.ga-tb-line-sel are ServiceLines' own Status pair
   (app.js names its Status state `lineF`, after LINE_FILTERS - "line" is
   this app's own name for what the UI itself calls Status, same as
   everywhere else in this file); .ga-tb-kit-seg/.ga-tb-kit-sel are
   KitList's, likewise named after KIT_FILTERS. Both selects are
   ThemedSelect instances (app.js), not <select> elements - see the
   .ga-ts-* rules near .pager-select, above, for that component's own
   styling and why it exists. Neither pair is wrapped in a .row-pair:
   unlike Usage+Condition, Status has no partner control of its own to
   share a grid cell with at the narrowest breakpoint, so there is nothing
   here for the existing .row-pair machinery to do - it stacks as an
   ordinary flex item instead, same as Account's own <select> already
   does at every width above the narrowest breakpoint.

   KitList's own round-5 comment (app.js) claimed the shared 868px rule
   below "simply matches nothing here" and that this was the correct
   outcome, not a gap - true when written, no longer true now that
   KitList gets its own .ga-tb-kit-seg/.ga-tb-kit-sel pair below; KitList's
   own app.js comment is updated to match, not left to silently disagree
   with this file. Nothing here needs a third breakpoint - both new pairs
   reuse the existing 868px/390px thresholds unchanged.
   ============================================================ */
.ga-tb-line-sel, .ga-tb-kit-sel{display:none}

@container (max-width:868px){
  .ga-tb-line-seg, .ga-tb-kit-seg{display:none}
  .ga-tb-line-sel, .ga-tb-kit-sel{display:inline-block}
}
@media (max-width:868px){
  .ga-tb-line-seg, .ga-tb-kit-seg{display:none}
  .ga-tb-line-sel, .ga-tb-kit-sel{display:inline-block}
}

/* Neither new select has a partner to grid-pair with (see banner above),
   so - unlike .ga-tb-usage-sel/.ga-tb-cond-sel, which get their full
   width from the existing .row-pair grid at this same breakpoint - each
   needs its own explicit full-width rule here, the same treatment
   .ga-tb-filters' own direct-child .ga-seg2 rule (above) already gives
   the segmented variant these replace at this width. */
@container (max-width:390px){
  .ga-tb-line-sel, .ga-tb-kit-sel{width:100%}
}
@media (max-width:390px){
  .ga-tb-line-sel, .ga-tb-kit-sel{width:100%}
}

/* ---- Plan Mask card "Plan Mask Usage" bar, static width (2026-09-03, lclarke direct feedback) ----
   "Usage bar need to be like the service list and be set to a static 300px long." Same fixed-width
   idea as .ga-usage-cell > div (the Services table's own usage cell, see that rule's own comment
   above) - but deliberately NOT that class: .ga-usage-cell's own comment states it is scoped "so it
   can ONLY ever match the Services table's usage cell" and every other UsageBar caller is untouched
   by design - reusing it here would silently break that documented invariant. A new, narrowly-scoped
   class instead, applied directly to the wrapper (UsageBar's own root <div> has no width of its own,
   so this needs no ">div" descendant trick). Responsive at the SAME 560px breakpoint .row2 itself
   collapses at (this bar lives inside a .row2 column) - the Services list's own fixed-300px behaviour
   is ALSO desktop/table-only (its mobile "card mode" wrapper carries no width rule at all, per
   .ga-usage-cell's own comment), so dropping the fixed width below that breakpoint here matches the
   same underlying design choice already made elsewhere, not a new one. */
.planmask-usagebar{width:300px}
@media (max-width:560px){ .planmask-usagebar{width:100%} }
