/* =========================================================================
   YBM-ITS design tokens - the control-room design language shared by every
   app UI and the website. Dark-first; light arrives via prefers-color-scheme
   AND an explicit [data-theme] override (the override always wins).

   HARD RULES
   - The interactive accent is YBM orange (--accent family). It marks
     interaction and brand, never state.
   - Red / amber / green (--sig-red / --sig-amber / --sig-green) are RESERVED
     for real signal and alarm state: an actual alarm, an actual warning, an
     actual healthy/running reading. Never decorate the interface with them.
   - Fully self-contained: system font stacks only, no external assets.
   ========================================================================= */

:root {
  /* surfaces: page background -> raised card -> highest inset */
  --bg: #0a0d12; --bg-2: #070a0e;
  --surface: #131922; --surface-2: #1a212c; --surface-3: #212a37;
  --border: #232b36; --border-hi: #303b49;

  /* ink scale: primary text -> muted -> dim */
  --ink: #e8eef5; --ink-mut: #9aa7b4; --ink-dim: #636e7b;

  /* YBM orange interactive accent (dark theme values) */
  --accent: #e8760f; --accent-hi: #f68b2e; --accent-lo: #b45a08;
  --accent-ink: #1f1002;
  --glow: rgba(232, 118, 15, .15);

  /* RESERVED signal colours - real signal & alarm state ONLY */
  --sig-green: #2fbf57; --sig-amber: #f5a623; --sig-red: #f0483e;
  --sig-green-ink: #04121a; --sig-amber-ink: #04121a; --sig-red-ink: #ffffff;

  /* non-signal status accents (maturity/beta chrome etc.) */
  --m-beta: #8f7ff0;

  /* Derived from the ACTIVE theme's accent/border, so focus rings, nav pills and
     table hairlines follow the palette instead of fighting it. Defined once on
     :root - var() substitution re-resolves them under every [data-theme] block.

     WHY once, and only here: a custom property's var() references are resolved
     at computed-value time on the element that DECLARES it. Every theme block
     below re-declares --accent/--border on this same element (:root), so these
     four recompute for free and every descendant inherits the themed result.
     Repeating them inside each theme block would be four more places to forget
     when a fifth theme lands - the half-adoption in app.css we are correcting. */
  --ring:          color-mix(in srgb, var(--accent) 30%, transparent);
  --accent-soft:   color-mix(in srgb, var(--accent) 12%, transparent);
  --accent-line:   color-mix(in srgb, var(--accent) 45%, var(--border));
  --border-subtle: color-mix(in srgb, var(--border) 55%, transparent);

  /* geometry */
  --radius-sm: 9px; --radius: 14px; --radius-lg: 20px;
  /* Fixed white, in BOTH themes, for the one surface that must not be themed: a
     QR's quiet zone. Scanners need dark-on-light; inverting it in dark mode makes
     enrolment fail exactly when an operator is most likely to be doing it. */
  --qr-plate: #ffffff;
  --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 12px 34px rgba(0, 0, 0, .28);

  /* Elevation ladder. The existing single --shadow stays (21 crates may use it);
     these three are the console's card / overlay / detached steps. Dark values -
     a dark surface needs more than a 1px line to lift off a dark background.
     NOT derived via color-mix: a box-shadow is a list of lengths plus a colour,
     not a colour, so there is nothing for var() to re-resolve. The light theme
     therefore overrides all three literally, in both light blocks below. */
  --shadow-sm: 0 1px 2px rgba(0,0,0,.30);
  --shadow-md: 0 4px 14px rgba(0,0,0,.34);
  --shadow-lg: 0 10px 28px rgba(0,0,0,.45);

  /* fluid type scale: phone 360px -> control-room wall. Steps keep working
     as the viewport keeps growing; no ceiling below wall width. */
  --fs-0: clamp(12.5px, 0.55vw + 10.5px, 20px);   /* fine print, chips   */
  --fs-1: clamp(14.5px, 0.6vw + 12.4px, 23px);    /* body                */
  --fs-2: clamp(16.5px, 0.8vw + 13.6px, 28px);    /* emphasized body     */
  --fs-3: clamp(19px, 1.2vw + 14.7px, 38px);      /* card / tile titles  */
  --fs-4: clamp(24px, 2vw + 16.8px, 56px);        /* page titles         */
  --fs-num: clamp(34px, 5.2vw + 15.3px, 148px);   /* big status numerals */

  /* fluid spacing */
  --sp-1: clamp(4px, .3vw + 2.9px, 10px);
  --sp-2: clamp(8px, .45vw + 6.4px, 17px);
  --sp-3: clamp(12px, .7vw + 9.5px, 26px);
  --sp-4: clamp(18px, 1.1vw + 14px, 40px);

  --font-ui: -apple-system, system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;

  color-scheme: dark light;
}

/* explicit light override - always wins over the media query */
:root[data-theme="light"], html[data-theme="light"] {
  --bg: #eef1f5; --bg-2: #e4e9ef;
  --surface: #ffffff; --surface-2: #f5f8fb; --surface-3: #eaf0f6;
  --border: #d6dce4; --border-hi: #c3ccd7;
  --ink: #16212c; --ink-mut: #53616e; --ink-dim: #8592a0;
  --accent: #c25f04; --accent-hi: #a85203; --accent-lo: #c25f04;
  --accent-ink: #ffffff;
  --glow: rgba(194, 95, 4, .12);
  --m-beta: #5f4dd0;
  --shadow: 0 1px 2px rgba(20, 40, 60, .06), 0 12px 30px rgba(20, 40, 60, .10);
  /* Light elevation ladder. Dark-tuned shadows read as a grey smear on white,
     so the ladder is re-stated rather than inherited: cooler, softer, and
     two-layer at the small and large steps so the edge stays crisp.
     --ring/--accent-soft/--accent-line/--border-subtle are deliberately NOT
     repeated here - they are color-mix() over --accent/--border, which this
     block has already re-declared on this same element, so they re-resolve on
     their own. Restating them would create a second copy to drift. */
  --shadow-sm: 0 1px 2px rgba(20,40,60,.06), 0 1px 3px rgba(20,40,60,.08);
  --shadow-md: 0 4px 14px rgba(20,40,60,.09);
  --shadow-lg: 0 10px 28px rgba(20,40,60,.13), 0 2px 6px rgba(20,40,60,.07);
}

/* OS light preference, unless the app explicitly pinned dark */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --bg: #eef1f5; --bg-2: #e4e9ef;
    --surface: #ffffff; --surface-2: #f5f8fb; --surface-3: #eaf0f6;
    --border: #d6dce4; --border-hi: #c3ccd7;
    --ink: #16212c; --ink-mut: #53616e; --ink-dim: #8592a0;
    --accent: #c25f04; --accent-hi: #a85203; --accent-lo: #c25f04;
    --accent-ink: #ffffff;
    --glow: rgba(194, 95, 4, .12);
    --m-beta: #5f4dd0;
    --shadow: 0 1px 2px rgba(20, 40, 60, .06), 0 12px 30px rgba(20, 40, 60, .10);
    /* Same light ladder as the explicit override above, and it has to be spelled
       out again: this block is a DIFFERENT selector, and CSS has no way for one
       ruleset to include another. Keep the two in step - a divergence here shows
       up only for users who never touched the theme toggle. */
    --shadow-sm: 0 1px 2px rgba(20,40,60,.06), 0 1px 3px rgba(20,40,60,.08);
    --shadow-md: 0 4px 14px rgba(20,40,60,.09);
    --shadow-lg: 0 10px 28px rgba(20,40,60,.13), 0 2px 6px rgba(20,40,60,.07);
  }
}

/* ---- Base ---------------------------------------------------------------- */
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
  background: var(--bg); color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--fs-1); line-height: 1.55;
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hi); }
h1, h2, h3, h4 { line-height: 1.15; letter-spacing: -.012em; margin: 0 0 var(--sp-2); }
h1 { font-size: var(--fs-4); }
h2 { font-size: var(--fs-3); }
h3 { font-size: var(--fs-2); }
code, kbd, pre, .mono { font-family: var(--font-mono); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* =========================================================================
   YBM-ITS app chrome - the shared skin every ported app UI wears on top of
   tokens.css. One stylesheet, one responsive range: a 360px phone held in a
   paddock up to a control-room multi-monitor wall, with no breakpoint cliff.
   Everything fluid (clamp) so tiles and numerals keep growing with the glass
   and stay legible at distance.

   Class map (the contract app agents build against):
     .app-topbar  > .brand .site .chips .ver     sticky status bar
     .chip        + .ok .warn .alarm .off        live state chips (signal colours = real state ONLY)
     .dot         + .ok .warn .alarm .off        alarm severity dots
     .app-main                                   fluid-width page body
     .tiles > .tile                              big status tile grid
       .tile-label .tile-value .tile-unit        label / big numeral / unit
       .tile.ok .warn .alarm .stale              real state; stale = input lost, fails visibly OFF
     .cards > .card                              detail card grid
     .btn  .btn-primary .btn-ghost .btn-stop     buttons (.btn-stop = real machine stop only)
     .btn.armed / .confirm-hint                  two-tap arm -> confirm control pattern
     .field > label + input/select/textarea      forms
   ========================================================================= */

/* ---- App frame ----------------------------------------------------------- */
.app { min-height: 100vh; display: flex; flex-direction: column; }
.app-main {
  flex: 1; width: 100%;
  padding: var(--sp-3) clamp(12px, 2vw, 48px) var(--sp-4);
}

/* ---- Top status bar ------------------------------------------------------ */
/* app name + site + live chips + build version. Sticky so state is always
   on-glass; wraps to two rows on phones instead of truncating chips. */
.app-topbar {
  position: sticky; top: 0; z-index: 50;
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-2);
  padding: var(--sp-1) clamp(12px, 2vw, 48px);
  min-height: clamp(52px, 4.4vw, 92px);
  background: color-mix(in srgb, var(--surface-2) 86%, transparent);
  backdrop-filter: saturate(1.4) blur(10px);
  border-bottom: 1px solid var(--border);
}
.app-topbar .brand {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  font-size: var(--fs-2); font-weight: 700; color: var(--ink); flex: none;
}
.app-topbar .brand img,
.app-topbar .brand .mark {
  width: clamp(24px, 1.8vw, 44px); height: clamp(24px, 1.8vw, 44px);
  object-fit: contain; flex: none;
}
.app-topbar .brand i { color: var(--accent); font-style: normal; }
.app-topbar .site {
  color: var(--ink-mut); font-size: var(--fs-0); font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.app-topbar .spacer { margin-left: auto; }
.app-topbar .chips { display: inline-flex; align-items: center; flex-wrap: wrap; gap: var(--sp-1); }
.app-topbar .ver {
  flex: none; font-family: var(--font-mono);
  font-size: calc(var(--fs-0) * .85); font-weight: 600; color: var(--ink-dim);
  border: 1px solid var(--border); border-radius: 999px; padding: 2px 8px;
  white-space: nowrap; cursor: default;
}

/* ---- Live state chips ---------------------------------------------------- */
/* Neutral by default. The signal colours appear ONLY when the chip reports a
   real state: green = confirmed healthy/online, amber = degraded/warning,
   red = active alarm, dim = link lost / input stale (fails visibly OFF). */
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-0); font-weight: 700; white-space: nowrap;
  color: var(--ink-mut); background: var(--surface-3);
  border: 1px solid var(--border-hi); border-radius: 999px;
  padding: 3px clamp(9px, .7vw, 16px);
}
.chip::before {
  content: ""; width: .55em; height: .55em; border-radius: 50%;
  background: currentColor; flex: none;
}
/* `display` above outranks the browser's own [hidden] rule, so a chip a page hides
   (the fuel kiosk's user chip, the IO gateway's MQTT chip) still drew as an empty pill. */
.chip[hidden] { display: none; }
.chip.ok    { color: var(--sig-green-ink); background: var(--sig-green); border-color: var(--sig-green); }
.chip.warn  { color: var(--sig-amber-ink); background: var(--sig-amber); border-color: var(--sig-amber); }
.chip.alarm { color: var(--sig-red-ink);   background: var(--sig-red);   border-color: var(--sig-red);
              animation: alarmblink 1s step-end infinite; }
.chip.off   { color: var(--ink-dim); background: transparent; border-style: dashed; }
@keyframes alarmblink { 50% { filter: brightness(.72); } }

/* ---- Alarm severity dots ------------------------------------------------- */
/* Bare state markers for lists and tables. Same reservation rule as chips. */
.dot {
  display: inline-block; vertical-align: middle; flex: none;
  width: clamp(10px, .7vw, 18px); height: clamp(10px, .7vw, 18px);
  border-radius: 50%; background: var(--ink-dim);
}
.dot.ok    { background: var(--sig-green); }
.dot.warn  { background: var(--sig-amber); }
.dot.alarm { background: var(--sig-red); box-shadow: 0 0 0 3px color-mix(in srgb, var(--sig-red) 30%, transparent);
             animation: alarmblink 1s step-end infinite; }
.dot.off   { background: transparent; border: 2px dashed var(--ink-dim); }

/* ---- Big status tiles ---------------------------------------------------- */
/* The at-distance readout. Column count grows with the glass: one column on
   a 360px phone, filling in continuously up to a video wall. */
.tiles {
  display: grid; gap: var(--sp-2);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, clamp(164px, 15vw, 380px)), 1fr));
}
.tile {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--sp-2) var(--sp-3);
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
}
.tile-label {
  color: var(--ink-mut); font-size: var(--fs-0); font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase;
  display: flex; align-items: center; gap: 8px;
}
.tile-value {
  font-size: var(--fs-num); font-weight: 800; line-height: 1.04;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums;
  color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tile-unit { font-size: .38em; font-weight: 600; color: var(--ink-mut); letter-spacing: 0; }
.tile-sub { color: var(--ink-dim); font-size: var(--fs-0); }
/* real state only - a coloured edge readable across the room */
.tile.ok    { border-color: color-mix(in srgb, var(--sig-green) 55%, var(--border)); box-shadow: inset 4px 0 0 var(--sig-green); }
.tile.warn  { border-color: color-mix(in srgb, var(--sig-amber) 55%, var(--border)); box-shadow: inset 4px 0 0 var(--sig-amber); }
.tile.alarm { border-color: var(--sig-red); box-shadow: inset 4px 0 0 var(--sig-red);
              animation: alarmblink 1s step-end infinite; }
.tile.alarm .tile-value { color: var(--sig-red); }
/* stale input: the reading can no longer be trusted - dim it, never fake it */
.tile.stale { border-style: dashed; }
.tile.stale .tile-value { color: var(--ink-dim); }
.tile.stale .tile-label::after { content: "STALE"; color: var(--ink-dim); font-size: .85em; letter-spacing: .12em; }

/* ---- Card grid ----------------------------------------------------------- */
.cards {
  display: grid; gap: var(--sp-2);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, clamp(280px, 24vw, 520px)), 1fr));
}
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--sp-3); min-width: 0;
}
.card h3 { display: flex; align-items: center; gap: 9px; margin: 0 0 var(--sp-1); }
.card .blurb { color: var(--ink-mut); margin: 0 0 var(--sp-2); }

/* ---- Buttons ------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font: inherit; font-size: var(--fs-1); font-weight: 700;
  padding: clamp(10px, .8vw, 18px) clamp(16px, 1.3vw, 30px);
  border-radius: 11px; border: 1px solid transparent; cursor: pointer;
  min-height: 44px; /* touch target floor on phones */
  transition: background .15s, border-color .15s, transform .05s;
}
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.btn-primary:hover { background: var(--accent-hi); color: var(--accent-ink); }
.btn-ghost { color: var(--ink-mut); border-color: var(--border); background: var(--surface); }
.btn-ghost:hover { color: var(--ink); border-color: var(--accent); }
/* .btn-stop: red is a signal colour - use ONLY for a real machine-stop /
   alarm-acknowledge action, never for cancel/close/delete-a-row. */
.btn-stop { background: var(--sig-red); color: var(--sig-red-ink); border-color: var(--sig-red); }
.btn-stop:hover { filter: brightness(1.08); }
.btn.big { font-size: var(--fs-2); padding: clamp(14px, 1.1vw, 24px) clamp(22px, 1.8vw, 40px); }

/* ---- Two-tap arm / confirm ----------------------------------------------- */
/* Every consequential control is two taps: tap 1 arms (orange pulse + hint),
   tap 2 within the app's arm window fires; the app disarms on timeout.
   Arming is an INTERACTION state, so it wears the accent - not a signal
   colour. Apply .armed to the control, un-hide its .confirm-hint. */
.btn.armed, .armed {
  background: var(--accent) !important; color: var(--accent-ink) !important;
  border-color: var(--accent) !important;
  animation: armpulse .7s infinite;
}
@keyframes armpulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 70%, transparent); }
  50%      { box-shadow: 0 0 0 8px transparent; }
}
.confirm-hint {
  color: var(--accent); font-size: var(--fs-0); font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
}
.confirm-hint[hidden] { display: none; }

/* ---- Forms --------------------------------------------------------------- */
.field { display: flex; flex-direction: column; gap: 6px; margin: 0 0 var(--sp-2); min-width: 0; }
.field > label {
  color: var(--ink-mut); font-size: var(--fs-0); font-weight: 700;
  letter-spacing: .07em; text-transform: uppercase;
}
input, select, textarea {
  font: inherit; color: var(--ink); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 10px;
  padding: clamp(10px, .8vw, 16px) clamp(12px, 1vw, 20px);
  width: 100%; min-height: 44px;
}
input::placeholder, textarea::placeholder { color: var(--ink-dim); }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
input[type="checkbox"], input[type="radio"] {
  width: 22px; height: 22px; min-height: 0; accent-color: var(--accent);
}
.field .help { color: var(--ink-dim); font-size: var(--fs-0); }
/* form-level error text is interface copy, not a signal lamp; it still uses
   the signal red because it reports a real fault the user must act on */
.field .error { color: var(--sig-red); font-size: var(--fs-0); font-weight: 600; }

/* ---- Tables -------------------------------------------------------------- */
.tablewrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
table.data { width: 100%; border-collapse: collapse; font-size: var(--fs-1); }
table.data thead th {
  position: sticky; top: 0; text-align: left; background: var(--surface-2); color: var(--ink);
  font-size: var(--fs-0); letter-spacing: .04em; text-transform: uppercase;
  padding: 12px 16px; border-bottom: 2px solid var(--accent); white-space: nowrap;
}
table.data td { padding: 11px 16px; border-bottom: 1px solid var(--border); color: var(--ink-mut); }
table.data td:first-child { color: var(--ink); font-weight: 600; }
table.data tbody tr:hover td { background: var(--surface-3); }
table.data tbody tr:last-child td { border-bottom: 0; }

/* ---- Wall scale ---------------------------------------------------------- */
/* Past ~2000px the clamps have mostly landed; open the gutters and let the
   grids breathe so a wall reads as a board, not a stretched phone page. */
@media (min-width: 2000px) {
  .app-main { padding-left: 3vw; padding-right: 3vw; }
  .tiles { gap: var(--sp-3); }
  .cards { gap: var(--sp-3); }
}

/* =========================================================================
   YBM-ITS portal console chrome. Layers on tokens.css + app.css.
   Every colour is a token reference. No colour literal of any kind - not hex,
   not rgb/rgba, not hsl - appears in this file, including inside these
   comments: the invariant is enforced by a plain text scan, so prose that
   merely QUOTES a forbidden form would fail the gate just as loudly as a rule
   that used one. That is the intended strictness, not a bug in the check.
   Red/amber/green appear ONLY in .alert (a real fault the operator must act
   on) and never as decoration: org status, plan tier and invite state use the
   non-signal .tag family. .chip stays the alarm lamp app.css defines.

   LOAD ORDER IS LOAD-BEARING: served as TOKENS_CSS + APP_CSS + CONSOLE_CSS in
   that order (one response). The handful of rules here that re-style an
   app.css selector (.btn-outline over .btn, table.data td's hairline) match it
   on specificity and win on source order alone - deliberately, so none of them
   needs !important and any app UI that loads app.css WITHOUT this file is
   completely unaffected.

   WHY a new .panel family instead of extending app.css's .card: 21 shipped app
   UIs render .card today. A head/body split with a fixed head floor is not what
   any of them draw, so widening .card to cover the console would restyle all
   21 at once, sight unseen. .panel, .tag and .stat are additive; .card, .chip,
   .btn-stop and table.data keep the shapes those UIs were built against.

   Motion: tokens.css already kills every transition under
   prefers-reduced-motion with !important, so the polish block at the bottom is
   neutralised there without repeating the media query.
   ========================================================================= */

/* ---- shell -------------------------------------------------------------- */
/* Column layout on <body> so .site-footer is pushed to the bottom of a short
   page (an empty members table) instead of floating under the header. */
body.console { min-height: 100vh; display: flex; flex-direction: column; }

.site-header {
  position: sticky; top: 0; z-index: 100;
  background: var(--surface-2); border-bottom: 1px solid var(--border);
}
/* Opaque, unlike app.css's translucent+blur .app-topbar: this header hosts the
   nav dropdown and the mobile drawer, and a backdrop-filter creates a
   containing block that would clip both to the header's own box. */
.header-inner {
  max-width: 1200px; margin: 0 auto; padding: 0 var(--sp-3);
  min-height: 60px; display: flex; align-items: center;
  justify-content: space-between; gap: var(--sp-2);
}
/* The brand lockup appears in TWO places and both must be listed. It is in the
   sticky header (.header-inner) and, chrome-less, at the top of the sign-in and
   forced-flow card (.auth-card in auth_base.html). app.css styles it only under
   .app-topbar, so a selector that named .header-inner alone left the auth pages
   matching NO img rule at all - and ybm-logo.png is 512x512 intrinsic, so the
   sign-in page rendered a 512px logo bursting out of a 420px card. Nothing in
   the template suite could see it: the markup is correct and it is the
   stylesheet that does not reach it. If a third placement appears, add it here
   rather than restating the rules. */
.header-inner .brand,
.auth-card .brand {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  font-size: var(--fs-2); font-weight: 700; color: var(--ink); flex: none;
}
.header-inner .brand img,
.auth-card .brand img { width: 28px; height: 28px; object-fit: contain; }
.header-inner .brand i,
.auth-card .brand i { color: var(--accent); font-style: normal; }

/* Belt and braces for the whole console surface. The defect above was a missing
   selector, not a wrong one, and this is the rule that bounds the blast radius
   of the next missing selector: no image the portal renders can exceed its
   column, whatever else is or is not matched. Scoped to console.css, which only
   the portal serves - app.css is shared by 21 crates and is not touched. */
.console img { max-width: 100%; }

.console-main {
  flex: 1; width: 100%; max-width: 1200px; margin: 0 auto;
  padding: var(--sp-4) var(--sp-3);
}
/* Admin tables (orgs, fleet, audit) carry more columns than prose does; .wide
   opts one page out of the reading measure rather than widening every page. */
.console-main.wide { max-width: 1800px; }

.site-footer { border-top: 1px solid var(--border); padding: var(--sp-3); }
.site-footer-inner {
  max-width: 1200px; margin: 0 auto; display: flex; flex-wrap: wrap;
  align-items: center; justify-content: space-between; gap: var(--sp-2);
  color: var(--ink-dim); font-size: var(--fs-0);
}

/* ---- nav ---------------------------------------------------------------- */
.nav { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.nav-link {
  color: var(--ink-mut); font-size: var(--fs-0); font-weight: 600;
  padding: 6px 11px; border-radius: var(--radius-sm); white-space: nowrap;
}
.nav-link:hover { color: var(--ink); background: var(--surface-3); }
/* The active pill is the accent at 12% - a derived token, so it stays legible
   on the light theme too. A hardcoded tint would be a fifth undeclared colour. */
.nav-link.active { color: var(--accent); background: var(--accent-soft); }
.nav-group { position: relative; }
.nav-dropdown {
  display: none; position: absolute; top: calc(100% + 6px); left: 0;
  min-width: 190px; padding: 5px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-lg); z-index: 200;
}
/* Opened by a class the script toggles, NOT :hover - a hover-only dropdown is
   unreachable by keyboard and sticky on touch. Under the strict CSP that
   script is an external file; there is no inline handler anywhere. */
.nav-group.open > .nav-dropdown { display: block; }
.nav-dropdown .nav-link { display: block; }

.nav-burger {
  display: none; flex-direction: column; justify-content: center; gap: 5px;
  width: 44px; height: 44px; padding: 0; cursor: pointer;
  background: none; border: none; border-radius: var(--radius-sm);
}
.nav-burger span {
  display: block; width: 22px; height: 2px; margin: 0 auto;
  background: var(--ink); border-radius: 2px;
}
@media (max-width: 860px) {
  .nav-burger { display: flex; }
  .nav-shell > .nav { display: none; }
  .nav-shell.open > .nav {
    display: flex; flex-direction: column; align-items: stretch; gap: 2px;
    position: absolute; top: 100%; left: 0; right: 0; padding: var(--sp-1);
    background: var(--surface-2); border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow-lg);
    /* dvh, not vh: on mobile Safari the collapsing URL bar makes 100vh taller
       than the visible viewport, which would hide the last nav item - and the
       last item is "Sign out". */
    max-height: calc(100dvh - 60px); overflow-y: auto;
  }
  /* In the drawer the dropdown is just an indented sub-list; a floating panel
     inside a scrolling drawer would clip against its own overflow. */
  .nav-dropdown { display: block; position: static; border: 0; box-shadow: none;
                  background: transparent; padding-left: var(--sp-2); }
}

/* ---- page header -------------------------------------------------------- */
.page-header {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-3);
}
/* Smaller than tokens.css's h1 (--fs-4, sized for a control-room wall): this
   one is read at desk distance and shares a row with its action buttons. */
.page-header h1 { margin: 0; font-size: var(--fs-3); }
.page-header-actions { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
/* flex-basis:100% forces the subtitle onto its own line under the title even
   when the actions have already wrapped. */
.page-subtitle {
  flex-basis: 100%; margin: 4px 0 0; color: var(--ink-mut); font-size: var(--fs-0);
}
.section-label {
  margin: var(--sp-3) 0 var(--sp-1); color: var(--ink-mut);
  font-size: var(--fs-0); font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase;
}

/* ---- panel (head + body; NOT app.css's .card) --------------------------- */
.panels {
  display: grid; gap: var(--sp-3); align-items: stretch;
  /* min(100%, 320px) so a 360px phone gets one full-width column instead of a
     320px track plus a horizontal scrollbar. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
}
.panel {
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
}
.panel.full { grid-column: 1 / -1; }
.panel-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-2); padding: var(--sp-2) var(--sp-3);
  /* fixed floor so panels sitting in one row line their bodies up */
  min-height: 3.5rem; box-sizing: border-box;
  border-bottom: 1px solid var(--border);
}
.panel-head h2 { margin: 0; font-size: var(--fs-2); font-weight: 600; }
.panel-body { flex: 1; padding: var(--sp-3); }
/* Kill the leading/trailing margin of whatever the template dropped in, so the
   body padding is the only spacing that shows. */
.panel-body > :first-child { margin-top: 0; }
.panel-body > :last-child { margin-bottom: 0; }
/* A table that fills a panel edge-to-edge would otherwise draw a second border
   and a second radius just inside the panel's own. overflow:hidden on .panel
   re-clips the corners. */
.panel .tablewrap { border: 0; border-radius: 0; }

/* ---- alerts (the ONE place signal colour is allowed outside app.css) ---- */
/* Tint and edge are color-mix over the signal token rather than the four
   hard-coded colour literals this vocabulary was ported from - those were a
   fourth, undeclared red that kept dark-theme text on a light background. */
.alert {
  padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-3);
  border: 1px solid; border-radius: var(--radius); font-size: var(--fs-1);
}
.alert-ok   { color: var(--sig-green);
              border-color: color-mix(in srgb, var(--sig-green) 45%, transparent);
              background: color-mix(in srgb, var(--sig-green) 12%, transparent); }
.alert-warn { color: var(--sig-amber);
              border-color: color-mix(in srgb, var(--sig-amber) 45%, transparent);
              background: color-mix(in srgb, var(--sig-amber) 12%, transparent); }
.alert-err  { color: var(--sig-red);
              border-color: color-mix(in srgb, var(--sig-red) 45%, transparent);
              background: color-mix(in srgb, var(--sig-red) 12%, transparent); }

/* ---- tags: NON-signal labels (org status, plan tier, invite state) ------ */
/* The whole reason this family exists: "active"/"lapsed"/"priority"/"pending"
   are metadata, not machine state. Painting them green/red would spend the
   alarm palette on a subscription column, leaving a real fault indistinguishable
   from a plan name. Shape and weight carry the difference here instead. */
.tag {
  display: inline-block; padding: 2px 9px; border-radius: 999px;
  font-size: var(--fs-0); font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ink-mut); background: var(--surface-3);
  border: 1px solid var(--border-hi);
}
.tag-accent { color: var(--accent); background: var(--accent-soft);
              border-color: var(--accent-line); }
/* --m-beta is tokens.css's declared NON-signal status accent - the one colour
   available for "this is different" that is not red/amber/green. */
.tag-beta   { color: var(--m-beta);
              background: color-mix(in srgb, var(--m-beta) 14%, transparent);
              border-color: color-mix(in srgb, var(--m-beta) 40%, transparent); }
/* Dashed + dim = absent/expired, the same visual grammar app.css already uses
   for .chip.off and .tile.stale. Consistency beats a new colour. */
.tag-dim    { color: var(--ink-dim); background: transparent; border-style: dashed; }

/* ---- buttons: outline + small (row actions). Destructive rows use
        .btn-outline + data-confirm - NEVER .btn-stop, which app.css reserves
        for a real machine stop. Revoking an invite is undoable paperwork; a
        stop button is a hazard control, and spending it on a table row trains
        operators to ignore the colour that means "the machine is stopping".
        The confirmation is the guard, not the paint. ------------------------ */
.btn-outline { background: transparent; color: var(--accent); border-color: var(--accent); }
.btn-outline:hover { background: var(--accent-soft); }
/* 34px, under app.css's 44px touch floor: these sit inside table rows where a
   44px control forces every row to 60px+. The @media block at the bottom does
   NOT relax this - the affected controls are buttons, not typed input. */
.btn-sm { min-height: 34px; font-size: var(--fs-0); padding: 6px 13px; }
.btn-row { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
.btn-full { width: 100%; }

/* ---- filter bar + pagination (live inside a .panel, around a .tablewrap) */
.filter-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border);
}
/* app.css sets width:100% on every input for stacked .field forms; a filter bar
   is a horizontal row, so width is released and a min-width keeps the controls
   from collapsing to their placeholder. */
.filter-bar input, .filter-bar select {
  width: auto; min-width: 150px; min-height: 34px;
  padding: 5px 11px; font-size: var(--fs-0); border-radius: var(--radius-sm);
}
.pagination {
  display: flex; align-items: center; justify-content: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3); border-top: 1px solid var(--border);
  color: var(--ink-mut); font-size: var(--fs-0);
}
/* Prev/next at the ends of the range stay in place and stop responding rather
   than disappearing - a control that vanishes moves the one beside it under
   the cursor mid-click. */
.pagination .off { opacity: .35; pointer-events: none; }
/* Console tables are long and dense; app.css's full-strength --border hairline
   turns 50 rows into a grid. Softer here only - app UIs read at distance and
   keep theirs. */
table.data td { border-bottom-color: var(--border-subtle); }

/* ---- stats, kv, empty state, one-time secrets --------------------------- */
.stats-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-3); }
.stat {
  flex: 1 1 150px; min-width: 140px; padding: var(--sp-2) var(--sp-3);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius);
}
/* --fs-3, not app.css's --fs-num: these are counts read at a desk, and --fs-num
   scales to 148px for numerals meant to be read across a shed. */
.stat-value { font-size: var(--fs-3); font-weight: 800; color: var(--ink);
              font-variant-numeric: tabular-nums; line-height: 1.1; }
.stat-label { margin-top: 2px; color: var(--ink-mut); font-size: var(--fs-0); }
/* A qualifier under a stat, drawn only when there IS something to qualify - today
   the count of subscription rows this build cannot decode. Dimmer than the label
   on purpose: it is a repair job, not an alarm, and red/amber/green stay reserved
   for real machine state. */
.stat-note { margin-top: 2px; color: var(--ink-dim); font-size: var(--fs-0); }
/* Only the anchor form gets the hover edge: a plain .stat is not clickable and
   must not pretend to be. */
a.stat:hover { border-color: var(--accent); }

.kv { display: grid; grid-template-columns: max-content 1fr; gap: 6px var(--sp-3); }
.kv dt { color: var(--ink-mut); font-size: var(--fs-0); font-weight: 700;
         letter-spacing: .06em; text-transform: uppercase; }
/* min-width:0 + overflow-wrap: a grid item's automatic minimum size is its
   content, so an unbroken value (a session id, a long slug) would otherwise
   widen the whole grid and scroll the page sideways. */
.kv dd { margin: 0; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
@media (max-width: 640px) { .kv { grid-template-columns: 1fr; } }

.empty { padding: var(--sp-4); text-align: center; color: var(--ink-mut); }

/* one-time secrets: temp password, invite link, backup codes */
/* Dashed accent edge = "this is shown once and is not stored anywhere you can
   get it back from". user-select:all makes the whole value one click to copy,
   so a hand-retyped temp password is never the failure mode. */
.secret {
  display: block; padding: var(--sp-2) var(--sp-3); margin: var(--sp-2) 0;
  font-family: var(--font-mono); font-size: var(--fs-1);
  color: var(--ink); background: var(--surface-2);
  border: 1px dashed var(--accent-line); border-radius: var(--radius);
  overflow-wrap: anywhere; user-select: all;
}
/* The one signal-coloured thing outside .alert, on the same grounds app.css
   allows --sig-red on .field .error: "save this now, it will never be shown
   again" is a real consequence the operator must act on before navigating, not
   decoration. It labels the warning line, never the secret itself. */
.secret-warn { color: var(--sig-amber); font-weight: 700; }

/* The TOTP enrolment QR. The white plate is NOT a theming oversight: a QR needs dark
   modules on a light background to scan, and an operator enrolling 2FA at night is
   exactly the person most likely to be in dark mode. The card frames it; the quiet
   zone stays white in both themes. `--qr-plate` is a fixed white on purpose and is
   the only such token, declared beside the rest so it is still one vocabulary. */
.qr {
  display: block; width: fit-content; margin: var(--sp-2) auto;
  padding: var(--sp-2); background: var(--qr-plate);
  border: 1px solid var(--border); border-radius: var(--radius);
}
.qr svg { display: block; width: 200px; height: 200px; }

/* The recovery-code list. `.secret` is display:block, so inside a default <ol>
   each code would clear its own marker and the numbers would float above the
   values. Numbering the <li> itself keeps the ordinal beside the code, which
   matters here: an operator transcribing 10 one-time codes needs to see which
   one they are on. */
.codes { list-style: decimal inside; padding: 0; margin: var(--sp-2) 0; }
.codes li { margin: 0 0 var(--sp-1); }
.codes li .secret { display: inline-block; width: 100%; margin: 4px 0 0; }

/* ---- auth pages (chrome-less; used by the forced flows too) ------------- */
/* No header, no footer, no nav - the forced password/TOTP pages render through
   this, and a logout control on them is a dead button: /logout is gated and is
   not on the gate's exemption list, so the POST 303s and the browser reissues
   it as a GET. Nothing to remove is the only reliable way to not ship it. */
.auth-wrap { min-height: 100vh; display: grid; place-items: center; padding: var(--sp-3); }
.auth-card {
  width: 100%; max-width: 420px; padding: var(--sp-4);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-md);
}
.auth-card h1 { font-size: var(--fs-3); margin-bottom: 4px; text-align: center; }
.auth-sub { margin: 0 0 var(--sp-3); color: var(--ink-mut);
            font-size: var(--fs-0); text-align: center; }
.auth-alt { margin: var(--sp-3) 0 0; text-align: center; font-size: var(--fs-0); }

/* ---- interaction polish (appended last; wins without !important) -------- */
/* No `transform` and no `all` in the transition list: app.css's two-tap .armed
   pattern animates box-shadow on a real timer, and transitioning `all` would
   also catch layout properties and make a resize crawl. */
.btn, .nav-link, .stat, table.data tbody tr {
  transition: background-color 140ms ease, border-color 140ms ease,
              color 140ms ease, box-shadow 140ms ease;
}
/* :where() keeps specificity at zero so any component rule still overrides the
   ring; :focus-visible (not :focus) means a mouse click does not draw it. */
:where(a, button, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
  border-radius: var(--radius-sm);
}
/* --ring, so the halo tracks the active theme's accent. */
input:focus, select:focus, textarea:focus { box-shadow: 0 0 0 3px var(--ring); }
@media (max-width: 860px) {
  /* iOS zooms any focused input under 16px - pin it. */
  input, select, textarea { font-size: 16px; }
}
