/* ==========================================================================
   MONITOR — Flight Dispatch Center
   Design language: a dispatch desk at night. Dark by function (control-room
   glare control), not by trend — with an accent language borrowed from
   runway/annunciator lighting rather than generic UI blue.
   ========================================================================== */

/* Font <link> tags (with preconnect) live in base.html's <head> — a CSS
   @import here would block this whole stylesheet on that fetch instead of
   letting the browser discover and load fonts in parallel with CSS. */

:root {
  /* -- palette -- */
  --ink:        #060B0F;   /* base background      */
  --panel:      #0D1518;   /* card / panel surface  */
  --panel-alt:  #101B1E;   /* recessed surface       */
  --border:     #1B2B2E;
  --text:       #E7F1EE;
  --text-muted: #7C9490;
  --text-faint: #718A85;

  --accent:     #2DD4BF;   /* deep teal — brand / interactive, glass glow  */
  --accent-dim: rgba(45, 212, 191, .14);
  --ok:         #3DDC84;   /* runway green   — nominal / online     */
  --warn:       #F5B023;   /* taxiway amber  — ready (<35 min)      */
  --urgent:     #F2751F;   /* beacon orange  — urgent (<20 min)     */
  --crit:       #E5484D;   /* strobe red     — critical (<10 min)   */

  --radius-sm: 4px;
  --radius: 8px;
  --radius-lg: 12px;
  --radius-pill: 999px;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --font-sans: 'IBM Plex Sans', -apple-system, 'Segoe UI', Roboto, sans-serif;
  --font-scale: 1;

  /* 4px-grid spacing scale - not applied retroactively everywhere in this
     pass (that's real, page-by-page work), but every new/touched rule
     going forward should pull from this instead of a fresh hand-picked
     value, which is how spacing drifted in the first place. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;

  /* Elevation scale, derived from the shadow values already in real use
     across cards/panels/modals (not invented) - just named and
     consolidated so new surfaces pick one of these instead of a fresh
     hand-tuned blur/spread. */
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, .35);
  --shadow-md: 0 6px 22px rgba(0, 0, 0, .16);
  --shadow-lg: 0 20px 50px rgba(0, 0, 0, .4);
  --shadow-xl: 0 24px 60px rgba(0, 0, 0, .35);

  /* Transition durations - existing usage already clustered in this
     150-250ms range (matches "subtle, premium, not flashy"); this just
     gives it a name so it stops drifting between .2s/220ms/.25s for
     effectively the same intent. */
  --transition-fast: 150ms ease;
  --transition-base: 200ms ease;
  --transition-slow: 250ms ease;

  /* translucent "glass" surfaces (sidebar, topbar chips, cards, panels) sit
     over the radial-gradient body background — these must repaint per
     theme too, so they're variables rather than a bare rgba() literal. */
  --glass:        rgba(13, 21, 24, .6);
  --glass-strong: rgba(13, 21, 24, .72);
  --native-scheme: dark;
}

:root[data-theme="light"] {
  --ink:        #F3F7F6;
  --panel:      #FFFFFF;
  --panel-alt:  #EEF3F2;
  --border:     #D6E2E0;
  --text:       #0E1B1A;
  --text-muted: #5B6D6A;
  --text-faint: #647873;

  --accent:     #0D9488;
  --accent-dim: rgba(13, 148, 136, .12);

  --glass:        rgba(255, 255, 255, .82);
  --glass-strong: rgba(255, 255, 255, .92);
  --native-scheme: light;
}

* { box-sizing: border-box; }

/* Global native checkbox/radio theming - without this, every plain
   <input type=checkbox/radio> renders with the browser's default (OS)
   accent color (blue on most platforms), clashing with this theme's
   teal accent. Confirmed via screenshot: Settings' "Dispatch Alert
   Sound" toggle rendered as a stock blue checkbox against this dark
   teal-accented design. .fdc-popover-chip already did this locally;
   this makes it universal so no checkbox/radio anywhere is missed. */
input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }

/* ============ FORM CONTROL / BUTTON BASELINE ============
   Design-system pass: this codebase already has a consistent visual
   language for form controls (.search, .auth-field input/select) and
   buttons (.btn-ghost, .btn-filter, .auth-btn) - the SAME background/
   border/radius/color/focus values, just each hand-copied onto its own
   purpose-named class rather than a shared base. Anything not given one
   of those specific classes (several controls added during tonight's
   own work included) fell through to the browser's default white/native
   styling, which is what read as "unfinished" against the dark theme.
   These base rules extend the SAME established values to every plain
   input/select/textarea/button by default; the specific classes above
   still apply on top for their own sizing/context and are unaffected. */
/* Deliberately BARE element selectors here (specificity 0,0,1) - not
   :not([type=...]) exclusions, which would each add attribute-selector
   specificity and end up MORE specific than the existing single-class
   rules (.search is 0,1,0; .auth-field input is 0,1,1), overriding their
   padding/font-size instead of yielding to them. A bare selector is
   guaranteed lower specificity than any class-based rule, so this only
   ever fills gaps, never fights an existing, more deliberate style.
   Checkbox/radio picking up background/padding/border-radius here is a
   no-op in every mainstream browser (native checkbox/radio rendering
   ignores box-model styling), so no exclusion is needed for them. */
input, select, textarea {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  padding: 8px 12px;
  font-size: calc(13px * var(--font-scale, 1));
  font-family: var(--font-sans);
}
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
/* Global fallback so every interactive element (nav links, icon buttons,
   the profile trigger, plain <a> tags, etc.) gets the same visible teal
   ring on keyboard focus instead of each browser's inconsistent default
   (blue in Chrome, often invisible against dark surfaces in others).
   More specific :focus-visible rules elsewhere in this file (inputs,
   toggles, etc.) still win via specificity/order where they differ. */
a:focus-visible, button:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
input[type="file"] {
  padding: 6px;
  font-size: calc(12.5px * var(--font-scale, 1));
  color: var(--text-muted);
}
input[type="file"]::file-selector-button {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  padding: 6px 12px;
  font-size: calc(12px * var(--font-scale, 1));
  font-family: var(--font-sans);
  cursor: pointer;
  margin-right: 10px;
}
input[type="file"]::file-selector-button:hover { border-color: var(--accent); }

button, .btn-ghost, .btn-filter, .auth-btn, .btn-preview, .btn-approve {
  font-family: var(--font-sans);
}
button:not(.icon-btn):not(.wx-icon-btn):not([class*="btn-"]):not([class*="auth-btn"]) {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  padding: 8px 14px;
  font-size: calc(12.5px * var(--font-scale, 1));
  cursor: pointer;
}
button:not(.icon-btn):not(.wx-icon-btn):not([class*="btn-"]):not([class*="auth-btn"]):hover { border-color: var(--accent); }

/* Disabled + active states - previously undefined anywhere in this
   stylesheet, so a disabled control looked identical to an enabled one
   (one single spot, _fdc_profile_popover.html, patched this itself via
   inline JS-set opacity; nowhere else had any treatment at all). */
button:disabled, input:disabled, select:disabled, textarea:disabled {
  opacity: .45;
  cursor: not-allowed;
  filter: none;
}
button:not(:disabled):active { transform: translateY(1px); }

/* Form-UX audit: only the CONTROL dimmed on disable, never its label - a
   bright label sitting directly over a ghosted-out input (e.g. Settings'
   "Use my saved admin credentials" toggling the username/password fields
   off) read as a rendering glitch rather than an intentional state. Dims
   the whole field group together instead. */
.auth-field:has(input:disabled) label { opacity: .45; }


/* Dark-themed scrollbars everywhere - previously undefined anywhere in
   this file, so every scrollable area (page body, .widget-list-scroll,
   any overflow:auto table wrapper) showed the browser's bright native
   scrollbar, jarring against this dark theme. Applies globally via `*`
   rather than to specific containers one at a time, since new scrollable
   areas are added throughout the app regularly (this session alone added
   several) and shouldn't each need to remember to opt in. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border) var(--panel-alt);
}
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: var(--panel-alt); }
*::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: var(--radius-pill);
  border: 2px solid var(--panel-alt);
}
*::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
*::-webkit-scrollbar-corner { background: var(--panel-alt); }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  /* The page itself never scrolls - .content below is the one real scroll
     container. This is the actual "GitHub/Linear/Vercel app shell"
     pattern: sidebar and topbar simply sit outside the scrolling area
     entirely, so there's no position:sticky edge case to fight (a sticky
     element riding along a single page-level scroll needs its containing
     block's height to exactly match the content it's next to, which broke
     twice in a row here). Nothing needs to "follow" scroll if it was never
     part of what scrolls. */
  overflow: hidden;
  background:
    radial-gradient(1100px 520px at 82% -8%, rgba(45, 212, 191, .10), transparent 60%),
    radial-gradient(700px 420px at -6% 12%, rgba(45, 212, 191, .06), transparent 55%),
    var(--ink);
  background-attachment: fixed;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: calc(14px * var(--font-scale, 1));
}

.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

a { color: inherit; }

/* ============ SHELL / LAYOUT ============ */

.shell {
  display: grid;
  grid-template-columns: 232px 1fr;
  height: 100vh;
  /* Exact height, not min-height: html/body no longer scroll at all (see
     above), so .shell must never grow taller than the viewport - .content
     is the one element that scrolls, internally, below. Sidebar and
     topbar simply sit outside that scrolling area, so they're always
     visible with no sticky-positioning math involved at all.
     User-configurable text size (Settings → Appearance) is applied per
     declaration via font-size: calc(Npx * var(--font-scale, 1)) on every
     font-size in this file (see the rest of the stylesheet) — NOT via
     zoom, which used to scale spacing/layout/cards/buttons along with
     text. At --font-scale: 1 every value below is byte-identical to its
     original px size. */
}

.sidebar {
  background: var(--glass-strong);
  backdrop-filter: blur(14px);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  padding: 22px 16px;
  /* Fills its grid cell exactly (.shell is exactly 100vh, single row) and
     never moves - no position:sticky needed since it was never part of
     any scrolling content to begin with. overflow-y:auto is the one
     remaining case: if the sidebar's own content (nav items,
     notifications) ever exceeds one viewport's worth, IT scrolls
     internally instead of the page needing to. */
  height: 100%;
  overflow-y: auto;
}

.brand { padding: 0 8px 22px; border-bottom: 1px solid var(--border); margin-bottom: 18px; }
.brand-mark {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: calc(20px * var(--font-scale, 1));
  letter-spacing: 2px;
  color: var(--text);
}
.brand-sub {
  font-size: calc(10.5px * var(--font-scale, 1));
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-top: 2px;
}

.nav { display: flex; flex-direction: column; gap: 2px; flex: 1; }
.nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  border-radius: var(--radius);
  color: var(--text-muted);
  text-decoration: none;
  font-size: calc(13.5px * var(--font-scale, 1));
  cursor: pointer;
}
.nav-badge {
  margin-left: auto;
  background: var(--warn);
  color: #1a1206;
  font-size: calc(10px * var(--font-scale, 1));
  font-weight: 700;
  line-height: 1;
  padding: 3px 6px;
  border-radius: var(--radius-pill);
  min-width: 16px;
  text-align: center;
}
.nav-item.active .nav-badge { background: var(--crit); color: #fff; }
.nav-item .nav-icon {
  width: 15px; height: 15px;
  flex-shrink: 0;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.3;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: .8;
}
.nav-item .nav-icon .icon-fill { fill: currentColor; stroke: none; }
.nav-item:hover { background: var(--panel-alt); color: var(--text); }
.nav-item.active { background: var(--panel-alt); color: var(--text); }
.nav-item.active .nav-icon { stroke: var(--accent); color: var(--accent); opacity: 1; filter: drop-shadow(0 0 4px rgba(45,212,191,.6)); }

.sidebar-foot {
  border-top: 1px solid var(--border);
  padding-top: 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.who-name {
  font-size: calc(13px * var(--font-scale, 1));
  font-weight: 600;
  /* Real contrast bug: this sits inside a <button> (base.html's
     #fdc-profile-trigger) with no explicit color, so it fell back to the
     browser's default button text color (effectively black) instead of
     the theme - the user's own name rendered far dimmer than the less
     important role line below it, which does set color explicitly.
     Confirmed via screenshot: name nearly unreadable against the dark
     sidebar, role line fine. */
  color: var(--text);
}
.who-role { font-size: calc(10.5px * var(--font-scale, 1)); color: var(--text-muted); letter-spacing: .5px; }
.logout { font-size: calc(12px * var(--font-scale, 1)); color: var(--text-muted); text-decoration: none; }
.logout:hover { color: var(--crit); }

/* ============ FDC PROFILE POPOVER (Discord-style, anchored above the sidebar-foot trigger) ============ */
.fdc-popover {
  position: absolute;
  left: 0;
  bottom: 100%;
  margin-bottom: 8px;
  width: 100%;
  max-width: 280px;
  max-height: 85vh;
  overflow-y: auto;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: 0 20px 50px rgba(0, 0, 0, .4);
  padding: 16px;
  z-index: 9999;
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px) scale(.97);
  transition: opacity var(--transition-fast), transform var(--transition-fast);
}
.fdc-popover.open { opacity: 1; visibility: visible; transform: translateY(0) scale(1); }
.fdc-popover-head { font-size: calc(11px * var(--font-scale, 1)); font-weight: 700; letter-spacing: .6px; color: var(--text-muted); text-transform: uppercase; margin-bottom: 12px; }
.fdc-popover-group { margin-bottom: 12px; }
.fdc-popover-group-label { font-size: calc(10.5px * var(--font-scale, 1)); color: var(--text-muted); letter-spacing: .5px; text-transform: uppercase; margin-bottom: 6px; display: block; }
.fdc-popover-options { display: flex; flex-wrap: wrap; gap: 8px; }
.fdc-popover-chip { display: flex; align-items: center; gap: 6px; padding: 5px 10px; border: 1px solid var(--border); border-radius: var(--radius-pill); font-size: calc(12px * var(--font-scale, 1)); cursor: pointer; background: var(--panel-alt); }
.fdc-popover-chip input { accent-color: var(--accent); }
.fdc-popover-actions { display: flex; gap: 8px; margin-top: 14px; }
.fdc-popover-empty { font-size: calc(12.5px * var(--font-scale, 1)); color: var(--text-muted); line-height: 1.5; }
.fdc-popover-error { font-size: calc(11.5px * var(--font-scale, 1)); color: var(--crit); margin-top: 8px; min-height: 1em; }

/* Transfer popover (per-row, All Flights table) - a fixed up/down CSS
   direction doesn't work here: a table row can be anywhere in the
   viewport (top, bottom, mid-scroll), so a row near the top pushes an
   upward-opening popover off-screen and a row near the bottom pushes a
   downward-opening one off-screen just the same. Positioned via
   getBoundingClientRect() in JS instead (flights.html), clamped to the
   viewport - position:fixed here so JS-computed top/left are relative
   to the viewport, not whatever scroll container the row sits in. */
.transfer-popover {
  position: fixed;
  left: auto;
  bottom: auto;
  top: 0;
  margin: 0;
  /* Explicit width, NOT inherited from .fdc-popover's width:100% - under
     position:fixed, 100% resolves against the viewport (not the trigger),
     so every one of these (one per table row) was computing to full
     viewport width, and the resulting box-model overflow (width + border
     + padding) pushed the whole page into unwanted horizontal scroll even
     while "closed" (opacity:0 doesn't remove layout impact). */
  width: 260px;
  max-width: 260px;
  box-sizing: border-box;
  transform: scale(.97);
}
.transfer-popover.open { transform: scale(1); }

/* ============ PEOPLE PICKER (search + Suggested/Available/Busy/Offline
   grouped assignee picker - static/js/people-picker.js) ============
   One popover shared across every trigger on the page (unlike
   .transfer-popover's one-per-row), repositioned and repopulated per
   trigger on open via positionFloatingEl() - see base.html. */
.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 50%;
  font-size: calc(9.5px * var(--font-scale, 1)); font-weight: 700;
  color: #fff; letter-spacing: .2px; text-transform: uppercase;
  flex-shrink: 0; line-height: 1;
}
.avatar-sm { width: 18px; height: 18px; font-size: calc(8.5px * var(--font-scale, 1)); }
.avatar-muted { background: var(--panel-alt) !important; border: 1px solid var(--border); color: var(--text-muted); }

.status-dot {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
}
.status-dot.online { background: var(--ok); box-shadow: 0 0 0 2px var(--accent-dim); }
.status-dot.busy { background: var(--warn); }
.status-dot.offline { background: var(--text-faint); }

.people-picker-trigger { display: inline-flex; align-items: center; gap: 6px; max-width: 168px; }
.people-picker-trigger-label {
  font-size: calc(12px * var(--font-scale, 1)); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; max-width: 100px;
}
.people-picker-trigger-chevron { color: var(--text-muted); font-size: 10px; }
.people-picker-trigger-avatar-slot:empty { display: none; }
/* Brief pulse confirming an assignment just landed (optimistic or
   confirmed) - kept snappier than the picker's own open/close transition
   since it's decorative feedback, not a state change the user waits on. */
@keyframes people-picker-pulse {
  0% { box-shadow: 0 0 0 0 var(--accent-dim); }
  60% { box-shadow: 0 0 0 5px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.people-picker-trigger.just-assigned { animation: people-picker-pulse 450ms ease; }
@keyframes people-picker-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-3px); } 60% { transform: translateX(3px); }
  40% { transform: translateX(-2px); } 80% { transform: translateX(2px); }
}
.people-picker-trigger.assign-failed { animation: people-picker-shake 380ms ease; }

.people-picker {
  position: fixed;
  width: 300px;
  max-width: calc(100vw - 16px);
  max-height: min(420px, 70vh);
  display: flex;
  flex-direction: column;
  background: var(--glass-strong);
  backdrop-filter: blur(16px);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xl);
  z-index: 9999;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(4px) scale(.97);
  transition: opacity var(--transition-slow), transform var(--transition-slow), visibility var(--transition-slow);
  overflow: hidden;
}
.people-picker.open { opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(0) scale(1); }
.people-picker-head { padding: 10px 12px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
.people-picker-subject {
  font-size: calc(10px * var(--font-scale, 1)); font-weight: 700; letter-spacing: .5px;
  text-transform: uppercase; color: var(--text-muted); margin-bottom: 8px;
}
.people-picker-search {
  width: 100%; background: var(--panel-alt); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 7px 10px; font-size: calc(12.5px * var(--font-scale, 1));
  color: var(--text); outline: none; box-sizing: border-box;
  transition: border-color var(--transition-fast);
}
.people-picker-search:focus { border-color: var(--accent); }
.people-picker-list { overflow-y: auto; padding: 6px; }
.people-picker-group + .people-picker-group { margin-top: 2px; }
.people-picker-group-label {
  font-size: calc(9.5px * var(--font-scale, 1)); font-weight: 700; letter-spacing: .6px;
  text-transform: uppercase; color: var(--text-muted); padding: 8px 8px 4px;
}
.people-picker-card {
  display: flex; align-items: center; gap: 9px; width: 100%; box-sizing: border-box;
  padding: 7px 8px; border: none; border-radius: var(--radius);
  background: transparent; color: var(--text); text-align: left; cursor: pointer;
  transition: background var(--transition-fast), transform var(--transition-fast);
}
.people-picker-card:hover, .people-picker-card:focus-visible { background: var(--panel-alt); }
.people-picker-card:active { transform: scale(.98); }
.people-picker-card.selected { background: var(--accent-dim); }
.people-picker-card-info { display: flex; flex-direction: column; min-width: 0; flex: 1; gap: 1px; }
.people-picker-card-name {
  font-size: calc(12.5px * var(--font-scale, 1)); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.people-picker-card-meta {
  display: flex; align-items: center; gap: 5px; font-size: calc(10.5px * var(--font-scale, 1)); color: var(--text-muted);
}
.people-picker-card-check {
  color: var(--accent); font-size: 13px; flex-shrink: 0;
  opacity: 0; transform: scale(.5); transition: opacity var(--transition-fast), transform var(--transition-fast);
}
.people-picker-card.selected .people-picker-card-check { opacity: 1; transform: scale(1); }
.people-picker-empty { padding: 22px 12px; text-align: center; color: var(--text-muted); font-size: calc(12px * var(--font-scale, 1)); }

/* ============ HEADER NOTIFICATION BELL (top-right, dropdown below) ============ */
#fdc-notif-trigger { position: relative; }
#fdc-notif-badge { position: absolute; top: -4px; right: -4px; margin-left: 0; }
.fdc-notif-dropdown {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 8px;
  width: 320px;
  max-width: 90vw;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: 0 20px 50px rgba(0, 0, 0, .4);
  padding: 16px;
  z-index: 9999;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px) scale(.97);
  transition: opacity var(--transition-fast), transform var(--transition-fast);
}
.fdc-notif-dropdown.open { opacity: 1; visibility: visible; transform: translateY(0) scale(1); }

.main {
  display: flex;
  flex-direction: column;
  min-width: 0;
  /* Matches .shell's exact 100vh - .main itself never scrolls (overflow:
     hidden); only .content below does. This is what keeps .topbar
     permanently visible/pinned above the scrolling content, same as
     .sidebar never moving beside it. */
  height: 100vh;
  overflow: hidden;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 26px;
  border-bottom: 1px solid var(--border);
  gap: 12px;
  /* Was flex-wrap:wrap - the menu button, status chips, and notification
     icons would break onto separate lines the moment they didn't all fit
     (narrow phones, or the 761-900px range where the sidebar is still the
     full desktop column). nowrap + .status-strip's own overflow-x:auto
     below keeps this a single row at every width - the chips scroll
     internally instead of the whole header wrapping. */
  flex-wrap: nowrap;
  /* .main is a flex column with overflow:hidden - without this, the
     topbar (like any flex item) would be free to shrink below its
     natural height as .content grows. It never should. */
  flex-shrink: 0;
}
.drawer-toggle, .topbar-actions { flex-shrink: 0; }

/* Fills the space between the menu button and the notification icons;
   scrolls horizontally (drag/swipe) instead of wrapping when the chips
   don't all fit, so the header stays one row instead of the DB chip
   dropping to a second line or overflowing off-screen uncontained. */
.status-strip {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1 1 auto;
  min-width: 0;
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.status-strip::-webkit-scrollbar { display: none; }

/* Real, confirmed bug (not mobile-only - happens at every width): the
   notification bell's position:relative wrapper <div> is block-level by
   default, so with no layout rule of its own here, it forced a line
   break before its sibling #sound-toggle button - the two topbar-right
   icons stacked vertically instead of sitting side by side. Confirmed
   via screenshot at both 375px and 1440px. */
.topbar-actions { display: flex; align-items: center; gap: 8px; }
.status-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
  background: var(--glass);
  backdrop-filter: blur(10px);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 7px 12px;
}
.status-label { font-size: calc(10.5px * var(--font-scale, 1)); color: var(--text-muted); letter-spacing: 1px; }
.status-value { font-size: calc(12.5px * var(--font-scale, 1)); }

.led { width: 8px; height: 8px; border-radius: 50%; background: var(--text-faint); display: inline-block; }
.led-ok { background: var(--ok); box-shadow: 0 0 6px var(--ok); }
.led-warn { background: var(--warn); box-shadow: 0 0 6px var(--warn); }
.led-crit { background: var(--crit); box-shadow: 0 0 6px var(--crit); }
.led-unknown { background: var(--text-faint); }
.led.blink { box-shadow: 0 0 6px var(--accent); background: var(--accent); }

/* ============ TOGGLE (shared boolean on/off control) ============
   One reusable capsule switch for every boolean setting in the app -
   replaces bare <input type="checkbox"> for persistent on/off settings
   (Settings page's Control Panel, Automatic OFP Retrieval, etc). Markup:
     <input type="hidden" name="x" value="0" form="...">
     <label class="toggle">
       <input type="checkbox" name="x" value="1" form="..." {{ 'checked' if ... }}>
       <span class="toggle-track"><span class="toggle-thumb"></span></span>
     </label>
   The checkbox itself is visually hidden (not display:none - that would
   drop it from the tab order) and the track/thumb are its next sibling,
   so :checked + .toggle-track can drive the animated state with no JS. */
.toggle { position: relative; display: inline-flex; align-items: center; cursor: pointer; vertical-align: middle; }
.toggle input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
}
.toggle-track {
  width: 42px;
  height: 24px;
  flex-shrink: 0;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  position: relative;
  transition: background-color var(--transition-base), border-color var(--transition-base);
}
.toggle-thumb {
  position: absolute;
  top: 1px;
  left: 1px;
  width: 20px;
  height: 20px;
  background: #fff;
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .35);
  transition: transform var(--transition-base);
}
.toggle input:checked + .toggle-track { background: var(--accent); border-color: var(--accent); }
.toggle input:checked + .toggle-track .toggle-thumb { transform: translateX(18px); }
.toggle input:focus-visible + .toggle-track { outline: 2px solid var(--accent); outline-offset: 2px; }
.toggle input:disabled + .toggle-track { opacity: .45; cursor: not-allowed; }
.toggle:has(input:disabled) { cursor: not-allowed; }
/* .auth-field label (0,1,1 specificity: one class + one element) beats a
   bare .toggle (0,1,0) whenever a toggle sits inside a .auth-field
   wrapper - confirmed live, it collapsed the whole control to ~2x14px
   (display:block stomping the inline-flex layout, plus the uppercase/
   letter-spacing text styling meant for a field label, not a switch).
   This scopes a definitively higher-specificity (0,2,1) override to that
   exact collision instead of relying on which one happens to be declared
   later in the file. */
.auth-field label.toggle { display: inline-flex; text-transform: none; letter-spacing: normal; margin-bottom: 0; }

/* ============ UNSAVED-CHANGES INDICATOR + CONFIRM DIALOG ============ */
/* :not([hidden]), not a bare selector - an unconditional display:inline-flex
   here has the SAME specificity as the browser's own built-in
   [hidden]{display:none} rule, and author styles beat user-agent styles
   at equal specificity - so toggling the `hidden` attribute via JS was
   silently doing nothing, the badge stayed visible regardless (confirmed
   live). Scoping to :not([hidden]) means this rule simply doesn't apply
   at all while hidden is set, letting the browser's default win. */
.unsaved-badge:not([hidden]) {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: calc(12px * var(--font-scale, 1));
  color: var(--warn);
  font-weight: 600;
  margin-right: 12px;
}

.confirm-modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .55);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  z-index: 2000;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--transition-fast);
}
.confirm-modal-overlay.open { opacity: 1; visibility: visible; }
.confirm-modal {
  width: 100%;
  max-width: 420px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: 0 24px 60px rgba(0, 0, 0, .35);
  padding: 20px;
  transform: translateY(12px) scale(.98);
  transition: transform var(--transition-fast);
}
.confirm-modal-overlay.open .confirm-modal { transform: translateY(0) scale(1); }
.confirm-modal-title { font-size: calc(15px * var(--font-scale, 1)); font-weight: 700; margin: 0 0 8px; }
.confirm-modal-body { font-size: calc(13px * var(--font-scale, 1)); color: var(--text-muted); line-height: 1.5; margin: 0 0 18px; }
.confirm-modal-actions { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }

.cp-bot-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  margin-bottom: 18px;
}
.cp-bot-card {
  background: var(--glass);
  backdrop-filter: blur(10px);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 16px;
  transition: border-color var(--transition-base), box-shadow var(--transition-base);
}
.cp-bot-card:hover {
  border-color: rgba(45, 212, 191, .28);
  box-shadow: var(--shadow-md);
}
.cp-bot-card .panel-head { padding: 0 0 10px; margin-bottom: 10px; border-bottom: 1px solid var(--border); }
.cp-bot-card .panel-head h2 { font-weight: 600; letter-spacing: .01em; font-size: calc(.95em * var(--font-scale, 1)); }
.cp-bot-card .status-value { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--radius-pill); background: var(--panel-alt); border: 1px solid var(--border); }

/* Standard padding for the content area directly under a .panel-head -
   was the same style="padding:12px 18px;" hand-copied onto every panel
   body across notams.html and weather.html independently. */
.panel-body { padding: var(--space-3) var(--space-5); }

/* Same role as .panel-body, but for content that follows something else
   with its own bottom spacing (a .panel-head border, typically) - no top
   padding to avoid doubling that gap. Was style="padding:0 18px 18px;"
   hand-copied 12 times across 7 templates. */
.panel-body-notop { padding: 0 var(--space-5) var(--space-5); }

/* Inline accent-button variant: .auth-btn defaults to a full-width
   login-form button (width:100%, padding:11px) - this is the same
   button used inline elsewhere (Import, Save, etc.), which was always
   the same style="width:auto;padding:9px 18px;" override, 7 times
   across 5 templates. */
.auth-btn.btn-inline { width: auto; padding: var(--space-2) var(--space-5); }

/* Checkbox/radio row label - not bold like a form <label> normally is,
   aligned inline with its input. Was hand-copied identically across
   edit_user.html and fdc_shift_start.html. */
.checkbox-label { display: flex; align-items: center; gap: 6px; font-weight: 400; }

/* Small button variant for compact contexts (Control Panel bot cards,
   inline action rows, task list quick-actions) - was several slightly
   different inline overrides (padding:6px 12px vs 7px 12px vs 4px 10px)
   with no real reason for the differences; one real modifier class
   instead, usable on either button style. */
.btn-ghost.btn-sm, .btn-filter.btn-sm { padding: var(--space-2) var(--space-3); font-size: calc(11.5px * var(--font-scale, 1)); }

/* Utility for <form> elements that need to sit inline next to buttons/
   other forms (block by default) - was 4 separate style="display:inline"
   attributes doing the same thing. */
.inline-form { display: inline-block; }
@media (max-width: 720px) {
  .cp-bot-grid { grid-template-columns: 1fr; }
}

.icon-btn {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 10px;
  cursor: pointer;
  color: var(--text);
  transition: border-color var(--transition-fast), color var(--transition-fast);
}
/* Every other button variant (.btn-ghost, .btn-preview, .wx-icon-btn, the
   generic button:hover rule) darkens/brightens or shifts border-color on
   hover - .icon-btn (close buttons, collapse chevrons, pin/copy icons
   that aren't in the weather-card context) was explicitly excluded from
   that generic rule (line 169) but never got its own replacement, so
   every icon-only button in the app gave zero hover feedback at all. */
.icon-btn:hover { border-color: var(--accent); color: var(--accent); }
/* Real bug, not a rendering artifact: the notification-bell SVG
   (base.html/_fdc_notification_bell.html) uses class="nav-icon" but sits
   inside a plain .icon-btn button, not .nav-item - so it never matched
   ".nav-item .nav-icon .icon-fill { fill: currentColor }", and its <path>
   elements have no icon-fill class either. Both misses meant the SVG fell
   back to its default fill (black), invisible against this dark theme.
   Confirmed via screenshot + DOM inspection - the icon rendered as a
   fully blank button. */
.icon-btn .nav-icon {
  width: 16px; height: 16px;
  vertical-align: middle;
  display: inline-block;
}
.icon-btn .nav-icon path { fill: currentColor; stroke: none; }
/* Sound-toggle icon's wave arcs/mute-slash need actual strokes, not fills -
   this specific selector (one more class than the blanket rule above)
   outweighs it in specificity. The mute slash was invisible before this
   existed: its inline stroke="var(--crit)" attribute was always losing to
   the blanket rule's stroke:none (a CSS property from a stylesheet always
   beats a presentation attribute, regardless of selector specificity) -
   confirmed by reading the cascade, not assumed. */
.icon-btn .nav-icon path.sound-wave { fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; }
.icon-btn .nav-icon path.sound-off-slash { fill: none; stroke: var(--crit); stroke-width: 1.6; stroke-linecap: round; }
.icon-btn .sound-off { display: none; }
.icon-btn[aria-pressed="true"] .sound-on { display: none; }
.icon-btn[aria-pressed="true"] .sound-off { display: inline; }

.icon-btn .chevron { display: inline-block; transition: transform var(--transition-fast); }
.icon-btn.collapsed .chevron { transform: rotate(-90deg); }

.hamburger-icon {
  width: 16px; height: 16px;
  vertical-align: middle;
  display: inline-block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: round;
}

.live-banner {
  margin: 0 26px;
  margin-top: 16px;
  padding: 10px 16px;
  border-radius: var(--radius);
  border: 1px solid var(--warn);
  background: rgba(245, 176, 35, 0.08);
  color: var(--warn);
  font-size: calc(13px * var(--font-scale, 1));
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}
/* `display: flex` must only apply when NOT hidden — an unconditional
   `display` on the element overrides the browser's default
   `[hidden] { display: none }`, which was leaving an empty bordered box
   visible on every page regardless of the `hidden` attribute. */
.live-banner:not([hidden]) { display: flex; }
.live-banner.crit { border-color: var(--crit); background: rgba(229, 72, 77, 0.1); color: var(--crit); }

.content {
  padding: 22px 26px 40px;
  flex: 1;
  /* The one real scroll container in the whole app shell now. min-height:0
     is the actual fix, not overflow-y alone - a flex item defaults to
     min-height:auto (never shrinks below its content's natural size),
     which silently defeats overflow:auto in a flex column; without it
     .content would just keep growing .main taller instead of scrolling. */
  overflow-y: auto;
  min-height: 0;
  /* Deliberately the only page-level motion in the whole app - every
     other navigation is a real server round-trip (not a SPA transition),
     so one quiet, fast settle-in here reads as polish rather than a
     second "loading" moment stacked on top of the network wait. Kept
     understated on purpose (a control-room tool should read as CALM, not
     flashy) - respected by the global prefers-reduced-motion rule below. */
  animation: content-settle var(--transition-slow) ease-out;
}
@keyframes content-settle {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes pulse-warn {
  0%, 100% { box-shadow: 0 0 0 0 rgba(245, 176, 35, .5); }
  50% { box-shadow: 0 0 0 6px rgba(245, 176, 35, 0); }
}
@keyframes pulse-crit {
  0%, 100% { box-shadow: 0 0 0 0 rgba(229, 72, 77, .5); }
  50% { box-shadow: 0 0 0 6px rgba(229, 72, 77, 0); }
}

/* High-visibility weather-minimums alert (NEAR MIN / BELOW MIN) — a
   dedicated, bolder animation from the ring-only pulse-warn/pulse-crit
   above (kept separate so this doesn't also change the unrelated ETOPS
   overdue badge, which reuses pulse-crit). Background-color pulses +
   a static severity-tinted border, so the row still reads as "alert"
   even at the animation's dim frame, not just at its peak. */
@keyframes pulse-warn-wx {
  0%, 100% { background: rgba(245, 176, 35, .09); box-shadow: 0 0 0 0 rgba(245, 176, 35, .55), inset 0 0 0 1px rgba(245, 176, 35, .35); }
  50% { background: rgba(245, 176, 35, .24); box-shadow: 0 0 0 5px rgba(245, 176, 35, 0), inset 0 0 0 1px rgba(245, 176, 35, .7); }
}
@keyframes pulse-crit-wx {
  0%, 100% { background: rgba(229, 72, 77, .12); box-shadow: 0 0 0 0 rgba(229, 72, 77, .65), inset 0 0 0 1px rgba(229, 72, 77, .45); }
  50% { background: rgba(229, 72, 77, .3); box-shadow: 0 0 0 7px rgba(229, 72, 77, 0), inset 0 0 0 2px rgba(229, 72, 77, .9); }
}
.mini-row.wx-pulse-warn { animation: pulse-warn-wx 1.4s ease-in-out infinite; border-radius: var(--radius-sm); border: 1px solid rgba(245, 176, 35, .4); }
.mini-row.wx-pulse-crit { animation: pulse-crit-wx 1s ease-in-out infinite; border-radius: var(--radius-sm); border: 1px solid rgba(229, 72, 77, .55); }

/* ============ STAT ROW ============ */

/* Dynamic column count instead of a fixed repeat(4, 1fr) - this row's
   card count varies by page and by what's conditionally rendered
   (dashboard alone has grown from 4 to 5 cards this session), and a fixed
   count either overflows into an awkward lone card on its own half-empty
   row (too few columns for the real count) or leaves dead trailing space
   (too many). grid-auto-flow: column + grid-auto-columns: 1fr always
   creates exactly as many equal-width columns as there are actual
   children, filling the row completely either way. Pages with a
   genuinely fixed, known-in-advance count (e.g. security_dashboard.html's
   3-column health row) still override via their own inline
   grid-template-columns, which wins over this by specificity same as
   before - this only changes the behavior for rows that didn't specify
   one. */
.stat-row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 14px;
  margin-bottom: 20px;
}
.stat-row-notams { grid-auto-flow: column; grid-auto-columns: 1fr; }
.stat-card {
  background: var(--glass);
  backdrop-filter: blur(10px);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  transition: border-color var(--transition-base);
}
.stat-card:hover { border-color: rgba(45, 212, 191, .28); }
/* Some stat-cards are plain <div> summaries, others are <a> links to a
   filtered view (Dashboard's Missed/Needs Check) - both rendered
   identically before, so nothing signaled which were clickable until you
   happened to hover. A quiet arrow that reveals on hover, matching the
   arrow affordance already used elsewhere in this app ("View details →",
   "Task Archive →"), not a new visual language. */
.stat-card-link { position: relative; text-decoration: none; color: inherit; cursor: pointer; }
.stat-card-link::after {
  content: '→';
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
  color: var(--text-faint);
  font-size: calc(13px * var(--font-scale, 1));
  opacity: 0;
  transform: translateX(-3px);
  transition: opacity var(--transition-fast), transform var(--transition-fast);
}
.stat-card-link:hover::after { opacity: 1; transform: translateX(0); }
.stat-label { font-size: calc(11px * var(--font-scale, 1)); letter-spacing: 1px; text-transform: uppercase; color: var(--text-muted); }
.stat-value { font-size: calc(26px * var(--font-scale, 1)); font-weight: 700; }

/* ============ BOARD GRID ============ */

.board-grid {
  display: grid;
  grid-template-columns: 1fr 300px;
  gap: 16px;
  align-items: start;
}

/* ============ DASHBOARD GRID (named areas, responsive) ============
   Main Dashboard only - distinct from .board-grid above (shared by
   Settings/NOTAM Center/etc., which rely on that grid's implicit
   auto-placement and must not change). Widgets never hardcode a
   position: dropping a new <section class="panel"> into
   .dashboard-grid-main or .dashboard-grid-sidebar just stacks into that
   column's existing flex flow - no CSS changes needed per new widget. */
.dashboard-grid {
  display: grid;
  grid-template-columns: 1fr 300px;
  grid-template-areas: "main sidebar";
  gap: 16px;
  align-items: start;
}
.dashboard-grid-main {
  grid-area: main;
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}
.dashboard-grid-sidebar {
  grid-area: sidebar;
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}
/* Tablet: responsive stacked layout - main content above the sidebar,
   both full-width, sidebar never shifts columns based on card count
   since it's always the second grid-area regardless of row count. */
@media (max-width: 1080px) {
  .dashboard-grid { grid-template-columns: 1fr; grid-template-areas: "main" "sidebar"; }
}
/* Mobile: single column, tighter gap. */
@media (max-width: 640px) {
  .dashboard-grid { gap: 12px; }
}

/* Explicit independent-column layout (Settings page): each column is its
   own flex stack, not a shared grid row, so expanding/collapsing a card
   in one column can never push, stretch, or misalign the other column —
   .board-grid's implicit row-based auto-placement (used elsewhere) is
   what caused that with plain alternating <section> children. */
.board-col-left, .board-col-right {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}

.panel {
  background: var(--glass);
  backdrop-filter: blur(10px);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 16px;
  transition: border-color var(--transition-base);
  /* Wide content (tables especially - flights-table has 7-8 columns)
     scrolls WITHIN its own panel instead of forcing the whole page
     into horizontal scroll. No wrapper div needed in each template -
     .panel already wraps every table in the app. */
  overflow-x: auto;
}
.panel:hover { border-color: rgba(45, 212, 191, .28); }
.panel-side { display: flex; flex-direction: column; grid-column: 2; }
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--border);
}
.panel-head h2 { font-size: calc(14px * var(--font-scale, 1)); margin: 0; }
.panel-sub { font-size: calc(11.5px * var(--font-scale, 1)); color: var(--text-muted); font-weight: 400; margin-left: 8px; }

/* Consistent header-to-content gap for every widget - individual
   templates were each responsible for their own top spacing (typically
   inline padding:0 18px 18px, a widely-copied pattern that starts at 0),
   so a table/first row often sat flush against .panel-head's bottom
   border. This adjacent-sibling rule guarantees a minimum gap after
   EVERY panel-head, everywhere, without editing each template. */
.panel-head + * { margin-top: 10px; }

/* ============ SETTINGS — COLLAPSIBLE SECTIONS ============
   grid-template-rows 1fr/0fr is the standard no-JS-measurement way to
   animate a "height: auto" element — the inner wrapper needs
   overflow:hidden so content doesn't show while the row is shrinking. */
.settings-section-head { cursor: pointer; user-select: none; }
.settings-section-head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.settings-section-head .chevron { transition: transform var(--transition-base); }
.settings-section-head.collapsed { border-bottom-color: transparent; }
.settings-section-head.collapsed .chevron { transform: rotate(-90deg); }

.settings-section-body-wrap {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows var(--transition-base);
}
.settings-section-body-wrap.collapsed { grid-template-rows: 0fr; }
.settings-section-body-inner { overflow: hidden; min-height: 0; }

.search {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  padding: 7px 12px;
  font-size: calc(12.5px * var(--font-scale, 1));
  width: 220px;
}
.search:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ============ FLIGHT STRIPS (signature element) ============
   Modeled on physical paper flight-progress strips used on ATC
   boards: one strip per flight, a coloured tab reads the urgency
   at a glance from across the room. */

.strip-board { padding: 10px; display: flex; flex-direction: column; gap: 8px; }

.strip {
  display: grid;
  grid-template-columns: 4px 1.4fr 1.2fr 110px auto;
  align-items: center;
  gap: 16px;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 12px 14px 12px 0;
  transition: border-color var(--transition-base);
}

.strip-tab { align-self: stretch; border-radius: var(--radius-sm); background: var(--text-faint); }
.strip.tier-ok .strip-tab { background: var(--ok); }
.strip.tier-warn .strip-tab { background: var(--warn); }
.strip.tier-urgent .strip-tab { background: var(--urgent); }
.strip.tier-crit .strip-tab { background: var(--crit); }
.strip.tier-crit { border-color: var(--crit); }

/* Preview modal's "Remaining" readout (preview.js) - the tier-* classes
   elsewhere are always compound selectors scoped to .strip/.flights-table
   rows, so a bare #preview-remaining.tier-crit would otherwise carry no
   color at all. */
#preview-remaining.tier-ok     { color: var(--ok); }
#preview-remaining.tier-warn   { color: var(--warn); }
#preview-remaining.tier-urgent { color: var(--urgent); }
#preview-remaining.tier-crit   { color: var(--crit); }

.strip-main { display: flex; flex-direction: column; gap: 3px; }
.strip-flight { font-size: calc(16px * var(--font-scale, 1)); font-weight: 700; }
.strip-route { font-size: calc(12px * var(--font-scale, 1)); color: var(--text-muted); }
.strip-route-arrow { margin: 0 3px; opacity: .6; }

.strip-mid { display: flex; flex-direction: column; gap: 3px; font-size: calc(12px * var(--font-scale, 1)); color: var(--text-muted); }

.strip-time { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
.strip-remaining { font-size: calc(15px * var(--font-scale, 1)); font-weight: 700; }
.strip-badge {
  font-size: calc(10px * var(--font-scale, 1));
  letter-spacing: .5px;
  padding: 2px 7px;
  border-radius: var(--radius-sm);
  background: var(--border);
  color: var(--text-muted);
}
.strip.tier-ok .strip-badge { background: rgba(61,220,132,.15); color: var(--ok); }
.strip.tier-warn .strip-badge { background: rgba(245,176,35,.15); color: var(--warn); }
.strip.tier-urgent .strip-badge { background: rgba(242,117,31,.15); color: var(--urgent); }
.strip.tier-crit .strip-badge { background: rgba(229,72,77,.18); color: var(--crit); }

.btn-preview {
  justify-self: end;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  text-decoration: none;
  font-size: calc(12.5px * var(--font-scale, 1));
  font-weight: 600;
  color: var(--ink);
  background: var(--accent);
  padding: 8px 16px;
  border-radius: var(--radius);
  white-space: nowrap;
}
.btn-preview:hover { filter: brightness(1.08); }
.btn-preview.disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }

/* ============ SIDE PANELS ============ */

.mini-list { padding: 6px 10px 12px; }

/* Widgets expand internally instead of stretching the page - long lists
   (Task Center feeds, etc.) scroll within their own panel. */
.widget-list-scroll { max-height: 420px; overflow-y: auto; }

/* Dashboard sidebar panels (Weather/Special Ops/Recent Activity) - same
   containment, scoped by id rather than .mini-list itself since Archive's
   .log-list-full intentionally wants an unbounded list. */
#weather-panel, #special-ops-list, #activity-panel { max-height: 340px; overflow-y: auto; }
.mini-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 8px;
  border-bottom: 1px solid var(--border);
  font-size: calc(12.5px * var(--font-scale, 1));
}
.mini-row:last-child { border-bottom: none; }
/* No fixed width - a plain 4-letter ICAO fit the old 46px fine, but
   "VIDP (DEL)"-style suffixes and flight numbers with a space ("SV 826")
   don't, and were wrapping onto a second line inside that narrow box.
   nowrap + flex-shrink:0 keeps it on one line at its natural width
   instead, letting .mini-vis (flex:1) take whatever's left. */
.mini-icao { white-space: nowrap; flex-shrink: 0; }
.mini-vis { color: var(--text-muted); flex: 1; margin-left: 10px; }
.mini-tag { font-size: calc(10.5px * var(--font-scale, 1)); color: var(--text-muted); text-transform: uppercase; }

.log-row { padding: 10px 8px; border-bottom: 1px solid var(--border); }
.log-row:last-child { border-bottom: none; }
.log-row-head { display: flex; justify-content: space-between; font-size: calc(12px * var(--font-scale, 1)); margin-bottom: 4px; }
.log-row-head-right { display: flex; align-items: center; gap: 8px; }
.log-time { color: var(--text-muted); }
.log-msg {
  margin: 0;
  white-space: pre-wrap;
  font-family: var(--font-mono);
  font-size: calc(11px * var(--font-scale, 1));
  color: var(--text-muted);
}

.log-list-full .log-msg { max-height: none; overflow: visible; }

.empty-state { padding: 26px 14px; text-align: center; color: var(--text-faint); font-size: calc(12.5px * var(--font-scale, 1)); }
.empty-state.small { padding: 16px 10px; }

.status-cell { display: flex; align-items: center; gap: 10px; }

/* ============ FLIGHTS TABLE (Phase 3) ============ */

/* ============ ARCHIVE TABS ============ */
.archive-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
.archive-tab {
  background: none; border: none; cursor: pointer;
  padding: 10px 16px; margin-bottom: -1px;
  color: var(--text-muted); font-size: calc(13px * var(--font-scale, 1)); font-weight: 600;
  border-bottom: 2px solid transparent;
}
.archive-tab:hover { color: var(--text); }
.archive-tab.active { color: var(--accent); border-bottom-color: var(--accent); }

/* padding here (not just display/gap) because every real usage across
   the app hand-copied the exact same style="padding:12px 18px 0;" as a
   filter-bar's own top-of-panel spacing - 13 identical inline overrides
   across 10 templates. Baked in as the default; the one real exception
   (a filter-bar that ISN'T a panel's first child) explicitly overrides
   back to 0 instead. */
.filter-bar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding: var(--space-3) var(--space-5) 0; }
.filter-bar select,
.filter-bar input[type="text"],
.filter-bar input[type="date"] {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  padding: 7px 10px;
  font-size: calc(12.5px * var(--font-scale, 1));
  font-family: var(--font-sans);
}
.filter-bar input[type="date"] {
  color-scheme: var(--native-scheme);
}
.filter-bar input[type="date"]::-webkit-calendar-picker-indicator {
  filter: invert(1) brightness(1.6);
  cursor: pointer;
}
:root[data-theme="light"] .filter-bar input[type="date"]::-webkit-calendar-picker-indicator {
  filter: none;
}
.btn-filter {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  padding: 7px 12px;
  font-size: calc(12.5px * var(--font-scale, 1));
  cursor: pointer;
}
.btn-filter:hover { border-color: var(--accent); }

/* Reusable "action row" for widget bodies - export/revoke/add buttons
   used to be crammed into .panel-head next to the title, squeezing the
   collapse chevron and reading as cluttered. This sits inside the body
   instead, below the header, with its own comfortable spacing. */
.widget-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 12px 18px 0;
}

.flights-table { width: 100%; border-collapse: collapse; }
.flights-table th, .flights-table td {
  padding: 10px 16px;
  border-bottom: 1px solid var(--border);
  font-size: calc(12.5px * var(--font-scale, 1));
  text-align: left;
  vertical-align: middle;
}
.flights-table th {
  color: var(--text-muted);
  font-weight: 600;
  font-size: calc(11px * var(--font-scale, 1));
  letter-spacing: .5px;
  text-transform: uppercase;
}
.flights-table tbody tr:hover { background: var(--panel-alt); }
.etd-time { display: none; }
.flights-table tr.has-countdown.tier-ok     { box-shadow: inset 3px 0 0 var(--ok); }
.flights-table tr.has-countdown.tier-warn   { box-shadow: inset 3px 0 0 var(--warn); }
.flights-table tr.has-countdown.tier-urgent { box-shadow: inset 3px 0 0 var(--urgent); }
.flights-table tr.has-countdown.tier-crit   { box-shadow: inset 3px 0 0 var(--crit); }

.badge { font-size: calc(10.5px * var(--font-scale, 1)); padding: 3px 9px; border-radius: var(--radius-sm); letter-spacing: .5px; background: var(--border); color: var(--text-muted); }
.badge-waiting  { background: rgba(245, 176, 35, .15); color: var(--warn); }
.badge-approved { background: rgba(79, 168, 255, .15); color: var(--accent); }
/* SENDING has no automatic retry (bot/fds_sender.py never re-attempts an
   unconfirmed uplink) - a flight stuck here needs a human to check FDS
   directly, so it gets its own distinct (not "sent", not "waiting")
   color rather than falling back to the plain unstyled .badge default. */
.badge-sending  { background: rgba(242, 117, 31, .15); color: var(--urgent); }
.badge-sent     { background: rgba(61, 220, 132, .15); color: var(--ok); }
/* sending_watchdog.py's auto-flagged state - the bot itself has gone
   dark while this flight was left at SENDING (not just "still in
   progress" like plain .badge-sending), so it reads as critical/red -
   this is the one that actually needs a dispatcher to act. */
.badge-sending_timeout { background: rgba(229, 72, 77, .15); color: var(--crit); }
.badge-vfr      { background: rgba(61, 220, 132, .15); color: var(--ok); }
.badge-mvfr     { background: rgba(245, 176, 35, .15); color: var(--warn); }
.badge-lifr     { background: rgba(229, 72, 77, .15); color: var(--crit); }
/* Re-Dispatch overdue - same high-visibility glow used for BELOW MIN
   weather alerts (pulse-crit-wx), reused as-is rather than a new
   animation, so both read as the same severity of "needs attention now"
   language across the app. */
.rdsp-overdue-glow { animation: pulse-crit-wx 1s ease-in-out infinite; }
.badge-etops      { background: rgba(242, 117, 31, .15); color: var(--urgent); }
.badge-redispatch { background: rgba(45, 212, 191, .15); color: var(--accent); }
.badge-both       { background: rgba(229, 72, 77, .15); color: var(--crit); }
.badge-none       { background: var(--border); color: var(--text-faint); }
/* NOTAM "Informational" priority bucket (severity info+normal, folded
   together everywhere else - _dashboard_counts(), the priority filter card,
   the flight-context tallies) used to render as two different badge colors
   (teal for 'info', green for 'normal') depending on which literal value a
   given row happened to have, even though the card border-left and map
   markers for both already share one blue (#4fa8ff). Single dedicated
   class so the badge matches those and the bucket reads as one color. */
.badge-notam-info { background: rgba(79, 168, 255, .15); color: #4fa8ff; }
/* User-role badges (System Users table) - dedicated classes rather than
   reusing badge-approved/badge-etops/badge-waiting, which carry specific
   flight-operational meaning (ETOPS classification, waiting-on-dispatch)
   everywhere else in the app. A role tag isn't a status. */
.badge-role-admin      { background: rgba(45, 212, 191, .15); color: var(--accent); }
.badge-role-dispatcher { background: var(--border); color: var(--text-muted); }
.badge-role-tester     { background: rgba(124, 148, 144, .15); color: var(--text-muted); }
/* fdc_role_flight_assignments.status (Flights page's role-imported table) -
   a different lifecycle vocabulary from the ACARS flight-status badges
   above (WAITING/APPROVED/SENDING/SENT), so its own dedicated classes
   rather than the same {{ status|lower }} pattern accidentally colliding
   with (or silently falling back to unstyled .badge next to) those. */
.badge-imported     { background: var(--border); color: var(--text-muted); }
.badge-assigned     { background: rgba(79, 168, 255, .15); color: #4fa8ff; }
.badge-in-progress  { background: rgba(245, 176, 35, .15); color: var(--warn); }
.badge-released     { background: rgba(61, 220, 132, .15); color: var(--ok); }
.badge-completed    { background: rgba(61, 220, 132, .15); color: var(--ok); }
.tag-row { display: flex; gap: 6px; flex-wrap: wrap; }

/* An <a> being used as a card/row (whole element clickable) rather than a
   text link - was five separate copies of the same
   style="text-decoration:none;color:inherit;" override. */
.link-reset { text-decoration: none; color: inherit; }
.link-reset.link-reset-block { display: block; }

.report-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-bottom: 16px; }
.report-stats .stat-card.active-filter { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

.btn-preview.small { padding: 5px 11px; font-size: calc(11.5px * var(--font-scale, 1)); }
.muted { color: var(--text-faint); }
.table-note { padding: 12px 16px; font-size: calc(11.5px * var(--font-scale, 1)); color: var(--text-muted); border-top: 1px solid var(--border); }

/* ============ PREVIEW PAGE (Phase 4) ============ */

.back-link {
  display: inline-block;
  margin-bottom: 14px;
  font-size: calc(12.5px * var(--font-scale, 1));
  color: var(--text-muted);
  text-decoration: none;
}
.back-link:hover { color: var(--accent); }

.preview-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; }

.info-list { padding: 6px 18px 16px; }
.info-row {
  display: flex;
  justify-content: space-between;
  padding: 9px 0;
  border-bottom: 1px solid var(--border);
  font-size: calc(12.5px * var(--font-scale, 1));
}
.info-row:last-child { border-bottom: none; }
.info-row span:first-child { color: var(--text-muted); }

.metar-box {
  margin: 0;
  padding: 16px 18px;
  font-family: var(--font-mono);
  font-size: calc(12.5px * var(--font-scale, 1));
  color: var(--text);
  white-space: pre-wrap;
  min-height: 60px;
}

#acars-form { padding: 16px 18px 18px; }
#final_message {
  width: 100%;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  padding: 14px;
  font-family: var(--font-mono);
  font-size: calc(13px * var(--font-scale, 1));
  resize: vertical;
}
#final_message:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.preview-actions { display: flex; align-items: center; gap: 12px; margin-top: 14px; }
.draft-status { font-size: calc(11.5px * var(--font-scale, 1)); color: var(--text-muted); flex: 1; }

.btn-ghost {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  padding: 9px 16px;
  font-size: calc(12.5px * var(--font-scale, 1));
  cursor: pointer;
}
.btn-ghost:hover { border-color: var(--accent); }

/* A destructive action (delete, permanently retire) rendering with the
   exact same visual weight as "View details" is a real trust problem in
   an ops tool, not just a style nit - confirmed live, every NOTAM card's
   Delete/Retire buttons were indistinguishable from its read-only
   actions at a glance. Same ghost-button shape everywhere (no surprise
   layout shift), but the crit-red border/text says "this one is
   different" before a click ever happens - and inverts to a solid fill
   only on hover, so it doesn't shout from across the room, only when
   actually about to be pressed. */
.btn-danger {
  background: transparent; border: 1px solid var(--crit); color: var(--crit);
  border-radius: var(--radius); padding: 9px 16px;
  font-size: calc(12.5px * var(--font-scale, 1)); cursor: pointer;
}
.btn-danger:hover { background: var(--crit); color: #fff; }
.btn-danger.btn-sm { padding: var(--space-2) var(--space-3); font-size: calc(11.5px * var(--font-scale, 1)); }

.btn-approve {
  background: var(--ok);
  color: var(--ink);
  border: none;
  border-radius: var(--radius);
  padding: 9px 18px;
  font-size: calc(12.5px * var(--font-scale, 1));
  font-weight: 700;
  cursor: pointer;
}
.btn-approve:hover { filter: brightness(1.08); }

@media (max-width: 900px) {
  .preview-grid { grid-template-columns: 1fr; }
}

/* ============ PREVIEW MODAL ============ */

.preview-modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .55);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--transition-fast);
}
.preview-modal-overlay.open { opacity: 1; visibility: visible; }

.preview-modal {
  width: 100%;
  max-width: 820px;
  max-height: 88vh;
  overflow-y: auto;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: 0 24px 60px rgba(0, 0, 0, .35);
  transform: translateY(12px) scale(.98);
  transition: transform var(--transition-fast);
}
.preview-modal-overlay.open .preview-modal { transform: translateY(0) scale(1); }

.preview-modal-head {
  position: sticky;
  top: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 20px;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  z-index: 1;
}
.preview-modal-title { font-size: calc(15px * var(--font-scale, 1)); margin: 0; }
.preview-modal-body { padding: 18px 20px 20px; }
.preview-modal-body .preview-grid { margin-bottom: 16px; }
.preview-modal-body .panel { background: var(--panel-alt); }

@media (max-width: 640px) {
  .preview-modal-overlay { padding: 0; }
  .preview-modal { max-width: 100%; max-height: 100vh; height: 100%; border-radius: 0; }
}

.notam-detail-drawer { max-width: 640px; }
.notam-detail-section { margin-bottom: 18px; }
.notam-detail-section h3 {
  font-size: calc(11px * var(--font-scale, 1)); text-transform: uppercase; letter-spacing: .5px;
  color: var(--text-muted); margin: 0 0 8px;
}
.notam-detail-summary { font-size: calc(15px * var(--font-scale, 1)); font-weight: 600; line-height: 1.4; }
.notam-detail-related-item {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 8px 10px; margin-bottom: 6px; cursor: pointer;
  transition: border-color var(--transition-fast);
}
.notam-detail-related-item:hover { border-color: var(--accent); }

/* ============ WEATHER GRID (Phase 5) ============ */

.wx-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 12px;
  padding: 14px;
}

.wx-card {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.wx-card-head { display: flex; align-items: center; justify-content: space-between; }
.wx-icao { font-size: calc(17px * var(--font-scale, 1)); font-weight: 700; }

.wx-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 12px; }
.wx-metric { display: flex; flex-direction: column; gap: 2px; }
.wx-metric-label { font-size: calc(10px * var(--font-scale, 1)); letter-spacing: .5px; text-transform: uppercase; color: var(--text-muted); }
.wx-metric-value { font-size: calc(13px * var(--font-scale, 1)); }
/* Flags a station's "Updated" time once it's old enough that the data
   probably isn't trustworthy anymore - see weather.js's applyStaleness().
   Underline (not just color) so it doesn't rely on color alone. */
.wx-updated-value.wx-stale-warn { color: var(--warn); text-decoration: underline dotted; cursor: help; }
.wx-updated-value.wx-stale-crit { color: var(--crit); text-decoration: underline dotted; cursor: help; }

.wx-raw {
  margin: 0;
  font-size: calc(12.5px * var(--font-scale, 1));
  font-weight: 700;
  color: var(--text-faint);
  white-space: pre-wrap;
  word-break: break-all;
  border-top: 1px solid var(--border);
  padding-top: 8px;
}
.wx-taf { color: var(--text-muted); }

.wx-section-head { display: flex; align-items: center; justify-content: space-between; margin-top: 2px; }

.wx-icon-btn {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  padding: 0;
  cursor: pointer;
  color: var(--text-muted);
  transition: border-color var(--transition-fast), color var(--transition-fast);
}
.wx-icon-btn:hover { border-color: var(--accent); color: var(--text); }
.wx-icon-btn.copied { border-color: var(--ok); color: var(--ok); }

.wx-pin-btn {
  opacity: .45;
  filter: grayscale(1);
}
.wx-pin-btn:hover { opacity: .8; }
.wx-pin-btn.active { opacity: 1; filter: none; border-color: var(--accent); color: var(--accent); }

.wx-card.pinned { order: -1; border-color: var(--accent); box-shadow: 0 0 0 1px rgba(45, 212, 191, .25); }

/* ============ AUTH / STANDALONE PAGES ============ */

.auth-shell {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.auth-card {
  width: 100%;
  max-width: 380px;
  background: var(--glass-strong);
  backdrop-filter: blur(14px);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 32px 28px;
}
.auth-title {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: calc(20px * var(--font-scale, 1));
  letter-spacing: 2px;
  text-align: center;
  margin: 0 0 4px;
}
/* .auth-sub started as the login card's one-line tagline (uppercase,
   letter-spaced, centered) but got reused across ~20 templates as the
   general "muted secondary text" class - full instructional sentences
   and data labels were inheriting that uppercase/centered/11px styling,
   which reads fine for a 1-word tagline but is genuinely hard to read
   for a paragraph. Base rule is now plain, readable secondary text;
   the original tagline look is restored below, scoped to just the
   subtitle directly under the login card's title. */
.auth-sub {
  font-size: calc(12.5px * var(--font-scale, 1));
  line-height: 1.5;
  color: var(--text-muted);
  margin: 0 0 14px;
}
.auth-card .auth-title + .auth-sub {
  font-size: calc(11px * var(--font-scale, 1));
  letter-spacing: 1px;
  text-transform: uppercase;
  text-align: center;
  margin: 0 0 22px;
}
.auth-field { margin-bottom: 14px; }
.auth-field label {
  display: block;
  font-size: calc(10.5px * var(--font-scale, 1));
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 6px;
}
.auth-field input, .auth-field select {
  width: 100%;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  padding: 10px 12px;
  font-size: calc(13px * var(--font-scale, 1));
  font-family: var(--font-sans);
}
.auth-field input:focus-visible, .auth-field select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
/* :user-invalid (not :invalid) - only flags a field once the user has
   actually interacted with it, so a required/min/max field doesn't show
   as an error on page load before anyone's touched it. Previously a
   number field outside its min/max just bounced to the browser's native
   tooltip with no app-consistent error state (System Configuration's six
   threshold fields, Change Password's minlength, etc). */
.auth-field input:user-invalid, .auth-field select:user-invalid {
  border-color: var(--crit);
}
.auth-field input:user-invalid:focus-visible, .auth-field select:user-invalid:focus-visible {
  outline-color: var(--crit);
}
.auth-btn {
  width: 100%;
  background: var(--accent);
  color: var(--ink);
  border: none;
  border-radius: var(--radius);
  padding: 11px;
  font-size: calc(13px * var(--font-scale, 1));
  font-weight: 700;
  cursor: pointer;
  margin-top: 4px;
}
.auth-btn:hover { filter: brightness(1.08); }

[data-theme-option] {
  background: var(--panel-alt);
  color: var(--text);
  border: 1px solid var(--border);
  margin-top: 0;
}
[data-theme-option][aria-pressed="true"] {
  background: var(--accent);
  color: var(--ink);
  border-color: var(--accent);
}

.alert {
  border-radius: var(--radius);
  padding: 10px 12px;
  font-size: calc(12.5px * var(--font-scale, 1));
  margin-bottom: 16px;
  text-align: center;
}
.alert-error { border: 1px solid var(--crit); background: rgba(229, 72, 77, .1); color: var(--crit); }
.alert-success { border: 1px solid var(--ok); background: rgba(61, 220, 132, .1); color: var(--ok); }
.alert-warning { border: 1px solid var(--warn); background: rgba(245, 176, 35, .1); color: var(--warn); }

/* ============ ERROR PAGE ============ */

.error-panel {
  max-width: 480px;
  margin: 60px auto;
  text-align: center;
  padding: 40px 24px;
}
.error-code {
  font-size: calc(48px * var(--font-scale, 1));
  margin: 0 0 8px;
  font-family: var(--font-mono);
  color: var(--crit);
}
.error-message { color: var(--text-muted); margin: 0 0 20px; }

/* ============ RESPONSIVE ============ */

@media (max-width: 1080px) {
  .board-grid { grid-template-columns: 1fr; }
}

/* ============ MOBILE NAV DRAWER ============ */
/* .sidebar is laid out inline by .shell's grid on desktop. Under 760px it
   becomes an off-canvas drawer instead: fixed, off-screen by default,
   slid in via .open. See #drawer-toggle / #drawer-overlay JS in
   base.html for the open/close logic (outside-tap, nav-item tap, Esc,
   and resize-back-to-desktop all close it). */
.drawer-toggle { display: none; }
.drawer-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .5);
  backdrop-filter: blur(2px);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-slow);
  z-index: 999;
}
html.drawer-locked, html.drawer-locked body { overflow: hidden; }

@media (max-width: 760px) {
  .shell { grid-template-columns: 1fr; }

  .drawer-toggle { display: inline-flex; }
  .drawer-overlay { display: block; }
  .drawer-overlay.open { opacity: 1; pointer-events: auto; }

  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    height: 100dvh;
    width: 280px;
    max-width: 85vw;
    z-index: 1000;
    transform: translateX(-100%);
    transition: transform .25s cubic-bezier(.4, 0, .2, 1);
    box-shadow: 12px 0 32px rgba(0, 0, 0, .4);
    overflow-y: auto;
  }
  .sidebar.open { transform: translateX(0); }

  /* Touch targets: Apple HIG / Material both call for >=44px hit areas -
     desktop's mouse-sized buttons are too small to reliably tap. */
  .icon-btn, .btn-ghost, .btn-preview, .auth-btn, .nav-item, button {
    min-height: 44px;
  }
  .icon-btn { min-width: 44px; }
  .nav-item { padding: 12px 10px; }

  .content { padding: 16px 14px 32px; }
  .topbar { padding: 12px 14px; gap: 10px; }
  /* Tighter chip padding/gap so fewer phones actually need to scroll the
     strip to see the DB chip - overflow-x:auto (base rule, all widths)
     remains the fallback for whatever still doesn't fit. */
  .status-strip { gap: 6px; }
  .status-chip { padding: 6px 9px; gap: 6px; }

  /* Matches .content's tightened mobile edge padding - this sat between
     the topbar and .content with the desktop 26px margin left over,
     misaligned against everything below it and, once the topbar wraps to
     two rows on a narrow phone, sitting uncomfortably close beneath it. */
  .live-banner { margin: 12px 14px 0; }

  /* grid-auto-flow must be reset to row here, not just left inherited as
     column from the base rule above - with explicit 2 columns, no row
     limit, and auto-flow still set to column, every card would stack
     into a single column instead of wrapping 2-per-row (column-flow
     fills an entire column before ever moving to the next one). */
  .stat-row { grid-auto-flow: row; grid-template-columns: repeat(2, 1fr); }

  /* Flight Queue cards: the old mobile override only changed
     grid-template-columns and left the 4 children (tab/main/mid/time/
     button) to CSS grid's auto-placement - with no explicit row/column
     assignment that scattered them across cells (the countdown/badge
     landed squeezed next to reg/ETD, and the button got auto-placed into
     the 4px tab column and spilled over everything below it). Named
     areas place every child explicitly instead. */
  .strip {
    grid-template-columns: 4px 1fr auto;
    grid-template-areas:
      "tab main  arrow"
      "tab mid   arrow"
      "tab time  arrow";
    grid-template-rows: auto auto auto;
    row-gap: 6px;
    column-gap: 12px;
    align-items: center;
    padding: 12px 12px 12px 0;
  }
  .strip-tab   { grid-area: tab; }
  .strip-main  { grid-area: main; }
  .strip-mid   { grid-area: mid; flex-direction: row; flex-wrap: wrap; gap: 10px; }
  .strip-time  { grid-area: time; flex-direction: row; align-items: center; gap: 8px; }

  /* Scoped to the Flight Queue card specifically - a bare ".btn-preview"
     rule here previously applied to EVERY button using that class
     app-wide (ETOPS "OFP Viewer ->"/"Preview ->", etc.), forcing all of
     them into 44px circles regardless of their text length - that's the
     "distorted green circles overlapping text" bug. Every other
     .btn-preview keeps its normal rectangular shape and full label at
     every width. This one stays arrow-only to save vertical space in an
     already-dense card, but as a small rounded-rect pill (var(--radius)),
     not a forced-equal-width/height circle. */
  .strip .btn-preview {
    grid-area: arrow;
    justify-self: center;
    min-height: 44px;
    min-width: 44px;
    padding: 8px 12px;
    border-radius: var(--radius);
  }
  .strip .btn-preview-label { display: none; }
  .strip .btn-preview-arrow { font-size: calc(16px * var(--font-scale, 1)); }

  /* ETD columns: full "YYYY-MM-DD HH:MMZ" wastes width that's scarce on
     a phone - flip to a time-only stamp instead (rows are today's queue,
     the date isn't load-bearing information at a glance). */
  .etd-full { display: none; }
  .etd-time { display: inline; }

  /* Data tables (ETOPS Flights, All Flights): rather than force
     horizontal scroll that hides STATUS/REG off-screen, each <tr> becomes
     its own stacked card and every <td> gets a label pulled from its
     data-label attribute. No JS involved - the underlying <table> markup,
     and every selector flights.js/etops already use against it, are
     unchanged. */
  .responsive-cards thead { display: none; }
  .responsive-cards, .responsive-cards tbody, .responsive-cards tr, .responsive-cards td {
    display: block;
    width: 100%;
  }
  .responsive-cards tr {
    margin-bottom: 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 4px 12px;
    background: var(--panel-alt);
  }
  .responsive-cards td {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    padding: 8px 0;
    border-bottom: 1px solid var(--border);
    text-align: right;
  }
  .responsive-cards td:last-child { border-bottom: none; }
  .responsive-cards td[data-label]::before {
    content: attr(data-label);
    font-weight: 600;
    color: var(--text-muted);
    font-size: calc(10.5px * var(--font-scale, 1));
    text-transform: uppercase;
    letter-spacing: .5px;
    text-align: left;
    flex-shrink: 0;
  }
  .responsive-cards td[data-label=""]::before { content: none; }
  .responsive-cards td[data-label=""] { justify-content: flex-end; }

  /* Long unbreakable strings (IPv6 addresses, Safari/iOS user-agent
     strings in the Active Sessions widget) were getting clipped at the
     right edge instead of wrapping - table cells default to nowrap-ish
     shrink behavior once the table is narrower than its content. */
  .flights-table td, .flights-table th {
    overflow-wrap: break-word;
    word-break: break-word;
  }
}

/* ============ MOTION ============ */

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* ============ ETOPS VIEWER ============ */

.etops-grid { align-items: start; }
.psn-table td { vertical-align: middle; }
.psn-eta-cell { display: flex; align-items: center; gap: 6px; }
.psn-eta-input {
  width: 64px;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: #b389f0; /* ETA column — purple, per spec */
  border-radius: var(--radius-sm);
  padding: 5px 7px;
  font-size: calc(12px * var(--font-scale, 1));
}
.psn-eta-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.eta-override-tag { font-size: calc(10px * var(--font-scale, 1)); text-transform: uppercase; letter-spacing: .5px; }
.eta-save-status { font-size: calc(10.5px * var(--font-scale, 1)); margin-left: 4px; }
.psn-actual-cell { display: flex; align-items: center; gap: 6px; }
.psn-actual-input {
  width: 64px;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--crit); /* Actual-time column — red, per spec */
  border-radius: var(--radius-sm);
  padding: 5px 7px;
  font-size: calc(12px * var(--font-scale, 1));
}
.psn-actual-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.psn-now-btn { padding: 5px 9px; font-size: calc(11px * var(--font-scale, 1)); }

.takeoff-block { padding: 6px 18px 16px; border-top: 1px solid var(--border); }
.takeoff-input {
  width: 64px;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  font-size: calc(12px * var(--font-scale, 1));
  margin-right: 6px;
}
.takeoff-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Same small mono-field look as .takeoff-input, but for a short text code
   (destination ICAO) rather than a takeoff time - kept as its own class so
   .takeoff-input can still be reasoned about as "a time input" without an
   unrelated field silently depending on it. */
.airport-code-input {
  width: 56px;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  font-size: calc(12px * var(--font-scale, 1));
}
.airport-code-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.alt-wx-meta { display: flex; gap: 14px; font-size: calc(11px * var(--font-scale, 1)); color: var(--text-muted); margin-bottom: 6px; }

@keyframes psn-flash {
  0%, 100% { background: rgba(229, 72, 77, .06); }
  50% { background: rgba(229, 72, 77, .22); }
}
tr.psn-overdue { animation: psn-flash 1.1s ease-in-out infinite; }
.psn-overdue-badge { animation: pulse-crit 1.1s ease-in-out infinite; }

.redispatch-due-badge { margin-left: 6px; animation: pulse-crit 1.1s ease-in-out infinite; }

.hl-toolbar { display: flex; align-items: center; gap: 6px; }
.hl-btn {
  width: 20px; height: 20px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  cursor: pointer;
  padding: 0;
}
.hl-btn.hl-yellow { background: #f5e34a; }
.hl-btn.hl-red { background: #f28b8b; }
.hl-btn.hl-green { background: #9be39b; }

.ofp-text {
  margin: 0;
  padding: 16px 18px;
  font-family: var(--font-mono);
  font-size: calc(12.5px * var(--font-scale, 1));
  line-height: 1.6;
  color: var(--text);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 560px;
  overflow-y: auto;
  user-select: text;
}
.ofp-text mark { cursor: pointer; }
.ofp-text mark.hl-yellow { background: rgba(245, 227, 74, .35); color: var(--text); }
.ofp-text mark.hl-red { background: rgba(242, 139, 139, .35); color: var(--text); }
.ofp-text mark.hl-green { background: rgba(155, 227, 155, .35); color: var(--text); }
.ofp-text mark.hl-blue { background: rgba(142, 201, 240, .3); color: var(--text); }

.ofp-import { padding: 14px 18px 18px; border-top: 1px solid var(--border); }
.ofp-import summary { cursor: pointer; font-size: calc(12.5px * var(--font-scale, 1)); color: var(--text-muted); margin-bottom: 10px; }
.ofp-import summary:hover { color: var(--accent); }
.ofp-import textarea {
  width: 100%;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  padding: 12px;
  font-family: var(--font-mono);
  font-size: calc(12.5px * var(--font-scale, 1));
  resize: vertical;
  margin-bottom: 10px;
}

.etp-box { padding: 6px 18px 16px; }

.alt-wx-row { padding: 10px 8px; border-bottom: 1px solid var(--border); }
.alt-wx-row:last-child { border-bottom: none; }
.alt-wx-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.alt-wx-row .wx-raw { padding-top: 0; border-top: none; margin-bottom: 4px; }

#handover-notes {
  width: 100%;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  padding: 12px;
  font-family: var(--font-sans);
  font-size: calc(13px * var(--font-scale, 1));
  resize: vertical;
}
#handover-notes:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ============ NOTAM CENTER ============ */

.notam-textarea {
  width: 100%; background: var(--panel-alt); border: 1px solid var(--border);
  border-radius: var(--radius-sm); color: var(--text); padding: 9px 12px;
  font-family: var(--font-sans); font-size: calc(13px * var(--font-scale, 1)); resize: vertical;
}

/* ---- Priority overview: 4 clickable buckets, direct relabel of the
   real severity column (see notams_core.py's _dashboard_counts). ---- */
.notam-priority-row {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 14px;
}
.notam-priority-card {
  display: flex; align-items: center; gap: 12px;
  background: var(--glass); backdrop-filter: blur(10px);
  border: 1px solid var(--border); border-left: 3px solid var(--border-strong, var(--border));
  border-radius: var(--radius); padding: 14px 16px;
  text-decoration: none; color: inherit;
  transition: border-color var(--transition-base), transform var(--transition-fast);
}
.notam-priority-card:hover { transform: translateY(-1px); }
.notam-priority-card.active-filter { box-shadow: 0 0 0 1px currentColor; }
.notam-priority-card.priority-critical { border-left-color: var(--crit); }
.notam-priority-card.priority-critical .notam-priority-value { color: var(--crit); }
.notam-priority-card.priority-critical.active-filter { color: var(--crit); border-color: var(--crit); }
.notam-priority-card.priority-operational { border-left-color: #f2751f; }
.notam-priority-card.priority-operational .notam-priority-value { color: #f2751f; }
.notam-priority-card.priority-operational.active-filter { color: #f2751f; border-color: #f2751f; }
.notam-priority-card.priority-attention { border-left-color: var(--warn); }
.notam-priority-card.priority-attention .notam-priority-value { color: var(--warn); }
.notam-priority-card.priority-attention.active-filter { color: var(--warn); border-color: var(--warn); }
.notam-priority-card.priority-informational { border-left-color: #4fa8ff; }
.notam-priority-card.priority-informational .notam-priority-value { color: #4fa8ff; }
.notam-priority-card.priority-informational.active-filter { color: #4fa8ff; border-color: #4fa8ff; }
.notam-priority-icon { font-size: calc(20px * var(--font-scale, 1)); line-height: 1; }
.notam-priority-body { display: flex; flex-direction: column; gap: 2px; }
.notam-priority-value { font-size: calc(24px * var(--font-scale, 1)); font-weight: 700; }
.notam-priority-label { font-size: calc(11px * var(--font-scale, 1)); color: var(--text-muted); text-transform: uppercase; letter-spacing: .4px; }
@media (max-width: 760px) { .notam-priority-row { grid-template-columns: repeat(2, 1fr); } }

.notam-flight-context-row {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 0 18px 12px;
}
.notam-flight-context-card {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--panel-alt); padding: 10px 12px; font-size: calc(12px * var(--font-scale, 1));
}
.notam-flight-context-label { font-size: calc(10.5px * var(--font-scale, 1)); letter-spacing: .5px; text-transform: uppercase; color: var(--text-muted); }
@media (max-width: 640px) { .notam-flight-context-row { grid-template-columns: 1fr; } }

.notam-category-breakdown { margin: -6px 0 16px; }
.notam-category-breakdown summary {
  cursor: pointer; color: var(--text-muted); font-size: calc(12px * var(--font-scale, 1));
  padding: 4px 0;
}

.notam-list-view-toggle { display: flex; gap: 6px; margin-left: auto; }
.notam-list-view-toggle .btn-ghost.active { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); }

/* Separates the filter form above from the card list below within the
   single merged "NOTAMs" widget - reuses .panel-head's own border/spacing
   look so the widget still reads as clearly sectioned without being a
   second stacked panel. */
.notam-list-subhead {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px 18px 10px; margin-top: 6px; border-top: 1px solid var(--border);
}
.notam-card-footer { margin-top: 4px; font-size: calc(11px * var(--font-scale, 1)); }

.notam-card {
  border: 1px solid var(--border); border-left: 3px solid var(--border);
  border-radius: var(--radius);
  margin: 0 18px 12px; padding: 12px 14px; background: var(--panel-alt);
  transition: box-shadow var(--transition-base), border-color var(--transition-base);
}
.notam-card.severity-critical { border-left-color: var(--crit); }
.notam-card.severity-high { border-left-color: #f2751f; }
.notam-card.severity-medium { border-left-color: var(--warn); }
.notam-card.severity-info, .notam-card.severity-normal { border-left-color: #4fa8ff; }
.notam-card-urgent { background: linear-gradient(90deg, rgba(245,176,35,.06), transparent 40%); }
.notam-urgency-badge { animation: pulse-warn 1.8s ease-in-out infinite; }
.notam-card:last-child { margin-bottom: 18px; }
.notam-airport-group-header {
  margin: 18px 18px 8px; padding-top: 10px; border-top: 1px solid var(--border);
  display: flex; align-items: center; gap: 10px; font-size: calc(13px * var(--font-scale, 1)); font-weight: 700;
}
.notam-airport-group-header:first-child { margin-top: 0; padding-top: 0; border-top: none; }
.notam-card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex-wrap: wrap; cursor: pointer;
}
.notam-card-highlight { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(45,212,191,.35); }
.notam-operational-summary {
  font-size: calc(13.5px * var(--font-scale, 1)); font-weight: 600; margin: 8px 0 6px;
  /* No line-height was set here at all (falling back to the browser
     default, ~1.15) - too cramped for what's often a real, sometimes
     multi-line sentence (dispatchers read this as the primary operational
     takeaway on every card). Matches .notam-detail-summary's own 1.4,
     already set for the identical text shown in the detail drawer. */
  line-height: 1.4;
  /* fdc_tasks' notam_review descriptions (notam_flight_impact.py's
     _create_task_for_stage()) are real multi-line text with embedded
     newlines (per-flight bullet reasons, TO/CC address lines) - without
     this they'd collapse into one unreadable run, same generic HTML
     whitespace-collapsing every other single-line use of this class never
     had to worry about. pre-wrap keeps normal long-line wrapping intact. */
  white-space: pre-wrap;
}
.notam-meta-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 8px 14px; margin-bottom: 6px;
}
.notam-meta-grid .auth-sub { display: block; font-size: calc(10px * var(--font-scale, 1)); }
.notam-affected { display: flex; gap: 6px; flex-wrap: wrap; margin: 6px 0; }
.notam-raw-text summary { cursor: pointer; color: var(--text-muted); font-size: calc(11.5px * var(--font-scale, 1)); }
.notam-raw-text .ofp-text { font-size: calc(12.5px * var(--font-scale, 1)); margin-top: 6px; line-height: 1.5; }

.notam-pin-btn { opacity: .5; transition: opacity var(--transition-fast); }
.notam-pin-btn:hover, .notam-pin-btn.active { opacity: 1; }

.leaflet-control-layers { background: var(--panel) !important; color: var(--text) !important; border: 1px solid var(--border) !important; }
.leaflet-control-layers-list { font-size: calc(11.5px * var(--font-scale, 1)); }
.leaflet-popup-content-wrapper, .leaflet-popup-tip { background: var(--panel); color: var(--text); }
