/* ===== CMAT theme tokens =====
   Two professional/scientific palettes selectable at runtime from the View
   menu (sets data-theme on <html>):
     • (default)          → Light   — cool neutral scientific light
     • data-theme="dark"       → Dark    — slate scientific dark
   Only colors change between themes; layout/spacing is shared.
   The 3D viewports and the bottom console stay dark in every theme
   by design (space view + terminal aesthetic).

   A third theme lived here until 2026-08-18: a cream-and-orange-red palette
   whose values were taken from an outside brand. It was removed with every
   other trace of that brand, and app.js migrates anyone whose browser still
   has it stored onto Light. Do not reintroduce a theme whose colours are
   sourced from someone else's site.                                   */

:root {
  /* ---- shared, theme-independent ---- */
  --mono: "JetBrains Mono", "SF Mono", "DejaVu Sans Mono", Consolas, Monaco, monospace;
  --sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  /* accents (kept consistent for brand recognition) */
  --red:       #0b8aa8;   /* teal-cyan accent (active states, links, accent bar) */
  /* The accent doubles as a TEXT colour (active tree leaf, breadcrumb, BETA pill,
     flagged metrics, hover labels). At full strength it only reaches ~3.3:1 on the
     darker surfaces it lands on — under WCAG AA for body-sized text, and most of
     those labels are 11-13px, so the large-text allowance does not apply. This is
     the same hue darkened until it clears 4.5:1 on the WORST surface it is used
     on (--hover-bg / --sk-base, the darkest of them); fills, borders, outlines and
     gradients keep the full-strength --red so the brand still reads at a glance.
     Recomputed if any surface token changes. */
  --red-text:  #097189;   /* 4.63:1 on --hover-bg, 5.5:1 on cards */
  --red-hover: #097791;
  --red-border:#0a7d95;
  --red-edge:  #12a4c4;
  --green:     #3f9d5e;
  --green-dk:  #2f7a48;
  --blue:      #5b9fd6;
  --amber:     #e8a23b;
  --focus-ring: rgba(11,138,168,.16);
  --accent-tint: rgba(11,138,168,.12);   /* soft accent wash (menu hover) */
  /* SEMANTIC ERROR, distinct from the accent. Invalid fields and an offline
     backend must read as "wrong", and in Light/Dark the accent is teal, so
     painting them with it says "active/selected" instead. --error is the
     border/fill strength (needs 3:1 against its surface, WCAG non-text),
     --error-text is the AA text strength (4.5:1), --error-ring is --error at
     35% for the invalid-input glow. Each theme's log console red is now the
     same colour, so there is one error red per theme rather than three. */
  --error:      #b23a22;
  --error-text: #b23a22;   /* 4.91:1 on the worst surface it lands on */
  --error-ring: rgba(178,58,34,.35);
  /* output-log console (warm/light in cream themes, dark only in Dark) */
  --log-bg:#eef0ed; --log-ink:#3a424c; --log-muted:#79838c; --log-btn-bg:#f7f8f6; --log-btn-border:#dcdfda; --log-scroll:#c8ccc5;
  --log-info:#2f6fb0; --log-ok:#2f7a48; --log-error:var(--error-text); --log-warn:#9a6414;
  /* thermal case accents (hot = red, cold = blue) — per-theme so they read
     clearly on that theme's card surface. Independent of the teal --red accent. */
  --hot:        #d34426;   --hot-text:  #b8381e;   --hot-soft:  #fbeae6;
  --cold:       #2f6fb0;   --cold-text: #275f97;   --cold-soft: #e9f1f9;

  /* ============ LIGHT (default) ============ */
  /* surfaces */
  --app-bg:    #f3f4f2;
  --rail-bg:   #ffffff;   /* left tree panel + right rail */
  --toolbar-bg:#ffffff;
  --card-bg:   #ffffff;
  --card-head: #f3f4f2;
  --white:     #ffffff;
  /* borders */
  --border:    #dcdfda;
  --border-2:  #e3e5e0;
  --border-3:  #e9ebe6;
  --border-4:  #eff1ec;
  --border-strong: #c8ccc5;
  /* text */
  --ink:       #0f1620;
  --label:     #2a323c;
  --text-2:    #4b5560;
  --muted:     #79838c;
  --faint:     #9aa3ac;
  /* derived surfaces / states */
  --btn-bg:        #f2f4f1;
  --btn-hover-bg:  #ffffff;
  --btn-disabled-text: #a6afb8;
  --hover-bg:      #e7eae5;
  --dot-faint:     #b6bdb4;
  --active-soft:   #e1f1f5;   /* pale teal tint for active tree leaf */
  --green-soft-bg: #e2f0e6;
  --conn-ok-bg:    #eaf3ec;
  --conn-ok-border:#cfe2d3;
  --conn-bad-bg:   #fbece8;
  --conn-bad-border:#f0cabf;
  --conn-unknown-bg:#eef1ec;
  --beta-border:   #a9d9e5;
  --scroll-thumb:  #c8ccc5;
  --scroll-thumb-hover:#adb4ad;
  --sk-base:       #e7eae5;
  --sk-highlight:  #f2f4f1;
  --sk-note-overlay: rgba(255,255,255,.6);
  --warn-text:     #9a6414;
  --warn-bg:       #fdefd6;
  --warn-mark:     #e0a94e;
  --arrow-stroke:  %2379838c;
}

/* ================= DARK ================= */
html[data-theme="dark"] {
  --red:       #29b6d4;   /* teal-cyan accent (desaturated from safety-orange for cross-theme cohesion) */
  --red-text:  #29b6d4;   /* light-on-dark already clears AA everywhere (6.3:1 worst) — no darkening needed */
  --red-hover: #45c6e0;
  --red-border:#1f9fbb;
  --red-edge:  #5ad2ea;
  --green:     #4bb06a;
  --green-dk:  #6fce8c;
  --blue:      #6bb0e0;
  --amber:     #eab24f;
  --focus-ring: rgba(41,182,212,.26);
  --accent-tint: rgba(41,182,212,.22);
  --error:      #e8745a;
  --error-text: #e8745a;   /* 5.14:1 worst — light-on-dark needs no darkening */
  --error-ring: rgba(232,116,90,.35);
  --log-bg:#0d141c; --log-ink:#c3ccd5; --log-muted:#6b7680; --log-btn-bg:#172029; --log-btn-border:#2b343e; --log-scroll:#2b343e;
  --log-info:#6bb0e0; --log-ok:#4bb06a; --log-error:var(--error-text); --log-warn:#e8c36b;
  --hot:        #ff7043;   --hot-text:  #ff8a63;   --hot-soft:  #2a1713;
  --cold:       #5aa6e0;   --cold-text: #7bbdf0;   --cold-soft: #142434;

  --app-bg:    #101418;
  --rail-bg:   #141922;
  --toolbar-bg:#141922;
  --card-bg:   #181d22;
  --card-head: #141922;
  --white:     #1f252b;
  --border:    #262c33;
  --border-2:  #2a313a;
  --border-3:  #222831;
  --border-4:  #1d232a;
  --border-strong: #363e46;
  --ink:       #f2f1ee;
  --label:     #d6d5d0;
  --text-2:    #a8aaa5;
  --muted:     #808580;
  --faint:     #626863;
  --btn-bg:        #1c222a;
  --btn-hover-bg:  #232a32;
  --btn-disabled-text: #5c6169;
  --hover-bg:      #20262e;
  --dot-faint:     #4a5158;
  --active-soft:   #13282e;   /* cool teal-tinted dark — matches the dark theme's teal accent */
  --green-soft-bg: #1c3327;
  --conn-ok-bg:    #16281d;
  --conn-ok-border:#2c4a37;
  --conn-bad-bg:   #2a1714;
  --conn-bad-border:#4a2820;
  --conn-unknown-bg:#181d22;
  --beta-border:   #1f4a55;   /* cool teal border — matches the dark theme's teal accent text */
  --scroll-thumb:  #313842;
  --scroll-thumb-hover:#434b55;
  --sk-base:       #1a2027;
  --sk-highlight:  #232a32;
  --sk-note-overlay: rgba(16,20,24,.55);
  --warn-text:     #e0a94e;
  --warn-bg:       #332512;
  --warn-mark:     #d39a4f;
  --arrow-stroke:  %23a8aaa5;
}

/* The light theme used to re-scope the right-hand rail to a dark navy contrast
   panel. That was removed once the previews inside it became theme-aware: a
   pale viewport sitting in a black rail was the one place the light theme still
   went dark, and it forced every scene colour to be a mid-tone compromise that
   had to stay legible on both surfaces. The rail now just follows the page
   theme like every other panel, so --rail-bg / --card-bg / --ink come straight
   from :root and the scene palette is free to use its natural values.

   One nuance: in the light theme --rail-bg and --card-bg are both #ffffff, so
   the legend and readout cards would disappear into the panel. Dark already
   seats the rail a step below its cards; this does the same for light, and
   only for the right-hand rail — the left tree panel shares --rail-bg but has
   no cards on it, so it stays white. */
html[data-theme="light"] .rail-panel,
html:not([data-theme]) .rail-panel { --rail-bg: #f3f4f2; }

* { box-sizing: border-box; }
html, body {
  margin: 0; padding: 0; height: 100%;
  background: var(--app-bg);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  font-feature-settings: "cv05", "cv11";   /* Inter: open l/a alternates read cleaner at UI sizes */
}
/* Engineering data must column-align: tabular figures on every numeric surface. */
input, select, .ro-value, .metric-value, .pass-table, .gt-readout, .tf-row,
.adcs-metrics, .about-line, table { font-variant-numeric: tabular-nums; }
/* Mobile viewport height, in three layers.

   100vh is the LARGEST viewport on mobile browsers, so at 100vh the output-log
   footer sits underneath the address bar. 100dvh tracks the visible area.

   But dvh on <body> alone is not enough, and this is the subtle part: a
   PERCENTAGE height resolves against the initial containing block, which Chrome
   on Android sizes to the LARGE viewport (address bar hidden). So
   `html { height: 100% }` stands taller than a 100dvh body, and the difference
   paints as bare app background below the console — the strip of dead space
   reported on a Galaxy S25. The two boxes were being measured against two
   different viewports.

   Layer three is the reliable one: app.js measures window.innerHeight into
   --app-h and adds .js-vh. innerHeight tracks the address bar but, unlike
   visualViewport.height, is NOT shrunk by the on-screen keyboard — so the shell
   does not collapse while you type into a field. The class gates the rule so
   var(--app-h) can never resolve to nothing. */
html { overflow: hidden; }
body { height: 100vh; height: 100dvh; overflow: hidden; }
html.js-vh, html.js-vh body { height: var(--app-h); }
::selection { background: var(--red); color: #fff; }
input:focus, select:focus { outline: none; }

/* Keyboard focus: a clear ring for users navigating with Tab (mouse clicks stay
   ring-free via :focus-visible). Inputs keep their existing box-shadow ring too. */
a:focus-visible, button:focus-visible, select:focus-visible,
input:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--red); outline-offset: 2px;
}

/* Respect the OS "reduce motion" setting: collapse spinners, shimmer, pulses and
   transitions so motion-sensitive users get a calm, static interface. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-duration: .001ms !important; scroll-behavior: auto !important;
  }
}

/* custom scrollbars */
.cmat-scroll::-webkit-scrollbar, .main-panel::-webkit-scrollbar,
.tree-panel::-webkit-scrollbar, .rail-panel::-webkit-scrollbar { width: 10px; height: 10px; }
.main-panel::-webkit-scrollbar-thumb, .tree-panel::-webkit-scrollbar-thumb,
.rail-panel::-webkit-scrollbar-thumb {
  background: var(--scroll-thumb); border: 3px solid transparent; background-clip: padding-box; border-radius: 8px;
}
.main-panel::-webkit-scrollbar-thumb:hover, .tree-panel::-webkit-scrollbar-thumb:hover,
.rail-panel::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); border: 3px solid transparent; background-clip: padding-box; }
.log-body::-webkit-scrollbar { width: 8px; }
.log-body::-webkit-scrollbar-thumb { background: var(--log-scroll); border-radius: 8px; }

@keyframes cmatPulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }

/* ===== Root workspace grid =====
   Rows are addressed BY NAME. Do not go back to a positional template.

   The history is worth keeping, because this trap caught the layout twice from
   opposite directions. It was `auto 1fr auto` for five children, which assumed
   the stale banner was display:none; the moment the banner appeared it took the
   1fr and the workspace collapsed. That was fixed by padding the template out to
   five rows, which assumed the opposite: with the banner hidden a display:none
   item claims no row at all, so every row below shifted up one, the
   minmax(0, 1fr) landed on the run strip, and the workspace stopped at its
   content height. The mission tree stopped with it, leaving a band of bare page
   between the tree and the output log the whole time no run was stale.

   Naming the areas makes the mapping independent of which children exist. An
   absent item leaves its row at auto, which is 0px. A child added later without
   an area lands in an implicit row instead of silently taking the flexible one.

   minmax(0, 1fr), not 1fr, so the workspace may be SMALLER than its content:
   that is what lets the inner panes own their scrolling instead of pushing the
   log off-screen. */
body {
  display: grid;
  grid-template-areas: "toolbar" "stale" "workspace" "runstrip" "log";
  grid-template-rows: auto auto minmax(0, 1fr) auto auto;
}
.toolbar      { grid-area: toolbar; }
.stale-banner { grid-area: stale; }
.app-body     { grid-area: workspace; }
.run-strip    { grid-area: runstrip; }
.output-log   { grid-area: log; }

/* ===== Toolbar ===== */
.toolbar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  background: var(--toolbar-bg);
  border-bottom: 1px solid var(--border);
  padding: 3px 22px; z-index: 5;
}
/* Two-row toolbar: row 1 = brand + account (full width), row 2 = menus + actions. */
.toolbar-left {
  display: flex; align-items: center; gap: 16px; min-width: 0;
  flex: 1 0 100%; padding: 5px 0; border-bottom: 1px solid var(--border);
}
.toolbar-left .cmat-user-chip { margin-left: auto; }

/* Mission-tree drawer toggle. Only rendered below 881px (see the responsive
   section at the end of this file); on desktop the tree pane is always visible so
   the button would be noise. */
.nav-toggle {
  display: none; align-items: center; justify-content: center;
  width: 34px; height: 34px; flex-shrink: 0;
  background: var(--btn-bg); color: var(--ink);
  border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
}
.nav-toggle svg { width: 18px; height: 18px; }
.nav-toggle:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }

/* Dimmer behind the drawer. Always in the DOM so it can transition; inert until
   the drawer opens. */
.nav-scrim {
  position: fixed; inset: 0; z-index: 1400;
  background: rgba(0, 0, 0, .45);
  opacity: 0; pointer-events: none; transition: opacity .18s ease;
}
body.nav-open .nav-scrim { opacity: 1; pointer-events: auto; }
.menubar, .toolbar-actions { padding: 5px 0; }
.logo-mark {
  width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.logo-mark img { width: 34px; height: 34px; display: block; object-fit: contain; }
/* theme-aware orbit-and-probe mark: light/cream variant by default, dark variant in Dark theme */
.logo-mark svg { width: 34px; height: 34px; display: block; }
.logo-mark .logo-dark { display: none; }
html[data-theme="dark"] .logo-mark .logo-light { display: none; }
html[data-theme="dark"] .logo-mark .logo-dark { display: block; }
.app-title { font-weight: 800; font-size: 19px; letter-spacing: .5px; }
.beta-pill {
  font-size: 10px; font-weight: 700; letter-spacing: 1px;
  color: var(--red-text); border: 1px solid var(--beta-border);
  border-radius: 4px; padding: 2px 6px; text-transform: uppercase;
}
.toolbar-divider { width: 1px; height: 22px; background: var(--border); }
.app-subtitle {
  font-family: var(--sans); font-size: 15px;
  color: var(--muted); white-space: nowrap;
}
.toolbar-actions { display: flex; align-items: center; gap: 10px; }

/* ===== Theme slider (Light / Dark) ===== */
.theme-switch {
  position: relative; display: inline-flex; align-items: stretch;
  background: var(--btn-bg); border: 1px solid var(--border);
  border-radius: 8px; padding: 3px;
}
.theme-switch-thumb {
  position: absolute; top: 3px; left: 3px; height: calc(100% - 6px); width: 0;
  background: var(--card-bg); border: 1px solid var(--border-2);
  border-radius: 6px; box-shadow: 0 1px 2px rgba(0,0,0,.10);
  transition: transform .22s cubic-bezier(.4,.1,.2,1), width .22s cubic-bezier(.4,.1,.2,1);
  pointer-events: none; z-index: 0;
}
.theme-opt {
  position: relative; z-index: 1; border: none; background: transparent;
  font-family: var(--sans); font-size: 11.5px; font-weight: 600; letter-spacing: .01em;
  color: var(--muted); padding: 5px 12px; border-radius: 6px; cursor: pointer;
  white-space: nowrap; transition: color .18s ease;
}
.theme-opt:hover:not(.is-active) { color: var(--label); }
.theme-opt.is-active { color: var(--ink); }
.theme-opt:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }
@media (max-width: 1180px) { .theme-opt { padding: 5px 9px; font-size: 11px; } }

.btn {
  font-family: var(--sans); font-size: 12.5px; font-weight: 600;
  color: var(--label); background: var(--btn-bg); border: 1px solid var(--border);
  border-radius: 7px; padding: 8px 14px; cursor: pointer;
  transition: all .15s ease; display: inline-flex; align-items: center; gap: 8px;
}
.btn-ghost:hover:not(:disabled) { background: var(--btn-hover-bg); border-color: var(--border-strong); }
.btn:disabled { color: var(--btn-disabled-text); background: var(--toolbar-bg); border-color: var(--border-2); cursor: not-allowed; }
.btn-primary {
  color: #fff; background: var(--red); border-color: var(--red-border);
  font-weight: 700; padding: 8px 16px; box-shadow: 0 1px 3px rgba(210,63,28,.3);
}
.btn-primary:hover:not(:disabled) { background: var(--red-hover); }
.btn-primary svg { margin-right: -2px; }
/* inline spinner shown on the Run button while an analysis is in flight */
.btn-spin {
  width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0;
  border: 2px solid rgba(255,255,255,.4); border-top-color: #fff;
  animation: spin .7s linear infinite;
}
.btn-primary:disabled .btn-spin { border-color: rgba(255,255,255,.35); border-top-color: rgba(255,255,255,.85); }
/* inline action icons (replace the old emoji glyphs); inherit button text color */
.ico { width: 13px; height: 13px; display: inline-block; vertical-align: -2px; flex-shrink: 0; }
.gt-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
.mini-btn, .log-btn { display: inline-flex; align-items: center; gap: 6px; }
#staleBanner .ico { width: 14px; height: 14px; vertical-align: -3px; margin-right: 5px; }

.conn-status {
  font-size: 12px; font-weight: 600; letter-spacing: .01em;
  border-radius: 7px; padding: 7px 13px; display: inline-flex; align-items: center; gap: 6px;
}
.conn-ok { color: var(--green-dk); background: var(--conn-ok-bg); border: 1px solid var(--conn-ok-border); }
.conn-bad { color: var(--error-text); background: var(--conn-bad-bg); border: 1px solid var(--conn-bad-border); }
.conn-unknown { color: var(--muted); background: var(--conn-unknown-bg); border: 1px solid var(--border-2); }
.conn-status::before { content: "●"; font-size: 9px; }
/* Two labels; the responsive block swaps which one shows. */
.conn-status .conn-short { display: none; }
.conn-ok::before { color: var(--green); }

/* ===== Body: 3 panes ===== */
.app-body {
  display: grid; grid-template-columns: 266px 1fr 396px;
  min-height: 0; background: var(--app-bg); overflow: hidden;
}
/* Sections with no directly-relevant rail content (see NO_RAIL_RESULTS in
   app.js) drop the right column entirely and let the results breathe. */
.app-body.no-rail { grid-template-columns: 266px 1fr; }
.app-body.no-rail .rail-panel { display: none; }
.app-body.no-rail .inspector-inner { max-width: 1100px; }

/* ===== Tree nav (left) ===== */
.tree-panel {
  background: var(--rail-bg); border-right: 1px solid var(--border);
  overflow-y: auto; display: flex; flex-direction: column;
}
.tree-header {
  font-size: 10.5px; letter-spacing: 2px; color: var(--faint);
  padding: 18px 18px 10px; font-weight: 700; text-transform: uppercase;
}
.tree { list-style: none; margin: 0; padding: 2px 12px; flex: 1; }
.tree > li { margin-bottom: 3px; }
.tree-root {
  padding: 9px 10px; cursor: pointer; font-size: 13.5px; font-weight: 600;
  color: var(--ink); display: flex; align-items: center; gap: 9px; border-radius: 7px;
}
.tree-root:hover { background: var(--hover-bg); }
/* disclosure chevron: points right when collapsed, rotates down when expanded */
.tree-root::before {
  content: ""; width: 9px; height: 9px; flex-shrink: 0; background-color: var(--faint);
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><path d='M9 6l6 6-6 6'/></svg>") center / contain no-repeat;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><path d='M9 6l6 6-6 6'/></svg>") center / contain no-repeat;
  transition: transform .16s ease;
}
.tree-root.expanded::before { transform: rotate(90deg); }
.tree-root:hover::before { background-color: var(--muted); }
.tree-name { flex: 1; }
/* category icon in the tree nav — muted, brightens with the row on hover/active */
.tree-ico { display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px; color: var(--faint); flex-shrink: 0; }
.tree-ico svg { width: 15px; height: 15px; }
.tree-root:hover .tree-ico, .tree-root.expanded .tree-ico { color: var(--muted); }
/* keyboard focus ring for tree items (they're divs/li, not native controls).
   ORDER IS LOAD-BEARING. These two selectors have identical specificity (0,2,0),
   and :focus matches whenever :focus-visible does, so whichever comes LAST wins.
   Written the other way round, `outline: none` silently killed the ring in the
   exact case it exists for, and the tree is the app's only navigation: a
   keyboard user had no indicator at all. Measured with a real Tab press before
   the swap: outline-width 0px, no box-shadow, no background change. */
.tree-root:focus, .tree-leaf:focus { outline: none; }
.tree-root:focus-visible, .tree-leaf:focus-visible { outline: 2px solid var(--red); outline-offset: -2px; }
.tree-status {
  font-size: 9px; font-weight: 700; letter-spacing: .5px;
  padding: 2px 6px; border-radius: 4px;
}
.st-idle { color: var(--faint); background: var(--hover-bg); }
.st-set  { color: var(--green-dk); background: var(--green-soft-bg); }

.tree-children {
  list-style: none; margin: 2px 0 8px 19px; padding-left: 13px;
  border-left: 1px solid var(--border); display: none;
}
.tree-root.expanded + .tree-children { display: block; }
.tree-leaf {
  position: relative; padding: 7px 10px; font-size: 12.5px; color: var(--text-2);
  cursor: pointer; display: flex; align-items: center; gap: 8px; border-radius: 6px; font-weight: 500;
}
.tree-leaf::before {
  content: ""; position: absolute; left: -13px; width: 9px; height: 1px; background: var(--border);
}
.tree-leaf::after {
  content: ""; width: 4px; height: 4px; border-radius: 50%; background: var(--dot-faint); flex-shrink: 0;
  order: -1;
}
.tree-leaf:hover { background: var(--hover-bg); color: var(--ink); }
.tree-leaf.active { background: var(--active-soft); color: var(--red-text); font-weight: 700; }
.tree-leaf.active::after { background: var(--red); }
.tree-divider { height: 1px; background: var(--border); margin: 10px 6px; list-style: none; }

.tree-footer {
  padding: 14px 20px; border-top: 1px solid var(--border);
  display: flex; flex-direction: column; gap: 5px;
}
.tree-footer .tf-row { display: flex; justify-content: space-between; font-size: 11px; color: var(--faint); }
.tree-footer .tf-row span:last-child { color: var(--muted); }

/* ===== Inspector (center) ===== */
/* scrollbar-gutter keeps the content width identical whether or not the section
   is tall enough to scroll, so nothing shifts when a scrollbar appears. */
.main-panel { overflow-y: auto; min-width: 0; background: var(--app-bg); scrollbar-gutter: stable; }
/* Left-anchored, NOT centered (margin: 0, not auto): sections have two widths
   (760px beside the rail, 1100px when a results view drops it), and centering
   each made the text's left edge jump sideways on every tree change. Anchoring
   to the left padding gives every branch the same start position. */
.inspector-inner { max-width: 760px; margin: 0; padding: 30px 44px 60px; }
.panel-section { display: none; }
.panel-section.active { display: block; }

.breadcrumb {
  display: flex; align-items: center; gap: 8px; font-size: 12px;
  color: var(--faint); margin-bottom: 18px;
}
.breadcrumb .crumb-sep { color: var(--border-strong); }
.breadcrumb .crumb-current { color: var(--red-text); font-weight: 600; }

.panel-section h2 {
  margin: 0; font-size: clamp(20px, 4.6vw, 27px); font-weight: 700; letter-spacing: -0.02em; line-height: 1.12; color: var(--ink);
}
.panel-section h2::after {
  content: ""; display: block; width: 46px; height: 3px; background: var(--red);
  border-radius: 2px; margin: 14px 0 16px;
}
/* Section intros run the full width of the content they describe (viewport,
   charts, cards) so text and visual always share one edge — and justify, so
   both edges of the paragraph are flush with the content below. */
.section-intro {
  font-family: var(--sans); font-size: 15px; line-height: 1.6;
  color: var(--text-2); margin: 0 0 28px; max-width: none;
  text-align: justify;
}
.rail-caption, .about-note { text-align: justify; }

/* Zoom hint shown as its own caption beneath a chart (under the x-axis label),
   not baked into the axis title text. */
.chart-hint {
  font-family: var(--sans);
  font-size: 11px; color: var(--muted); text-align: center;
  margin: 8px 0 0; letter-spacing: 0.2px;
}
/* Keep the DOI range-sliders visually plain: hide the miniature chart preview
   Plotly draws inside the slider so only the flat brand-tinted strip remains. */
.rangeslider-rangeplot .scatterlayer,
.rangeslider-rangeplot .barlayer,
.rangeslider-rangeplot .gridlayer,
.rangeslider-rangeplot .zerolinelayer,
.rangeslider-rangeplot .maplayer { display: none !important; }

/* ===== Panel cards ===== */
.panel-card {
  background: var(--card-bg); border: 1px solid var(--border-2);
  border-radius: 12px; overflow: hidden; margin-bottom: 22px;
}
.panel-card-head {
  display: flex; align-items: center; gap: 9px; padding: 13px 20px;
  border-bottom: 1px solid var(--border-3); background: var(--card-head);
  font-size: 11px; font-weight: 700; letter-spacing: 1.5px; color: var(--muted);
}
.panel-card-head.between { justify-content: space-between; }
.panel-card-head.between > span:first-child { display: flex; align-items: center; gap: 9px; }
.panel-card-head .head-meta { font-size: 11px; font-weight: 400; letter-spacing: 0; color: var(--faint); }
.dot-red { width: 5px; height: 5px; border-radius: 50%; background: var(--red); display: inline-block; flex-shrink: 0; }
.panel-card-body { padding: 20px; }

.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 22px 24px; }
.field { display: flex; flex-direction: column; gap: 2px; }
.field-label { display: block; font-size: 13px; font-weight: 600; color: var(--label); margin-bottom: 2px; }
/* contextual help "?" badge next to input labels, with a hover/focus tooltip */
.help-tip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 14px; height: 14px; margin-left: 5px; vertical-align: -2px;
  border: 1px solid var(--border-strong); border-radius: 50%;
  font-size: 9.5px; font-weight: 700; line-height: 1; color: var(--muted);
  background: var(--btn-bg); cursor: help; position: relative; user-select: none;
  text-transform: none; letter-spacing: 0;
}
.help-tip:hover, .help-tip:focus-visible { color: var(--red-text); border-color: var(--red); }
/* Hover may recolour and stop there; KEYBOARD focus may not. A 1px border that
   changes colour is the same cue twice and no cue at all to anyone who cannot
   separate the two hues, which is WCAG 1.4.1 and 2.4.11. The badge is 14px, so
   the ring sits outside it rather than inside. */
.help-tip:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
/* Single body-level tooltip positioned by JS (help-tooltip.js logic in app.js),
   so it escapes the scrolling panels and is clamped to the viewport — never clipped. */
.help-tooltip {
  position: fixed; z-index: 4000; max-width: 260px; width: max-content;
  background: var(--ink); color: var(--app-bg);
  font-family: var(--sans); font-size: 11px; font-weight: 500; line-height: 1.45;
  letter-spacing: .01em; text-align: left; padding: 8px 11px; border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0,0,0,.28); pointer-events: none;
  opacity: 0; visibility: hidden; transition: opacity .12s ease;
}
.help-tooltip.show { opacity: 1; visibility: visible; }
/* Caption line above each chart. Charts had no title element at all, so there
   was nothing naming them and nothing to hang a help badge on. Inserted before
   the .chart-box by wireChartHelp so Plotly's redraws cannot wipe it. */
.chart-cap {
  display: flex; align-items: center; gap: 2px;
  font-family: var(--sans); font-size: 11px; font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase;
  /* No left offset: the caption shares the section's left edge with h2, the
     intro and the chart frame. Vertically it belongs to the chart BELOW it:
     clear space above (away from the previous chart) and a tight 8px to its
     own chart. Stacked charts rely on this instead of per-box inline margins. */
  color: var(--muted); margin: 24px 0 8px;
}
/* Output badges sit on labels that are themselves small, so the badge follows
   the text size down rather than looming over it. */
.metric-label .help-tip, .adcs-metric .help-tip, th .help-tip, .chart-cap .help-tip,
.case-card .row .help-tip {
  width: 12px; height: 12px; font-size: 8.5px; margin-left: 4px; vertical-align: -1px;
}
/* A table header is often right-aligned and tight; keep the badge from forcing a
   wrap that would change the column width. */
th .help-tip { flex: none; }
.field-sub { display: block; font-size: 11px; color: var(--faint); margin-bottom: 8px; }
.field-input { position: relative; display: flex; align-items: center; margin-top: auto; }
.field-input input, .field-input select {
  width: 100%; font-family: var(--sans); font-size: 14px; font-weight: 500;
  padding: 11px 13px; border: 1px solid var(--border); border-radius: 8px;
  background-color: var(--white); color: var(--ink);
}
.field-input.has-unit input { padding-right: 44px; }
/* A select has to keep the 32px of right padding the base `select` rule below
   reserves for the chevron it draws at "right 14px center". The shared input
   padding above is more specific and was overriding it to 13px, so the arrow
   was painted UNDER the option text and vanished the moment a label was long
   enough to reach it. Every other dropdown escaped only because its longest
   option happened to be short; PV Extraction's is not. */
.field-input select { padding-right: 32px; }
.field-input .unit {
  position: absolute; right: 13px; font-size: 11px; color: var(--faint);
  pointer-events: none; letter-spacing: .03em;
}
input:focus, select:focus {
  border-color: var(--red); box-shadow: 0 0 0 3px var(--focus-ring);
}
select {
  appearance: none; -webkit-appearance: none; cursor: pointer; padding-right: 32px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='13' height='13' viewBox='0 0 24 24' fill='none' stroke='%237a8592' stroke-width='2.5' stroke-linecap='round'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat; background-position: right 14px center;
}
html[data-theme="dark"] select {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='13' height='13' viewBox='0 0 24 24' fill='none' stroke='%239aa6b2' stroke-width='2.5' stroke-linecap='round'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat; background-position: right 14px center;
}

/* form-factor panel */
.form-factor-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: start; }
.envelope-spec { border-left: 1px solid var(--border-3); padding-left: 24px; }
.spec-title { font-size: 11px; font-weight: 700; letter-spacing: 1px; color: var(--faint); }
.spec-row { display: flex; justify-content: space-between; font-size: 13px; margin-top: 10px; }
.spec-row span:first-child { color: var(--muted); }
.spec-row span:last-child { font-weight: 600; }

/* ===== Face table ===== */
.face-table {
  border-collapse: separate; border-spacing: 0; width: 100%;
  background: transparent; font-family: var(--sans);
}
.face-table-flush { border: none; }
.face-table th, .face-table td {
  padding: 10px 20px; text-align: left; font-size: 13px;
  border-bottom: 1px solid var(--border-4);
}
.face-table th {
  background: transparent; font-weight: 700; letter-spacing: 1px; text-transform: uppercase;
  font-size: 10.5px; color: var(--faint); border-bottom: 1px solid var(--border-3); padding: 11px 20px;
}
/* alpha and epsilon are lowercase by convention, and CSS uppercasing a Greek
   lowercase letter turns it into its CAPITAL: (α) and (ε) were rendering as
   (Α) and (Ε), which are indistinguishable from a Latin A and E. On a screen
   about radiative properties that is the wrong symbol, not a styling nit. The
   header keeps its small-caps look; only the symbol opts out. */
.face-table th .sym { text-transform: none; }
.face-table td:first-child { font-weight: 700; display: flex; align-items: center; gap: 7px; }
.face-chip { width: 7px; height: 7px; border-radius: 2px; display: inline-block; flex-shrink: 0; }
.face-table input {
  width: 100%; max-width: 160px; font-family: var(--sans); font-size: 13.5px;
  padding: 9px 12px; border: 1px solid var(--border-2); border-radius: 7px; background: var(--white);
  color: var(--ink);
}
.face-table tfoot .face-total td {
  background: var(--card-head); font-weight: 700; border-bottom: none; padding: 13px 20px;
}
.face-table tfoot .face-total td:first-child { color: var(--muted); display: table-cell; }

/* Per-face surface type. The select takes the same shape as the number inputs
   beside it, so the row reads as one strip of controls.
   background-COLOR, not the shorthand: `background: var(--white)` also resets
   background-image, which silently threw away the chevron the base `select`
   rule draws. These six then looked exactly like text inputs, with no hint
   that they open. The right padding clears the arrow the same way the form
   selects do. */
.face-table select.face-type {
  width: 100%; min-width: 176px; max-width: 252px; font-family: var(--sans); font-size: 13px;
  padding: 9px 30px 9px 10px; border: 1px solid var(--border-2); border-radius: 7px;
  background-color: var(--white); color: var(--ink);
}
/* 252px is the longest option ("Panel + solar cells + camera lens", 205px at
   13px Inter) plus the arrow. Raising the cap alone was not enough: a table
   column is sized from its content, and auto layout spreads the spare width
   across all five columns, so the select stopped at 206px and the option was
   still truncated. The percentage width below makes THIS column the one that
   absorbs the surplus, so it reaches its 252px cap whenever the card can
   afford it and still falls back to 176px when the card is narrow, rather
   than forcing a horizontal scrollbar under an input table on the desktop.
   The rest of the space is bought back from the neighbours: the area box was
   160px for a four-character number, the cells box 104px for a percentage,
   and this table's cells carried 40px of horizontal padding each. */
.face-table-flush td:nth-child(3) { width: 100%; }
.face-table-flush th, .face-table-flush td { padding-left: 12px; padding-right: 12px; }
/* The min-widths are not optional. The percentage width above hands the surplus
   to the surface column, which squeezes these two back to their min-content,
   and a number input CLIPS its value rather than shrinking it: without these,
   the area box showed "0" for 0.01 and the cells box showed nothing at all.
   Measured on the ORIGINAL build too, where both were already clipping at
   1200 and 1300px; this fixes that as well. */
.face-table-flush td:nth-child(2) input { min-width: 86px; max-width: 108px; }
.face-table-flush td:nth-child(4) input.face-cellpct { min-width: 62px; max-width: 76px; }
.face-table input.face-cellpct { max-width: 104px; }
/* A face with no cells has nothing to say about coverage. Dimmed rather than
   emptied, so the column keeps its width and the rows do not jump as the type
   changes. */
.face-table td.is-off { opacity: .38; }
.face-table td.is-off input { pointer-events: none; }
/* Cell area is derived from the two controls to its left, so it is set as a
   value rather than as another box someone might try to type into. */
.face-table td.face-derived { font-variant-numeric: tabular-nums; color: var(--muted); }

/* ============================================================
   LIVE-PREVIEW VIEWPORT PALETTE  (--vp-*)
   ------------------------------------------------------------
   The rail previews (orbit viewport, spacecraft cube, station map,
   mission timeline) used to be hard-coded #080808 in every theme, so a
   black slab sat inside a cream or navy rail. These tokens make them
   follow the theme instead.

   They are declared on <html>, NOT inside .rail-panel, on purpose: the
   light theme re-scopes the rail's surface tokens to dark navy, and the
   previews must keep following the PAGE theme rather than inheriting
   that override.

   Two groups:
     - surface     (--vp-bg / grid / border / label / foot / glow / shadow)
     - scene ink   (--vp-orbit / sat / node / ... / ax-x|y|z)
   The scene-ink values are read back in JS (getComputedStyle) to colour
   the three.js orbit scene and the CSS orbit triad, and are reused by the
   legend swatches in index.html — one source of truth, so the legend can
   never drift from what is drawn.

   Axis convention: X red, Y green, Z blue, desaturated toward
   instrumentation rather than primary colours. This is what GMAT, STK and
   CAD packages use, so the triad reads correctly to an engineer without
   consulting the legend. It applies to BOTH the ECI arrows in the orbit
   viewport (vernal equinox is ECI +X, so it takes the X colour) and the
   LVLH triad on the spacecraft cube.

   Because the axes claim red, the orbit path moved to violet — the one
   strong hue left that clashes with neither the RGB triad nor the amber
   satellite. The equatorial plane is a light blue, deliberately lighter
   than the Z axis so a translucent plane and a solid axis never read as
   the same element.
   ============================================================ */
:root {
  --vp-bg:        #eaeff5;
  --vp-bg-top:    #fbfdff;
  --vp-grid:      rgba(38,64,94,.075);
  --vp-vignette:  rgba(38,64,94,.13);
  --vp-border:    #c6d1de;
  /* Floating chip over the scene (the playback dock). Near-opaque on purpose:
     it has to hold its own over the pale sky AND over the Earth it half covers.
     12.6:1 over the canvas, 11.5:1 over the globe. */
  --vp-chip-bg:   rgba(247,250,253,.92);
  --vp-chip-ink:  #21313f;
  --vp-chip-hover:rgba(33,49,63,.08);
  --vp-label:     #6a7a8c;
  --vp-hint:      #93a2b2;
  --vp-foot:      #55647a;
  --vp-ink:       #21313f;                /* 3-D text sprites */
  --vp-halo:      rgba(255,255,255,.92);  /* halo behind sprites / labels */
  --vp-shadow:    rgba(30,50,75,.20);     /* contact shadow under the cube */
  --vp-glow:      rgba(120,160,225,.15);  /* ambient backglow */
  --vp-stars:     0;                      /* 1 = draw the starfield */
  --vp-path:      rgba(58,84,120,.55);    /* dashed orbit ring (cube view) */
  /* scene ink */
  --vp-ax-x:      #b8453a;                /* ECI +X / velocity  */
  --vp-ax-y:      #4f8f3f;                /* ECI +Y             */
  --vp-ax-z:      #2d6cad;                /* ECI +Z / nadir     */
  --vp-eq:        #5b93d6;                /* equatorial plane   */
  --vp-orbit:     #d61b8f;               /* hot magenta — pops against the blue planet and clashes with no axis/marker */
  --vp-orbit-glow:#ef6fc0;
  --vp-sat:       #c08a10;
  --vp-vernal:    #b8453a;                /* vernal equinox IS ECI +X */
  --vp-node:      #3fa8a2;                /* line of nodes  */
  --vp-nodemk:    #0f8f8a;                /* node markers   */
  --vp-peri:      #b5487f;
  --vp-apo:       #c9741f;
}
html[data-theme="dark"] {
  --vp-bg:        #0b0f14;
  --vp-bg-top:    #131b24;
  --vp-grid:      rgba(255,255,255,.05);
  --vp-vignette:  rgba(0,0,0,.45);
  --vp-border:    #242c35;
  --vp-chip-bg:   rgba(17,24,32,.86);
  --vp-chip-ink:  #dbe6f1;
  --vp-chip-hover:rgba(219,230,241,.10);
  --vp-label:     #74818e;
  --vp-hint:      #616d79;
  --vp-foot:      #8592a0;
  --vp-ink:       #dbe6f1;
  --vp-halo:      rgba(0,0,0,.62);
  --vp-shadow:    rgba(0,0,0,.6);
  --vp-glow:      rgba(96,128,196,.17);
  --vp-stars:     1;
  --vp-path:      rgba(205,220,242,.70);
  --vp-ax-x:      #e8695c;
  --vp-ax-y:      #6fc25a;
  --vp-ax-z:      #5a9fe0;
  --vp-eq:        #7cb4ec;
  --vp-orbit:     #ff3db5;               /* hot magenta ribbon — vivid over the real Earth and the dark sky */
  --vp-orbit-glow:#ff8ad4;
  --vp-sat:       #f5c451;
  --vp-vernal:    #e8695c;
  --vp-node:      #5fd8d0;
  --vp-nodemk:    #38e1d6;
  --vp-peri:      #f08cc0;
  --vp-apo:       #ffa85c;
}

/* ===== Right rail (live preview) ===== */
.rail-panel {
  background: var(--rail-bg); border-left: 1px solid var(--border);
  overflow-y: auto; padding: 20px;
}
.rail-label, .rail-readout-label {
  font-size: 10.5px; font-weight: 700; letter-spacing: 2px; color: var(--faint);
  margin-bottom: 13px;
}
.rail-readout-label { margin-top: 22px; margin-bottom: 11px; }
.rail-caption {
  font-family: var(--sans); font-size: 13.5px; line-height: 1.5;
  color: var(--muted); margin: 13px 0 0;   /* flush with the rail label above */
}

/* Shared preview-surface recipe: theme-tinted base, soft top light, faint
   graticule and an inset vignette that seats the content in the frame. */
.mission-timeline-box, .gs-map-box, .viewport, .cube-viewer {
  background-color: var(--vp-bg);
  background-image:
    radial-gradient(125% 92% at 50% 0%, var(--vp-bg-top), transparent 62%),
    linear-gradient(var(--vp-grid) 1px, transparent 1px),
    linear-gradient(90deg, var(--vp-grid) 1px, transparent 1px);
  background-size: auto, 26px 26px, 26px 26px;
  border: 1px solid var(--vp-border);
  box-shadow: inset 0 0 46px var(--vp-vignette);
}

/* Mission timeline (rail, Mission section) */
.mission-timeline-box {
  position: relative; min-height: 320px; border-radius: 12px; overflow: hidden;
  padding: 40px 16px 20px 34px;
}
.mission-timeline { position: relative; }
.mission-timeline::before {
  content: ""; position: absolute; left: 4px; top: 4px; bottom: 4px; width: 2px;
  background: linear-gradient(var(--vp-hint), var(--vp-hint) 94%, transparent);
  opacity: .6;
}
.mt-row { position: relative; padding-bottom: 24px; }
.mt-row:last-child { padding-bottom: 0; }
.mt-dot {
  position: absolute; left: -28px; top: 2px; width: 11px; height: 11px; border-radius: 50%;
  background: var(--vp-hint); box-shadow: 0 0 0 3px var(--vp-bg), 0 0 0 4px var(--vp-border);
}
.mt-row.mt-start .mt-dot { background: var(--vp-orbit);  box-shadow: 0 0 0 3px var(--vp-bg), 0 0 0 4px var(--vp-orbit); }
.mt-row.mt-doi   .mt-dot { background: var(--vp-sat);    box-shadow: 0 0 0 3px var(--vp-bg), 0 0 0 4px var(--vp-sat); }
.mt-row.mt-season .mt-dot{ background: var(--vp-nodemk); box-shadow: 0 0 0 3px var(--vp-bg), 0 0 0 4px var(--vp-nodemk); }
.mt-row.mt-end   .mt-dot { background: var(--vp-foot);   box-shadow: 0 0 0 3px var(--vp-bg), 0 0 0 4px var(--vp-hint); }
.mt-label { font-size: 11px; letter-spacing: .04em; color: var(--vp-ink); font-weight: 700; text-transform: uppercase; }
.mt-date  { font-size: 12.5px; color: var(--vp-foot); margin-top: 2px; font-variant-numeric: tabular-nums; }
.mt-date .mt-day { color: var(--vp-hint); margin-left: 8px; }
.mt-empty { color: var(--vp-hint); font-size: 12.5px; text-align: center; padding: 40px 14px; }

/* Ground-station world map (rail, Ground Station section) */
.gs-map-box {
  position: relative; border-radius: 12px; overflow: hidden; aspect-ratio: 3 / 2;
}
#gsMapPlot { position: absolute; inset: 0; }

.viewport {
  position: relative; border-radius: 12px; overflow: hidden; aspect-ratio: 1 / 1.08;
}
.viewport-label {
  position: absolute; top: 13px; left: 15px; z-index: 2;
  font-size: 10.5px; letter-spacing: 1px; color: var(--vp-label); pointer-events: none;
}
.viewport-hint {
  position: absolute; top: 13px; right: 15px; z-index: 2;
  font-size: 9.5px; letter-spacing: .06em; color: var(--vp-hint); pointer-events: none;
}
.viewport-foot {
  position: absolute; bottom: 13px; left: 15px; right: 15px; z-index: 2;
  font-size: 10px; letter-spacing: .04em; color: var(--vp-foot); pointer-events: none;
}
#orbitViewport { width: 100%; height: 100%; }
#orbitViewport canvas { display: block; width: 100% !important; height: 100% !important; cursor: grab; }
#orbitViewport canvas:active { cursor: grabbing; }

/* Orbit viewport HUD chrome (scoped to the orbit box so the station-map's
   shared .viewport-label stays untouched): bracketed title top-left, centred
   "data stream" readout chip along the bottom, mission-graphic style. */
#orbitVpBox .viewport-label {
  border-left: 2px solid var(--vp-label); border-top: 2px solid var(--vp-label);
  padding: 6px 10px 3px; font-weight: 700; letter-spacing: 2.5px;
}

/* ===== Orbit section: hero viewport + input-method toggle ===== */
/* Segmented control choosing between the six elements and the TLE importer.
   Sits centred between the hero viewport and the input cards. */
.orbit-mode-row { display: flex; justify-content: center; margin: 18px 0; }
.orbit-mode-toggle {
  display: inline-flex; gap: 2px; padding: 3px;
  background: var(--btn-bg); border: 1px solid var(--border); border-radius: 9px;
}
.orbit-mode-btn {
  font-family: var(--sans); font-size: 11.5px; font-weight: 700; letter-spacing: .02em;
  color: var(--muted); background: transparent; border: 1px solid transparent;
  border-radius: 7px; padding: 7px 16px; cursor: pointer;
}
.orbit-mode-btn:hover { color: var(--ink); }
.orbit-mode-btn.is-active {
  background: var(--white); color: var(--red-text); border-color: var(--border-strong);
}
.orbit-mode-btn:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }

/* The viewport as the section centrepiece: wide, capped height, legend as a
   single horizontal strip beneath it instead of the rail's 2-column grid. */
.orbit-hero { margin-bottom: 4px; }
.orbit-hero .viewport { aspect-ratio: auto; height: min(40vh, 420px); border-radius: 14px; }
.orbit-hero .viewport-legend {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; margin-top: 12px; padding: 8px 12px;
}

/* Legend chips double as visibility toggles for their scene element. */
button.lg-item {
  font-family: inherit; background: none; border: 0; cursor: pointer;
  padding: 4px 7px; border-radius: 7px; transition: opacity .14s ease, background-color .14s ease;
}
button.lg-item:hover { background: var(--hover-bg); }
button.lg-item:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }
.lg-item.is-off { opacity: .38; }
.lg-item.is-off .lg-swatch { filter: grayscale(.9); }
.lg-item.is-off { text-decoration: line-through; text-decoration-thickness: 1px; }
.lg-note {
  margin-left: auto; font-size: 10px; letter-spacing: .05em; color: var(--faint);
  text-transform: uppercase; white-space: nowrap;
}

/* Six classical elements in one compact row under the viewport (3×2 when narrow). */
.elements-strip { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 16px; }
@media (max-width: 1280px) { .elements-strip { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.elements-strip .field-input.has-unit input { padding-right: 40px; }

/* The six boxes must sit on ONE line. Left to themselves each field is an
   independent stack, so a description that wraps to two lines (semi-major
   axis) or a derived readout printed under the box (mean altitude) pushed
   that column's box off the line the other five share. Fix: the strip owns
   four rows (label, description, box, derived readout) and every field
   borrows them with subgrid, so the four bands are shared instead of
   per-column and no amount of wrapping can stagger the boxes.
   Row gap stays 2px, matching the flex gap it replaces, so the internal
   spacing is unchanged; the 14px field margin restores the 16px separation
   between the two banks when the strip wraps, and the negative margin on the
   strip stops that margin adding space under the last bank. */
.elements-strip { column-gap: 16px; row-gap: 2px; margin-bottom: -14px; }
@supports (grid-template-rows: subgrid) {
  .elements-strip .field {
    display: grid; grid-row: span 4; grid-template-rows: subgrid;
    gap: 0; margin-bottom: 14px;
  }
}
/* Last band, and the bottom of the card: it does not need the description's
   trailing margin. */
.elements-strip #sma_derived { margin-bottom: 0; }

/* TLE mode: the strip becomes a read-only readback of the imported set. */
#orbitElementsCard.is-readonly .field-input input {
  background: var(--btn-bg); color: var(--muted); pointer-events: none;
}
#orbitElementsCard.is-readonly .field-input input::-webkit-outer-spin-button,
#orbitElementsCard.is-readonly .field-input input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
#orbitElementsCard .head-meta { color: var(--red-text); font-weight: 700; letter-spacing: .06em; }

/* Environment card: one narrow field, no need to span the full form grid. */
.env-grid { grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); }

.viewport-legend {
  background: var(--card-bg); border: 1px solid var(--border-2); border-radius: 10px;
  padding: 12px 14px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px 10px; margin-top: 13px;
}
.lg-item { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--text-2); }
.lg-swatch { width: 11px; height: 11px; border-radius: 3px; flex-shrink: 0; }

/* cube viewer in rail */
.cube-viewer {
  position: relative; aspect-ratio: 1 / 1.08; border-radius: 12px; overflow: hidden;
}
.cube-viewer-head {
  position: absolute; top: 13px; left: 15px; right: 15px; display: flex; justify-content: space-between;
  font-size: 10.5px; letter-spacing: 1px; color: var(--vp-label); z-index: 2;
}
.cube-viewer-head span:last-child { color: var(--red-text); font-weight: 700; }
.cube-viewer-foot {
  position: absolute; bottom: 13px; left: 15px; font-size: 10.5px; color: var(--vp-foot); z-index: 2;
}
.cube-rotating {
  position: absolute; bottom: 13px; right: 15px; display: flex; align-items: center; gap: 5px;
  font-size: 10px; color: var(--vp-foot); z-index: 2;
}
.rot-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--green); }
/* Grabbable: the stage is the camera control for the preview. touch-action
   is off so a drag on a phone rotates the body instead of scrolling the
   panel out from under it. */
.cube-stage { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; perspective: 760px;
  cursor: grab; touch-action: none; }
.cube-stage.is-grabbing { cursor: grabbing; }
/* The Attitude view's preview is a readout, not an object to turn. */
.cube-stage.no-drag { cursor: default; }
/* soft ambient backglow behind the body + a grounding contact shadow below it
   (absolute so they don't disturb the flex centering; painted below .cube) */
.cube-stage::before {
  content: ""; position: absolute; width: 56%; height: 56%; border-radius: 50%;
  background: radial-gradient(circle, var(--vp-glow), transparent 68%);
  filter: blur(5px); z-index: 0; pointer-events: none;
}
.cube-stage::after {
  content: ""; position: absolute; bottom: 15%; width: 40%; height: 9%; border-radius: 50%;
  background: radial-gradient(ellipse, var(--vp-shadow), transparent 72%);
  filter: blur(3px); z-index: 0; pointer-events: none;
}
/* .cube is now a fixed camera (3/4 view). The body orientation and the gentle
   cosmetic spin live on .cube-body; the orbit-frame triad stays fixed. */
.cube { position: relative; z-index: 1; transform-style: preserve-3d; transform: rotateX(-20deg) rotateY(-30deg); }
.cube-body, .orbit-frame { position: absolute; top: 0; left: 0; transform-style: preserve-3d; will-change: transform; }
/* Orbit-frame triad. Previously each rod/tip carried a coloured box-shadow
   halo, which read as a neon glow and fought the rest of the UI. The rods are
   now matte: a soft neutral drop shadow gives depth without the bloom. */
.axis-rod, .axis-head, .axis-label { position: absolute; top: 50%; left: 50%; pointer-events: none; }
.axis-rod {
  border-radius: 2px;
  box-shadow: 0 1px 2px var(--vp-shadow), inset 0 0 0 .5px rgba(255,255,255,.18);
}
/* Arrowhead: a zero-size box whose borders draw the triangle, so the shadow has
   to come from a filter (box-shadow would outline the box, not the triangle). */
.axis-head { width: 0; height: 0; filter: drop-shadow(0 1px 2px var(--vp-shadow)); }
/* Axis letters sit on a chip rather than relying on a glow. Z points at nadir,
   so past the horizon it was blue text on the blue ocean and disappeared; a
   halo alone could not carry it. The chip is filled with the preview's own
   background, which means it is invisible against the empty sky where X and Y
   live and only becomes a plate where a label crosses the planet. */
.axis-label {
  margin-left: -10px; margin-top: -10px;
  font-size: 12px; font-weight: 700; line-height: 1;
  padding: 3px 5px; border-radius: 5px;
  background: var(--vp-bg); border: 1px solid var(--vp-border);
}
.orbit-path {
  position: absolute; top: 50%; left: 50%; box-sizing: border-box;
  border: 2.5px dashed var(--vp-path); border-radius: 50%; pointer-events: none;
}
/* Earth limb in the cube preview. The surface itself is a <canvas> painted by
   earthDiscCanvas() in app.js — a real orthographic projection of the coastline
   map, so the continents are actual geography rather than the blurred gradient
   ellipses that used to stand in for them and read as stains.

   What stays here is a plain-ocean fallback: if the canvas ever fails to paint,
   this shows a clean lit sphere with a terminator instead of an empty hole. It
   deliberately has no landmasses — a bare ocean globe looks deliberate, a
   badly-faked continent does not. It is also what the _theme-preview.html
   harness renders, since that has no access to the projection code. */
.orbit-earth {
  position: absolute; top: 50%; left: 50%; border-radius: 50%; pointer-events: none;
  background:
    /* thin airglow band hugging the limb — the blue line of atmosphere from LEO */
    radial-gradient(circle at 50% 50%, rgba(150,215,255,0) 88%, rgba(180,225,255,0.55) 96%, rgba(205,236,255,0) 100%),
    /* ocean, lit from the upper left and falling off to the terminator */
    radial-gradient(circle at 34% 26%, #8fc8ef 0%, #4f95d2 18%, #2f72b4 38%, #1d5088 60%, #103358 82%, #06192f 100%);
  box-shadow: 0 0 10px rgba(150,210,255,0.40);
}
.orbit-arrow {
  position: absolute; top: 50%; left: 50%; width: 0; height: 0;
  border-top: 5px solid transparent; border-bottom: 5px solid transparent;
  border-left: 9px solid var(--vp-ax-x); pointer-events: none;
  filter: drop-shadow(0 1px 1px var(--vp-shadow));
}
.orbit-frame-legend {
  position: absolute; top: 38px; left: 15px; z-index: 2;
  display: flex; gap: 11px; align-items: center;
  font-size: 10px; letter-spacing: .5px; color: var(--vp-foot);
}
.orbit-frame-legend i { display: inline-block; width: 9px; height: 9px; border-radius: 2px; margin-right: 4px; vertical-align: -1px; }
.orbit-frame-legend .ofl-x { background: var(--vp-ax-x); }
.orbit-frame-legend .ofl-y { background: var(--vp-ax-y); }
.orbit-frame-legend .ofl-z { background: var(--vp-ax-z); }
.orbit-frame-legend .ofl-note { color: var(--vp-hint); }
/* ---- Spacecraft body panels ------------------------------------------------
   Built in four stacked layers so it reads as real hardware rather than a
   textured box:
     1. ::after  - brushed-aluminium rail frame around every face
     2. ::before - structural seams every 1 U (--u-px, set from JS)
     3. background - glass sheen + silver bus bars + solar-cell gaps + cell body
     4. box-shadow - inset ambient occlusion and a thin rim light
   --cell is the solar-cell pitch; --u-px is the pixel size of one CubeSat unit. */
/* The face itself is now the bare PANEL: brushed, anodised structure. The solar
   array moved to the .cube-cells layer inside it, because a face carries cells
   over some fraction of itself and not always all of it. A face typed as panel
   only, or as a camera, simply has no cells layer showing. */
.cube-face {
  position: absolute; top: 50%; left: 50%;
  --cell: 13px;
  border: 1px solid rgba(150,176,216,0.5);
  box-shadow: inset 0 0 20px rgba(0,8,24,0.55), inset 0 0 1px rgba(190,210,245,0.45);
  background:
    linear-gradient(125deg, rgba(255,255,255,0.13), rgba(255,255,255,0) 52%),
    repeating-linear-gradient(102deg,
      rgba(255,255,255,.045) 0 1px, rgba(0,0,0,.05) 1px 2.5px),
    linear-gradient(158deg, #7c8798 0%, #5b6575 52%, #454e5c 100%);
}
/* The solar array. Anchored to the bottom of its face and grown upward to the
   coverage percentage, so 60 percent reads as six tenths of the panel rather
   than as a uniformly darker face. */
/* The array sits INSIDE the panel with structure all round it, which is how a
   real CubeSat panel is built: cells never reach the rails. --cov is the LINEAR
   scale, set to the square root of the coverage fraction by paintCubeFaces, so
   the drawn AREA is the fraction the user entered. */
.cube-cells {
  position: absolute; left: 50%; top: 50%;
  width: calc(var(--cov, 0) * 100%); height: calc(var(--cov, 0) * 100%);
  transform: translate(-50%, -50%);
  --cell: 13px;
  border: 1px solid rgba(190,210,245,.34);
  box-shadow: 0 0 0 1px rgba(8,14,28,.55);
  background:
    /* cover-glass sheen */
    linear-gradient(125deg, rgba(255,255,255,0.16), rgba(255,255,255,0) 46%),
    /* silver bus bars: two fine current tracks across each cell */
    repeating-linear-gradient(90deg,
      transparent 0 3.4px, rgba(200,218,252,.22) 3.4px 4.1px,
      transparent 4.1px 8.9px, rgba(200,218,252,.22) 8.9px 9.6px,
      transparent 9.6px var(--cell)),
    /* gaps between individual cells */
    repeating-linear-gradient(0deg,  rgba(6,12,26,.92) 0 1.5px, transparent 1.5px var(--cell)),
    repeating-linear-gradient(90deg, rgba(6,12,26,.92) 0 1.5px, transparent 1.5px var(--cell)),
    /* cell body */
    linear-gradient(155deg, #1b3060 0%, #0d1830 100%);
}
/* Structural seam every 1 U: a dark joint line with a bright machined lip. */
.cube-face::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: repeating-linear-gradient(0deg,
    rgba(0,4,14,.55) 0 1.5px, rgba(226,236,255,.28) 1.5px 2.5px,
    transparent 2.5px var(--u-px, 9999px));
}
.cube-face.cf-top::before, .cube-face.cf-bottom::before {
  background:
    repeating-linear-gradient(0deg,  rgba(0,4,14,.5) 0 1.5px, rgba(226,236,255,.24) 1.5px 2.5px, transparent 2.5px var(--u-px, 9999px)),
    repeating-linear-gradient(90deg, rgba(0,4,14,.5) 0 1.5px, rgba(226,236,255,.24) 1.5px 2.5px, transparent 2.5px var(--u-px, 9999px));
}
/* Anodised aluminium rail running around the edge of every panel. */
.cube-face::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  border: 1.5px solid transparent;
  border-image: linear-gradient(150deg,
    rgba(236,243,255,.85) 0%, rgba(150,170,205,.35) 38%,
    rgba(206,222,252,.70) 62%, rgba(120,138,172,.30) 100%) 1;
}
/* Top and underside catch the light differently from the walls. LIGHTING ONLY:
   these used to restate the whole `background`, solar cells included, which
   overrode the panel underneath and left the top and bottom faces painted as
   arrays whatever the user had selected for them. A face's surface is now the
   face's business and lighting is this rule's business. */
/* Which of the two decks is lit follows the BODY, which is now drawn nadir
   down in every preview: -Z is the deck facing away from Earth and so the one
   catching the light, and +Z is the one looking down at it. These were the
   other way round while the body was drawn +Z up. */
.cube-face.cf-bottom {
  border-color: rgba(184,208,248,0.62);
  box-shadow: inset 0 0 20px rgba(12,24,52,0.42), inset 0 0 2px rgba(210,226,255,0.55);
  filter: brightness(1.10);
}
.cube-face.cf-top {
  border-color: rgba(90,112,152,0.46);
  box-shadow: inset 0 0 24px rgba(0,4,14,0.7);
  filter: brightness(0.72);
}

/* ---- Payload + comms detail ------------------------------------------------
   A camera/optical aperture on the +Z panel and a deployed monopole antenna off
   the -Z panel. Both are positioned from JS (they scale with the form factor).
   The antenna is drawn as two crossed ribbons, the same trick the spin axis
   uses, so it still reads as a rod from any viewing angle. */
.cube-lens {
  position: absolute; top: 50%; left: 50%; border-radius: 50%; pointer-events: none;
  background:
    radial-gradient(circle at 36% 30%, rgba(255,255,255,.55), rgba(255,255,255,0) 44%),
    radial-gradient(circle at 50% 50%,
      #081726 0 34%, #14456e 34% 56%, #0a1e33 56% 74%,
      #9aa8bf 74% 86%, #616e83 86% 100%);
  box-shadow: 0 0 0 1px rgba(226,238,255,.55), inset 0 0 7px rgba(0,0,0,.75);
}
/* spin-axis indicator drawn through the cube (Attitude section, spin mode) */
.spin-rod, .spin-tip { position: absolute; top: 50%; left: 50%; display: none; pointer-events: none; }
.spin-rod {
  border-radius: 3px;
  background: linear-gradient(90deg, transparent, var(--red) 42%, var(--red) 58%, transparent);
  box-shadow: 0 1px 3px var(--vp-shadow);
}
.spin-tip {
  border-radius: 50%;
  background: radial-gradient(circle at 38% 34%, var(--red-edge), var(--red) 72%);
  box-shadow: 0 1px 4px var(--vp-shadow);
}

/* ADCS (attitude dynamics) inputs + results */
.adcs-chk { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink); cursor: pointer; }
.adcs-chk input { accent-color: var(--red); }
#btnRunAdcs {
  background: var(--red); color: #fff; border: none; border-radius: 8px;
  padding: 10px 18px; font: 600 13px/1 inherit; letter-spacing: .3px; cursor: pointer;
}
#btnRunAdcs:hover { filter: brightness(1.06); }
#btnRunAdcs:disabled { opacity: .6; cursor: default; }
/* Notice shown when a run did not do what was asked of it — currently only the
   attitude window being shortened to keep the integration step honest. Uses the
   existing warn palette so it reads as "read this", not as an error. */
.run-notice {
  margin: 0 0 12px; padding: 10px 12px; border-radius: 6px;
  background: var(--warn-bg); color: var(--warn-text);
  border-left: 3px solid var(--warn-mark);
  font-size: 12.5px; line-height: 1.5;
}
.run-notice b { font-weight: 700; }
.run-notice .why { display: block; margin-top: 5px; opacity: .85; font-size: 11.5px; }
/* Rejected run. The hot (red) triple is defined in all three themes, so this
   tracks the theme the same way the warn state does. */
.run-notice.is-error {
  background: var(--hot-soft); color: var(--hot-text); border-left-color: var(--hot);
}

/* ---------- DOI: Attitude Animation ----------------------------------------
   The 3D host is a .chart-box so it inherits the card, the fixed height and the
   overflow clip every other figure has, and .viewport so the themed backdrop
   shows through the transparent WebGL canvas. The height is owned HERE, by the
   container, and the renderer sizes itself from clientWidth/clientHeight: the
   same contract the Plotly boxes use, and for the same reason, which is that a
   size baked into the drawing code grows on every resize call.

   The transport lives ON the canvas rather than under it. Split across the page
   chrome it survives a wide window and disappears in a narrow one, which is a
   failure that a screenshot at one size does not catch. */
.att-anim-view { position: relative; height: 460px; min-height: 460px; aspect-ratio: auto; }
.att-anim-canvas { position: absolute; inset: 0; }
.att-anim-view canvas { display: block; }

.att-anim-legend {
  position: absolute; left: 13px; top: 13px; z-index: 2; pointer-events: none;
  display: flex; flex-direction: column; gap: 4px;
  padding: 8px 11px; border-radius: 8px;
  /* Panel tokens, not the viewport's label colours. The scene behind this chip
     is whatever the camera is pointing at: deep space on one side of the orbit,
     a sunlit ocean on the other. Painted in --vp-label it was legible against
     the dark and vanished against the light. A panel-coloured chip carries its
     own contrast in both themes and in both halves of the orbit. */
  background: var(--card-bg); border: 1px solid var(--border-2); color: var(--ink);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .18);
  font-size: 11px;
}
.att-anim-legend b { font-size: 10px; letter-spacing: .8px; font-weight: 700; }
.att-anim-legend .row { display: flex; align-items: center; gap: 7px; }
.att-anim-legend .sw { width: 15px; height: 3px; border-radius: 2px; flex: 0 0 auto; }
/* The equator is scene furniture, not a body axis, so it sits under a rule
   rather than reading as a fourth axis of the spacecraft. */
.att-anim-legend .row.sep { margin-top: 3px; padding-top: 5px; border-top: 1px solid var(--border-2); }

.att-anim-dock {
  position: absolute; left: 50%; bottom: 15px; transform: translateX(-50%); z-index: 3;
  display: flex; align-items: center; gap: 6px; padding: 6px;
  border-radius: 9px; border: 1px solid var(--vp-border);
  /* --vp-chip-bg was REFERENCED here and defined nowhere, so the near-black
     fallback painted in both themes. On the light canvas that put --vp-label,
     a grey chosen to read against a PALE background, on a black chip: 1.68:1.
     Dark scraped 4.85:1 over the canvas but only 3.52:1 over the globe, which
     is where the dock actually sits. The border came from --border-2, a
     page-surface token, so a cream hairline was drawn round a black chip. */
  background: var(--vp-chip-bg);
  box-shadow: 0 4px 16px rgba(0, 0, 0, .28);
}
.att-anim-dock .seg {
  display: flex; gap: 4px; padding-left: 6px;
  border-left: 1px solid var(--vp-border);
}
.att-anim-btn {
  font: inherit; font-size: 12.5px; font-weight: 600; white-space: nowrap; line-height: 1.15;
  padding: 8px 15px; border-radius: 6px; cursor: pointer;
  border: 1px solid var(--vp-border); background: transparent; color: var(--vp-chip-ink);
}
/* The hover wash has to be relative to the CHIP, not to the page: --hover-bg is
   a page-surface token and washed out to nothing on it. Its own token rather
   than borrowing --vp-grid, which means grid lines and would drift the moment
   someone tuned the grid. */
.att-anim-btn:hover { background: var(--vp-chip-hover); }
/* ONE state colour for the whole dock. Play, Wide and Chase share a single
   neutral appearance and the accent means exactly one thing: this is the state
   you are in. Before this, Play was permanently red as a "primary" button while
   the active camera was also red, so two buttons were highlighted at once and
   the colour said nothing. The play button carries it only while playing, and
   the camera buttons only for the view you are looking through. */
.att-anim-btn.is-on { background: var(--red); border-color: var(--red); color: #fff; }
.att-anim-btn#attAnimPlay { min-width: 100px; }
.att-anim-btn.is-ghost { color: var(--ink); font-weight: 500; }

.att-anim-sub { font-size: 11.5px; color: var(--faint); margin: 9px 2px 0; line-height: 1.5; }

.att-anim-transport {
  display: flex; align-items: center; gap: 10px 13px; flex-wrap: wrap;
  margin: 11px 0 13px;
}
.att-anim-transport #attAnimScrub { flex: 1 1 200px; min-width: 150px; accent-color: var(--red); cursor: pointer; }
.att-anim-speed { font-size: 11.5px; color: var(--faint); }
/* Position within the ORBIT. The quieter of the two now: it is the same
   information the scrub bar directly above it already draws. */
.att-anim-clock { font-size: 12px; color: var(--faint); font-variant-numeric: tabular-nums; }
/* Position within the DAY, and the headline number of the transport row.
   Nothing else on the panel says where in the day of interest you are, and
   with the orbit selectable that is the question the row exists to answer, so
   it is set as a value rather than as another label: a bordered chip, ink on
   the card surface, bold. The fixed width is so the row does not shuffle as
   the digits tick over. */
.att-anim-hours {
  font-size: 13px; font-weight: 700; color: var(--ink);
  background: var(--card-bg); border: 1px solid var(--border-2);
  border-radius: 999px; padding: 4px 12px;
  min-width: 104px; text-align: center; letter-spacing: .2px;
}
/* The orbit dropdown goes inert when the day offers only one orbit, and while
   a request for another one is in flight. Say so rather than look broken. */
#attAnimOrbit:disabled { opacity: .55; cursor: not-allowed; }
#attAnimCtrl.is-on { color: var(--red); }

@media (max-width: 900px) {
  .att-anim-view { height: 340px; min-height: 340px; }
  .att-anim-dock { bottom: 10px; padding: 5px; }
  .att-anim-btn { padding: 7px 12px; font-size: 12px; }
  .att-anim-btn.is-primary { min-width: 88px; }
}

.adcs-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 9px; }
.adcs-metric {
  background: var(--card-bg); border: 1px solid var(--border-2); border-radius: 9px; padding: 10px 12px;
  display: flex; flex-direction: column; gap: 4px;
}
.adcs-metric span { font-size: 10px; letter-spacing: .4px; color: var(--faint); }
.adcs-metric b { font-size: 15px; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }

/* quick readout cards */
.rail-readouts { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
.readout-card {
  background: var(--card-bg); border: 1px solid var(--border-2); border-radius: 9px; padding: 13px;
}
.readout-card .ro-label { font-size: 10px; letter-spacing: .5px; color: var(--faint); margin-bottom: 6px; }
.readout-card .ro-value { font-size: 18px; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.readout-card .ro-unit { font-size: 11px; font-weight: 500; color: var(--faint); margin-left: 3px; }

/* ===== Results ===== */
.placeholder {
  color: var(--text-2); font-size: 13px; padding: 36px;
  text-align: center; background: var(--card-bg); border: 1px dashed var(--border-2); border-radius: 12px;
}
/* Empty-state icon (theme-adaptive via mask + --faint) shown above the hint text.
   Shared by .placeholder and .pass-note so every "run an analysis" state matches. */
.placeholder::before, .pass-note::before {
  content: ""; display: block; width: 30px; height: 30px; margin: 0 auto 12px;
  background-color: var(--faint); opacity: .75;
  -webkit-mask: var(--empty-ico) center / contain no-repeat;
          mask: var(--empty-ico) center / contain no-repeat;
}
:root {
  --empty-ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M3 3v18h18'/><path d='M7 15l4-5 3 3 5-7'/></svg>");
}
.metrics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; }
/* Summary metrics grouped by category, each group titled with its swatch. */
.metrics-groups { display: flex; flex-direction: column; gap: 26px; }
.metric-group-head {
  display: flex; align-items: center; gap: 8px; margin-bottom: 11px;
  font-size: 11px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted);
}
.mg-swatch { width: 8px; height: 8px; border-radius: 2.5px; background: var(--border-strong); flex-shrink: 0; }
.metric-group.cat-orbital .mg-swatch { background: var(--vp-ax-z); }
.metric-group.cat-thermal .mg-swatch { background: var(--vp-apo); }
.metric-group.cat-power .mg-swatch { background: var(--green); }
.metric-group.cat-decay .mg-swatch { background: var(--vp-ax-x); }
/* Metric cards carry a quiet category accent (left rail + label dot):
   blue = orbital geometry, orange = thermal, green = power/energy input,
   red = decay/lifetime. Labels lifted from --faint to --muted for legibility. */
.metric-card { background: var(--card-bg); border: 1px solid var(--border-2); border-left-width: 3px; border-radius: 10px; padding: 15px 17px; }
.metric-card .metric-label { display: flex; align-items: center; gap: 6px; font-size: 10.5px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .07em; }
.metric-card .metric-label::before { content: ""; width: 6px; height: 6px; border-radius: 2px; background: var(--border-strong); flex-shrink: 0; }
.metric-card .metric-value { font-size: 22px; font-weight: 700; color: var(--ink); margin-top: 7px; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
.metric-card .metric-value.flag-red { color: var(--red-text); }
.metric-card .metric-unit { font-size: 12px; font-weight: 600; color: var(--faint); margin-left: 4px; letter-spacing: 0; }
.metric-card.cat-orbital { border-left-color: var(--vp-ax-z); }
.metric-card.cat-orbital .metric-label::before { background: var(--vp-ax-z); }
.metric-card.cat-thermal { border-left-color: var(--vp-apo); }
.metric-card.cat-thermal .metric-label::before { background: var(--vp-apo); }
.metric-card.cat-power { border-left-color: var(--green); }
.metric-card.cat-power .metric-label::before { background: var(--green); }
/* --vp-ax-x, not --red: the --red token is the THEME ACCENT (teal in dark),
   while --vp-ax-x stays a true red in every theme — decay must read as caution. */
.metric-card.cat-decay { border-left-color: var(--vp-ax-x); }
.metric-card.cat-decay .metric-label::before { background: var(--vp-ax-x); }

.hotcold-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.case-card { background: var(--card-bg); border: 1px solid var(--border-2); border-top: 3px solid var(--border-2); border-radius: 12px; padding: 16px 20px 18px; }
.case-card.hot  { border-top-color: var(--hot); }
.case-card.cold { border-top-color: var(--cold); }
.case-card h3 { margin: 0 0 14px 0; font-size: 12px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; display: flex; align-items: center; gap: 8px; }
.case-card h3::before { content: ""; width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; }
.case-card.hot  h3 { color: var(--hot-text); }
.case-card.hot  h3::before { background: var(--hot); }
.case-card.cold h3 { color: var(--cold-text); }
.case-card.cold h3::before { background: var(--cold); }
.case-card .row { display: flex; justify-content: space-between; font-size: 13px; padding: 6px 0; border-bottom: 1px solid var(--border-4); }
.case-card .row:last-child { border-bottom: none; }
.case-card .row span:first-child { color: var(--muted); }
.case-card .row span:last-child { font-weight: 700; font-variant-numeric: tabular-nums; }

.chart-box { background: var(--card-bg); border: 1px solid var(--border-2); border-radius: 12px; padding: 0; overflow: hidden; height: 380px; min-height: 380px; }
.chart-box.gt-map { height: 340px; min-height: 340px; padding: 0; }
.chart-box.gantt-box { height: 540px; min-height: 540px; padding: 0; }

/* ground-track animation controls */
.gt-controls { display: flex; align-items: center; gap: 14px; margin-bottom: 13px; flex-wrap: wrap; }
.gt-btn {
  font-family: var(--sans); font-size: 12.5px; font-weight: 700; color: #fff;
  background: var(--red); border: 1px solid var(--red-border); border-radius: 7px;
  padding: 7px 16px; cursor: pointer; min-width: 96px;
  box-shadow: 0 1px 3px rgba(210,63,28,.3);
}
.gt-btn:hover { background: var(--red-hover); }
.gt-btn.gt-reset { color: var(--label); background: var(--btn-bg); border-color: var(--border); box-shadow: none; min-width: 88px; }
.gt-btn.gt-reset:hover { background: var(--btn-hover-bg); border-color: var(--border-strong); }
.gt-slider { flex: 1; min-width: 150px; accent-color: var(--red); cursor: pointer; }
.gt-speed-wrap { display: inline-flex; align-items: center; gap: 6px; font-family: var(--sans); font-size: 11.5px; color: var(--muted); }
.gt-speed { font-family: var(--sans); font-size: 12px; font-weight: 600; color: var(--label); background-color: var(--white); border: 1px solid var(--border); border-radius: 7px; padding: 6px 30px 6px 10px; cursor: pointer; }
.gt-toggle { display: inline-flex; align-items: center; gap: 5px; font-family: var(--sans); font-size: 11.5px; color: var(--muted); cursor: pointer; user-select: none; }
.gt-toggle input { accent-color: var(--red); cursor: pointer; }
.mission-actions { margin-top: 16px; }

/* TLE import — the textarea stays monospace on purpose: a two-line element
   set is a fixed-column format, and alignment is how you eyeball-validate one. */
.tle-input { width: 100%; box-sizing: border-box; font-family: var(--mono); font-size: 12px; line-height: 1.55; color: var(--ink); background: var(--white); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; resize: vertical; white-space: pre; overflow-x: auto; }
/* :focus, not :focus-visible, so this also caught keyboard entry, and at
   (0,2,0) it outranked the global textarea:focus-visible ring at (0,1,1). What
   it left behind was --focus-ring, which is 16% alpha: a hint, not an
   indicator. The soft ring stays for pointer focus; the real one comes back
   for the keyboard. */
.tle-input:focus { border-color: var(--red); box-shadow: 0 0 0 3px var(--focus-ring); outline: none; }
.tle-input:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
.tle-actions { display: flex; align-items: center; gap: 12px; margin-top: 10px; flex-wrap: wrap; }
.tle-status { font-family: var(--sans); font-size: 11.5px; line-height: 1.5; }
.tle-status.ok { color: var(--green-dk); }
.tle-status.err { color: var(--error-text); }
.gt-readout { display: flex; gap: 16px; flex-wrap: wrap; font-family: var(--sans); font-size: 12px; color: var(--text-2); letter-spacing: .01em; margin-bottom: 13px; }
.gt-readout b { color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }

/* ground-station passes */
.pass-note { color: var(--text-2); font-size: 13px; padding: 30px; text-align: center; background: var(--card-bg); border: 1px dashed var(--border-2); border-radius: 12px; }
.pass-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 16px; }
.pass-summary > div { background: var(--card-bg); border: 1px solid var(--border-2); border-radius: 9px; padding: 10px 13px; }
.pass-summary span { display: block; font-size: 10px; text-transform: uppercase; letter-spacing: .06em; color: var(--faint); font-weight: 700; margin-bottom: 3px; }
.pass-summary b { font-size: 14px; color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }
.pass-tablewrap { max-height: 470px; overflow: auto; border: 1px solid var(--border-2); border-radius: 12px; background: var(--card-bg); }
.pass-table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: var(--sans); font-size: 12.5px; }
.pass-table th { position: sticky; top: 0; z-index: 1; background: var(--card-head); color: var(--muted); font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--border-3); white-space: nowrap; }
.pass-table td { padding: 8px 12px; border-bottom: 1px solid var(--border-4); color: var(--ink); white-space: nowrap; font-variant-numeric: tabular-nums; }
.pass-table tbody tr:hover { background: var(--hover-bg); }
.pass-table tbody tr:last-child td { border-bottom: none; }
/* The virtualised mission table (see renderMissionPassRows in app.js) keeps only
   the visible rows in the DOM and carries the rest as two spacer rows. They hold
   height and nothing else, so they must not paint a border, take padding, or
   light up on hover the way a real row does — a hovering blank band above the
   first row reads as a rendering fault. The sizer row is measured and removed
   within the same paint and is never shown; it is styled only so that a failure
   to remove it would look wrong rather than pass for data. */
.pass-table tbody tr.pass-spacer td { padding: 0; border: 0; }
.pass-table tbody tr.pass-spacer:hover { background: none; }
.pass-table tbody tr.pass-sizer { visibility: hidden; }
.pass-dir { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 5px; }
/* Dagger on a pass whose AOS/LOS is clipped by the edge of the analysis
   window, so its duration is a lower bound (see CLIP_MARK in app.js). */
.pass-clip { color: var(--warn-text); font-weight: 700; margin-left: 3px; cursor: help; }
.pass-dir.asc { color: var(--green-dk); background: var(--green-soft-bg); }
.pass-dir.desc { color: var(--blue); background: var(--hover-bg); }
.gt-passes { margin-top: 16px; }
.gt-passes-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); margin: 0 0 10px; }
/* Second and later ground-station headings need breathing room from the table
   (or empty-mask line) of the station above them. */
.pass-tablewrap + .gt-passes-head,
.gt-passes-head + .gt-passes-head { margin-top: 28px; }
.gt-passes-head b { color: var(--ink); }
.gt-passes .pass-tablewrap { max-height: 300px; }
.pass-clickable tbody tr { cursor: pointer; }

/* passes: stats bands, filter toolbar, mini buttons */
.pass-bands { display: flex; flex-wrap: wrap; gap: 8px; margin: -6px 0 16px; }
.pass-bands .pill { font-size: 11px; font-weight: 700; padding: 3px 10px; border-radius: 20px; color: var(--label); background: var(--hover-bg); border: 1px solid var(--border-2); }
.pass-bands .pill.b3 { color: var(--green-dk); background: var(--green-soft-bg); }
.pass-bands .pill.b2 { color: var(--blue); background: var(--hover-bg); }
.pass-bands .pill.b1 { color: var(--amber); background: var(--hover-bg); }
.pass-bands .pill.b0 { color: var(--faint); background: var(--hover-bg); }
.pass-toolbar { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-bottom: 12px; }
.pass-toolbar label { display: inline-flex; align-items: center; gap: 7px; font-family: var(--sans); font-size: 11.5px; color: var(--muted); }
.pass-toolbar select { font-family: var(--sans); font-size: 12px; font-weight: 600; color: var(--label); background-color: var(--white); border: 1px solid var(--border); border-radius: 7px; padding: 6px 28px 6px 10px; cursor: pointer; }
.pass-showing { font-family: var(--sans); font-size: 11.5px; color: var(--faint); margin-left: auto; }
.mini-btn { font-family: var(--sans); font-size: 12.5px; font-weight: 600; color: var(--label); background: var(--btn-bg); border: 1px solid var(--border); border-radius: 7px; padding: 6px 12px; cursor: pointer; white-space: nowrap; }
.mini-btn:hover { background: var(--btn-hover-bg); border-color: var(--border-strong); }
.pass-toolbar .mini-btn { margin-left: 0; }

/* sky plot */
.sky-toolbar { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-bottom: 12px; }
.sky-toolbar label { display: inline-flex; align-items: center; gap: 7px; font-family: var(--sans); font-size: 11.5px; color: var(--muted); }
.sky-hint { font-family: var(--sans); font-size: 11.5px; color: var(--text-2); }
.chart-box.sky-chart { height: 360px; min-height: 360px; padding: 0; margin: 0 auto; }
#railSky .sky-toolbar { margin-bottom: 10px; }
#railSky .sky-hint { display: block; margin-top: 4px; line-height: 1.5; }

.face-selector { display: flex; gap: 6px; margin-bottom: 14px; flex-wrap: wrap; }
.face-btn { border: 1px solid var(--border-2); background: var(--white); padding: 6px 15px; border-radius: 7px; font-family: var(--sans); font-size: 12px; font-weight: 600; cursor: pointer; color: var(--label); }
.face-btn.active { background: var(--red); color: #fff; border-color: var(--red); }

/* ===== Console (bottom) ===== */
.output-log {
  background: var(--log-bg); border-top: 2px solid var(--red);
  display: flex; flex-direction: column; height: 118px; transition: height .2s ease;
}
.output-log.collapsed { height: 39px; }
.output-log.collapsed .log-body { display: none; }
.log-header {
  display: flex; justify-content: space-between; align-items: center; padding: 9px 20px; cursor: pointer;
}
.log-header-left { display: flex; align-items: center; gap: 11px; font-size: 10.5px; letter-spacing: 2px; color: var(--log-ink); font-weight: 700; text-transform: uppercase; }
.log-header-left::before { content: "●"; color: var(--red-text); font-size: 6px; }
.log-count { color: var(--log-muted); font-weight: 400; letter-spacing: .02em; text-transform: none; }
.log-header-right { display: flex; gap: 8px; }
.log-btn { background: var(--log-btn-bg); border: 1px solid var(--log-btn-border); color: var(--log-ink); font-family: var(--mono); font-size: 11px; padding: 5px 11px; border-radius: 6px; cursor: pointer; letter-spacing: .04em; }
.log-btn:hover { border-color: var(--red); color: var(--red-text); }
/* The output log is the one true terminal surface — monospace stays. */
.log-body { flex: 1; overflow-y: auto; padding: 4px 20px 10px; font-family: var(--mono); font-size: 12px; line-height: 1.65; color: var(--log-ink); }
.log-line { white-space: pre-wrap; word-break: break-word; }
.log-line.log-info { color: var(--log-info); }
.log-line.log-ok { color: var(--log-ok); }
.log-line.log-error { color: var(--log-error); }
.log-line.log-warn { color: var(--log-warn); }
.log-time { color: var(--log-muted); margin-right: 10px; }

/* ===== Skeleton loaders (pending results) ===== */
@keyframes skShimmer { 0% { background-position: -460px 0; } 100% { background-position: 460px 0; } }
.sk {
  position: relative; border-radius: 6px;
  background: var(--sk-base);
  background-image: linear-gradient(90deg, var(--sk-base) 0, var(--sk-highlight) 40%, var(--sk-base) 80%);
  background-size: 460px 100%; background-repeat: no-repeat;
  animation: skShimmer 1.4s ease-in-out infinite;
}
.skeleton-wrap { position: relative; }
.skeleton-tag {
  position: absolute; top: 12px; right: 14px; z-index: 2;
  font-size: 9.5px; font-weight: 700; letter-spacing: .12em; color: var(--faint);
  background: var(--card-head); border: 1px solid var(--border-2);
  border-radius: 5px; padding: 3px 8px; text-transform: uppercase;
}
/* chart skeleton: faux axes + rising bars */
.sk-chart { display: flex; align-items: flex-end; gap: 10px; height: 320px; padding: 18px 18px 30px; }
.sk-bar { flex: 1; border-radius: 4px 4px 0 0; }
.sk-axis { position: absolute; left: 18px; right: 18px; bottom: 30px; height: 1px; background: var(--border-2); }
/* metric skeleton cards */
.sk-metric-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; }
.sk-metric { height: 74px; border-radius: 10px; }
.sk-case-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.sk-case { height: 190px; border-radius: 12px; }
.sk-line { height: 12px; border-radius: 5px; }
/* "Run an analysis to see results." caption over the pending skeleton */
.sk-note {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-family: var(--sans); font-size: 14px; color: var(--muted);
  letter-spacing: .01em; pointer-events: none; text-align: center; padding: 0 16px;
}
.sk-note::before {
  content: ""; position: absolute; inset: 0; background: var(--sk-note-overlay);
}
.sk-note { z-index: 3; }

/* ===== Run status strip ===== */
/* Idle, this must occupy NO height. box-sizing is border-box, so height:0 with
   vertical padding and a border still reserved 15px: an invisible band (opacity
   is 0) sitting between the workspace and the output log, which read as the
   mission tree stopping short of the log. Padding and border now belong to the
   active state, and both are transitioned so the strip still slides open. */
.run-strip {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 16px;
  background: #1d1a15; border-top: 0 solid #2a2620; color: #cfc8bb;
  padding: 0 20px; height: 0; overflow: hidden; opacity: 0;
  transition: height .2s ease, padding .2s ease, border-width .2s ease, opacity .2s ease;
}
.run-strip.active { height: 34px; opacity: 1; padding: 7px 20px; border-top-width: 1px; }
.run-strip-left { display: flex; align-items: center; gap: 9px; min-width: 168px; }
.run-spin {
  width: 11px; height: 11px; border-radius: 50%;
  border: 2px solid #4a443b; border-top-color: var(--red); animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.run-phase { font-size: 11px; font-weight: 600; letter-spacing: .04em; color: #e7e0d4; }
.run-strip-mid { display: flex; }
.run-bar { width: 100%; height: 6px; background: #2c2820; border-radius: 4px; overflow: hidden; }
.run-bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--red), var(--red-edge)); border-radius: 4px; transition: width .12s linear; }
.run-strip-right { display: flex; align-items: center; gap: 12px; font-size: 11px; }
.run-steps { color: #9a948b; font-variant-numeric: tabular-nums; letter-spacing: .02em; }
.run-pct { color: #fff; font-weight: 700; font-variant-numeric: tabular-nums; min-width: 38px; text-align: right; }
.run-strip.done .run-spin { border-top-color: var(--green); border-color: #3a4a3a; animation: none; }
.run-strip.done .run-bar-fill { background: var(--green); }

/* ===== Tree completeness states ===== */
/* Results shows a DONE badge after a run; input groups carry no status marker
   (GMAT-style: the tree is a browser, validity surfaces via field checks + the run). */
.tree-root.is-set .tree-status { color: var(--green-dk); background: var(--green-soft-bg); }
.tree-status.st-warn { color: var(--warn-text); background: var(--warn-bg); }

/* ===== Responsive: collapse right rail on narrow screens ===== */
/* Responsive layout: see the single RESPONSIVE block at the end of this file. */

/* ---------- Date-of-Interest validity highlight ---------- */
/* Error states. Still NOT the brand accent (in Light/Dark that is teal, which
   would read as "active" rather than "wrong") — they use the per-theme --error
   tokens defined at the top of this file. */
.field.doi-invalid .field-label, .field.field-invalid .field-label { color: var(--error-text); }
.field.doi-invalid .field-sub, .field.field-invalid .field-sub { color: var(--error-text); }
.field.doi-invalid input, .field.field-invalid input { border-color: var(--error) !important; box-shadow: 0 0 0 1px var(--error-ring); }

/* Last-run provenance strip (top of every results section): what inputs produced
   the numbers below, and when they were computed. */
.run-prov {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  background: var(--card-bg); border: 1px solid var(--border-2);
  border-left: 3px solid var(--red); border-radius: 10px;
  padding: 9px 15px; margin-bottom: 20px; font-size: 12px;
}
.run-prov .rp-main { display: flex; align-items: baseline; gap: 10px 14px; min-width: 0; flex-wrap: wrap; }
.run-prov .rp-name { font-weight: 700; color: var(--ink); letter-spacing: .01em; }
.run-prov .rp-params { color: var(--muted); font-variant-numeric: tabular-nums; }
.run-prov .rp-time { color: var(--faint); white-space: nowrap; flex-shrink: 0; font-variant-numeric: tabular-nums; }

/* ---------- Extra ground stations (2-3) ---------- */
.gs-extra { margin-top: 16px; padding-top: 12px; border-top: 1px dashed rgba(120, 110, 95, 0.45); }
.gs-extra-head { display: flex; align-items: center; gap: 8px; cursor: pointer; margin-bottom: 10px; }
.gs-extra-head input[type="checkbox"] { accent-color: var(--red); width: 15px; height: 15px; }
.gs-extra:not(.on) .gs-extra-grid { opacity: 0.45; pointer-events: none; }

/* ---------- Station tabs on the mission-pass panel ---------- */
.gs-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }

/* ---------- Mission presets (toolbar) ---------- */
.preset-select { background: transparent; border: 1px solid rgba(127, 127, 127, 0.4); border-radius: 6px;
  padding: 5px 8px; font: inherit; font-size: 12px; color: inherit; max-width: 170px; cursor: pointer; }
.preset-select option { color: #1a1a1a; }

/* ---------- Menubar (File / View) ---------- */
.menubar { display: flex; align-items: center; gap: 2px; margin-left: 10px; }
/* Quick-access toolbar: compact icon buttons for the most-used actions, sits
   next to the menus; the auto margin pushes the run/preset actions to the right. */
.qat { display: flex; align-items: center; gap: 1px; margin-left: 8px; margin-right: auto;
  padding-left: 8px; border-left: 1px solid var(--border); }
.qat-btn { display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 28px; border: 0; background: transparent; color: var(--muted);
  border-radius: 6px; cursor: pointer; transition: background .12s ease, color .12s ease; }
.qat-btn:hover:not(:disabled) { background: var(--hover-bg); color: var(--red-text); }
.qat-btn:disabled { opacity: .4; cursor: default; }
.qat-btn .ico { width: 16px; height: 16px; vertical-align: 0; }
@media (max-width: 1180px) { .qat { display: none; } }
.menu { position: relative; }
.menu-btn { background: transparent; border: 0; color: inherit; font: inherit; font-size: 12.5px;
  padding: 6px 11px; border-radius: 6px; cursor: pointer; }
.menu-btn:hover, .menu.open .menu-btn { background: rgba(127, 127, 127, 0.22); }
.menu-items { position: absolute; top: calc(100% + 4px); left: 0; min-width: 218px; z-index: 1200;
  background: var(--card-bg); color: var(--ink); border: 1px solid rgba(127, 127, 127, 0.35);
  border-radius: 8px; box-shadow: 0 10px 26px rgba(0, 0, 0, 0.24); padding: 6px; display: none; }
.menu.open .menu-items { display: block; }
.menu-item { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; background: none; border: 0;
  padding: 7px 10px; border-radius: 6px; cursor: pointer; font: inherit; font-size: 12.5px; color: var(--ink); }
.menu-item .ico { width: 15px; height: 15px; color: var(--muted); flex-shrink: 0; }
.menu-item:hover:not([disabled]) { background: var(--accent-tint); }
.menu-item:hover:not([disabled]) .ico { color: var(--red-text); }
.menu-item[disabled] { opacity: 0.45; cursor: default; }
.menu-sep { height: 1px; background: rgba(127, 127, 127, 0.3); margin: 5px 4px; }
.menu-item.theme-opt.is-active::after { content: "✓"; color: var(--red-text); font-weight: 700; margin-left: auto; }

/* ---------- Stale-results banner ---------- */
.stale-banner { background: #8a5a00; color: #ffe9c2; font-size: 12.5px; padding: 7px 16px;
  text-align: center; border-bottom: 1px solid rgba(0, 0, 0, 0.25); }
[data-theme="light"] .stale-banner {
  background: #fff3d6; color: #7a4d00; border-bottom: 1px solid #e8c77a; }

/* ---------- About dialog ---------- */
/* Scrollable overlay + capped card. A dialog taller than the viewport — About in
   landscape on a phone, or Feedback with the on-screen keyboard up — used to be
   unreachable: no scroll on the overlay, no max-height on the card. */
.about-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.45); z-index: 2000;
  display: flex; align-items: center; justify-content: center; overflow-y: auto; padding: 20px 0; }
.about-card { background: var(--card-bg); color: var(--ink); border-radius: 12px;
  max-height: calc(100dvh - 40px); overflow-y: auto;
  padding: 24px 28px; max-width: 520px; width: calc(100% - 48px);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.35); border: 1px solid rgba(127, 127, 127, 0.3); }
.about-card h3 { margin: 0 0 12px; font-size: 15px; }
.about-line { display: flex; justify-content: space-between; margin: 4px 0; font-size: 12.5px; }
.about-line span { opacity: 0.65; }
.about-note { font-size: 12px; line-height: 1.55; margin: 10px 0; }
.about-card .mini-btn { margin-top: 8px; }

/* Feedback dialog (Help → Send Feedback…) — reuses the About card shell. */
.fb-text { margin-top: 4px; font-family: var(--sans); white-space: pre-wrap; min-height: 110px; }
.fb-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.fb-status { min-height: 16px; margin-bottom: 0; }

/* ===========================================================================
   RESPONSIVE — phone, tablet, desktop
   ===========================================================================
   The workspace is a desktop three-pane layout — 266px tree | results | 396px
   rail — each pane owning its own scroll inside a body that never scrolls. Three
   tiers, and the reasoning for each:

     DESKTOP   > 1180px
               Unchanged: all three panes side by side.

     TABLET    761-1180px (and any width up to 1180px taller than 520px)
               The rail no longer fits beside the results. It used to be
               `display: none`, which silently removed the sky plot, the
               spacecraft preview and the station map from every tablet in
               portrait. It now folds to a band UNDER the results, spanning the
               results column, with its own scroll and a capped height. The tree
               stays pinned — at 768px it still leaves ~470px of readable results,
               and a visible tree is worth more than the width it costs.

     PHONE     <= 760px, OR shorter than 520px up to 1180px wide
               The tree becomes an overlay drawer on the toolbar hamburger
               (`body.nav-open`). The SHORT condition is what catches a phone in
               landscape (844x390): wide enough for a pinned tree, but with only
               ~210px of workspace height there is no room to spend on one.
               Previously the tree was `display: none` below 880px, which left a
               phone user stranded on whichever section happened to be active —
               it is the app's only navigation.

   Every pane track is minmax(0, ...): a 9-column pass table or a wide Plotly SVG
   must be allowed to overflow its pane and scroll, not to stretch the grid track
   and push the whole layout sideways.

   The phone media condition below is duplicated in app.js as NAV_DRAWER_MQ.
   Change one, change the other.
   =========================================================================== */

/* ---------- TABLET and below: the rail folds under the results ---------- */
@media (max-width: 1180px) {
  .app-body {
    grid-template-columns: 240px minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) auto;
  }
  .tree-panel  { grid-column: 1; grid-row: 1 / span 2; }
  .main-panel  { grid-column: 2; grid-row: 1; }
  /* Was display:none — same content, now a band under the results. */
  .rail-panel {
    display: block; grid-column: 2; grid-row: 2;
    border-left: 0; border-top: 1px solid var(--border);
    max-height: 40vh; padding: 14px 18px;
  }
  /* A section with no rail content collapses the band away entirely. Columns are
     restated, not inherited: .app-body.no-rail outranks .app-body. */
  .app-body.no-rail {
    grid-template-columns: 240px minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
  }
  .app-body.no-rail .rail-panel { display: none; }
  .inspector-inner { max-width: none; padding: 24px 28px 48px; }
  .app-body.no-rail .inspector-inner { max-width: 900px; }
}

/* ---------- PHONE: tree becomes a drawer ----------
   Narrow OR short. Keep this condition in sync with NAV_DRAWER_MQ in app.js. */
@media (max-width: 760px), (max-height: 520px) and (max-width: 1180px) {
  .nav-toggle { display: inline-flex; }

  /* No tree track: the results column takes the whole width. Both selectors,
     since .app-body.no-rail is more specific than .app-body. */
  .app-body, .app-body.no-rail { grid-template-columns: minmax(0, 1fr); }
  .main-panel { grid-column: 1; grid-row: 1; }
  .rail-panel { grid-column: 1; grid-row: 2; }

  /* ONE scroll container, not two. On tablet the live-view band is worth its
     own scroll because it sits beside a tall results column, but on a phone a
     40vh band at the bottom of a ~570px workspace left barely two input fields
     visible at once. Here the panes share the workspace scroller: the inputs get
     the full height, and the preview follows below them one scroll away rather
     than holding screen space open for itself.

     Rows are auto/auto so each pane is its natural height and .app-body — which
     the body grid already bounds at minmax(0, 1fr) — does the scrolling. */
  .app-body, .app-body.no-rail { grid-template-rows: auto auto; overflow-y: auto; }
  .main-panel { overflow: visible; }
  .rail-panel { max-height: none; overflow: visible; }

  /* The tree leaves the grid. Off-canvas by transform (not display) so it
     animates, plus visibility so a closed drawer is neither a tab stop nor a
     screen-reader target. */
  .tree-panel {
    display: flex;
    position: fixed; top: 0; bottom: 0; left: 0;
    width: min(86vw, 320px); z-index: 1500;
    border-right: 1px solid var(--border);
    box-shadow: 6px 0 28px rgba(0, 0, 0, .28);
    transform: translateX(-100%); visibility: hidden;
    transition: transform .22s cubic-bezier(.4, 0, .2, 1), visibility .22s;
  }
  body.nav-open .tree-panel { transform: none; visibility: visible; }

  /* Touch targets: on desktop these tree rows are a dense pointer control; here
     they are the primary navigation and need a thumb-sized hit area. */
  .tree-root, .tree-leaf { padding-top: 11px; padding-bottom: 11px; }
  .tree-header { padding: 16px 18px 8px; }
}

/* ---------- NARROW WIDTH (includes landscape phones) ---------- */
@media (max-width: 880px) {
  /* Justified text needs a wide measure. At ~40 characters per line it opens
     rivers of white space between words, so narrow columns set ragged-right. */
  .section-intro, .rail-caption, .about-note { text-align: left; }
  .section-intro { font-size: 14px; margin-bottom: 20px; }
  .panel-section h2::after { margin: 11px 0 13px; }

  /* Toolbar height is the scarcest thing on a phone: four wrapped rows was
     ~40% of the viewport. Collapse the connection pill to its status dot (the
     wording stays in aria-label/title) and tighten the control paddings. */
  .conn-status { padding: 6px 9px; }
  .conn-status .conn-text { display: none; }
  .conn-status .conn-short { display: inline; }
  .btn { padding: 7px 12px; }
  .menu-btn { padding: 6px 9px; }
  .menubar, .toolbar-actions { padding: 3px 0; }
  .toolbar-left { padding: 4px 0; }

  /* Two toolbar rows on a phone is one too many: the brand row drops its
     subtitle and divider, and menus/actions wrap as needed. */
  .toolbar { padding: 3px 12px; }
  .app-subtitle, .toolbar-left .toolbar-divider { display: none; }
  .menubar { margin-left: 0; }
  .toolbar-actions { gap: 6px; flex-wrap: wrap; }
  .preset-select { max-width: 132px; }

  /* Dropdown menus were `position: absolute; left: 0; min-width: 218px` with no
     viewport clamp, so File/View/Help ran off the right edge. Pin to the
     viewport instead. */
  .menu-items {
    position: fixed; left: 8px; right: 8px; top: auto;
    width: auto; min-width: 0; max-height: 70dvh; overflow-y: auto;
  }
  .menu-item { padding: 11px 12px; }

  .inspector-inner { padding: 18px 16px 40px; }
  .rail-panel { padding: 12px 16px; }     /* height: see the phone block above */
  .breadcrumb { margin-bottom: 14px; }

  /* Charts: the desktop fixed heights (380/340/540/360px) eat a phone screen.
     Tall enough to read, but proportional. */
  .chart-box            { height: clamp(240px, 46vh, 380px); min-height: 0; }
  .chart-box.gt-map     { height: clamp(200px, 34vh, 340px); min-height: 0; }
  .chart-box.gantt-box  { height: clamp(300px, 58vh, 540px); min-height: 0; }
  .chart-box.sky-chart  { height: clamp(260px, 46vh, 360px); min-height: 0; }
  .mission-timeline-box { min-height: 0; }

  /* Hero viewport: shorter, so there is always page above and below it to start
     a scroll from — see the touch-action note below. */
  .orbit-hero .viewport { height: min(32vh, 300px); }
  .viewport-label, .viewport-hint { font-size: 8.5px; letter-spacing: 1.2px; }
  .viewport-hint { display: none; }         /* "drag · scroll · right-drag" is mouse advice */
  .orbit-frame-legend { flex-wrap: wrap; gap: 7px; top: 32px; }

  /* Wide tables scroll horizontally instead of being clipped by the card's
     overflow:hidden. The pass tables already had a scrolling wrapper; the
     per-face table now has one too (.table-xscroll, above). This line used to
     name .face-table-flush, which IS the <table>, and a browser ignores
     overflow on display:table — so the per-face table was clipped anyway and
     179px of it was unreachable on a phone. */
  .panel-card-body { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .face-table th, .face-table td { padding-left: 10px; padding-right: 10px; }
  .face-table input, .face-table-flush td:nth-child(2) input,
  .face-table-flush td:nth-child(4) input.face-cellpct { max-width: none; min-width: 74px; }
  .pass-tablewrap { -webkit-overflow-scrolling: touch; }
  .pass-table th, .pass-table td { padding-left: 8px; padding-right: 8px; }

  /* Ground-track controls: the min-widths (96/88/150px) forced a 334px row. */
  .gt-btn, .gt-btn.gt-reset { min-width: 0; }
  .gt-slider { min-width: 100px; }
  .run-strip-left { min-width: 0; }
  .run-strip { padding: 7px 12px; }

  /* Log console: shorter, header stays tappable. */
  .output-log { height: 104px; }
  .output-log.collapsed { height: 40px; }
  .log-header { padding-top: 9px; padding-bottom: 9px; }
}

/* ---------- NARROW PHONE ---------- */
@media (max-width: 560px) {
  .inspector-inner { padding: 16px 12px 36px; }
  .panel-card-body { padding: 14px; }

  /* Every forced two-up grid becomes one column. At 320-390px a 2x2 of numeric
     inputs leaves ~120px per field, and .has-unit reserves 44px of that for the
     unit suffix. */
  .form-factor-grid, .hotcold-grid, .rail-readouts, .sk-case-row, .viewport-legend {
    grid-template-columns: minmax(0, 1fr);
  }
  .envelope-spec { border-left: 0; padding-left: 0; border-top: 1px solid var(--border-2); padding-top: 14px; }
  .elements-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .case-card { padding: 13px 15px 15px; }
  .run-strip { grid-template-columns: 1fr auto; }
  .run-strip-left { grid-column: 1 / -1; }
}
@media (max-width: 400px) {
  .elements-strip { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- Short viewports (landscape phone) ---------- */
@media (max-height: 520px) and (max-width: 1180px) {
  .output-log { height: 84px; }
  .output-log.collapsed { height: 38px; }
  .chart-box, .chart-box.gt-map, .chart-box.sky-chart { height: clamp(200px, 72vh, 340px); }
  .orbit-hero .viewport { height: min(56vh, 260px); }
}

/* ---------- Touch input ----------
   The WebGL globe needs the gesture to orbit, but claiming it outright
   (touch-action: none) traps the user: with the hero filling much of a phone
   screen there would be nowhere left to start a scroll. `pan-y` is the
   compromise — vertical drags scroll the page, horizontal drags rotate the
   globe, which is the axis that matters for reading an orbit. Pointer devices
   keep full control. */
@media (pointer: coarse) {
  #orbitViewport, #orbitViewport canvas { touch-action: pan-y; }
}
@media (pointer: fine) {
  #orbitViewport, #orbitViewport canvas { touch-action: none; }
}

/* Hover styles stick after a tap on touch devices — the tapped row or menu item
   stays highlighted until you tap elsewhere, which reads as a selection that is
   not one. Give the highest-traffic ones a real pressed state instead. */
@media (hover: none) {
  .pass-table tbody tr:hover { background: transparent; }
  .pass-clickable tbody tr:active { background: var(--hover-bg); }
  .menu-item:hover:not([disabled]) { background: transparent; }
  .menu-item:active:not([disabled]) { background: var(--accent-tint); }
  .tree-root:hover, .tree-leaf:hover { background: transparent; }
  .tree-root:active, .tree-leaf:active { background: var(--hover-bg); }
}

/* A pass table narrower than its 9 columns scrolls sideways, but nothing said so
   — on a phone it visibly ends at "Dur" and Max El, the azimuths and the
   direction were undiscoverable. app.js measures each table and sets
   .has-xscroll on its panel only when content really is off-screen. */
.has-xscroll { position: relative; }
.has-xscroll::after {
  /* The wording is per table and comes from the wrapper's data-xnote, which
     markScrollableTables copies onto the host. It used to be one hardcoded
     sentence here, which is why only the pass table could ever be captioned. */
  content: attr(data-xnote);
  display: block; font-family: var(--sans); font-size: 11px;
  color: var(--muted); text-align: right; margin: 6px 2px 0;
}
/* Inside a card the caption would otherwise sit hard against the rounded edge. */
.panel-card.has-xscroll::after { margin: 8px 20px 14px; }

/* Any table that is allowed to scroll sideways rather than be clipped by an
   ancestor's overflow:hidden. Costs nothing when the table fits. */
.table-xscroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ============================================================
   PLAN LOCKS
   ============================================================
   What a plan takes away, shown rather than hidden, and never in a dialog.
   The first version opened a modal on every locked click; it was tried and
   rejected, correctly, because a popup that stops the work to sell something
   is a consumer-software pattern and this is an engineering tool. What is left
   is a padlock, a tooltip, a line in the output log, and a quiet notice inside
   the panel you actually opened.

   The lock uses --muted and a padlock glyph, NOT the error red. Not being on
   the paid plan is not a mistake the user made. */

.tree-leaf.leaf-locked { color: var(--muted); }
.tree-leaf.leaf-locked::after {
  content: "\1F512"; font-size: 9px; opacity: .65;
  filter: grayscale(1);
  /* The bullet dot and the padlock are the SAME pseudo-element, so the dot's
     geometry has to be undone or the glyph is painted inside a 4px circle and
     ordered ahead of the label, which is what it was doing. A locked row shows
     the padlock instead of its dot, at the right edge, where the root rows
     already put their status chip. */
  width: auto; height: auto; border-radius: 0; background: none;
  order: 0; margin-left: auto; line-height: 1;
}
.tree-leaf.leaf-locked:hover { background: var(--hover-bg); color: var(--muted); }

/* A results panel the plan does not include. It OPENS, keeps its heading and
   its description, and replaces the data with one line. Hiding it outright
   answered "where did it go" instead of "what would this have told me". */
.plan-locked-section > *:not(h2):not(.section-intro):not(.plan-locked-note) { display: none !important; }
.plan-locked-note {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin-top: 18px; padding: 14px 16px;
  background: var(--hover-bg); border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong); border-radius: 10px;
  font-family: var(--sans); font-size: 13px; line-height: 1.5; color: var(--muted);
}
.plan-locked-note .pln-lock { filter: grayscale(1); opacity: .7; font-size: 12px; }
.plan-locked-note .pln-text { flex: 1 1 240px; }
.plan-locked-note .pln-link { color: var(--red-text); text-decoration: none; font-weight: 600; white-space: nowrap; }
.plan-locked-note .pln-link:hover { text-decoration: underline; }

.is-plan-locked { cursor: pointer; opacity: .55; }
.is-plan-locked::after {
  content: "\1F512"; font-size: 9px; margin-left: 6px; filter: grayscale(1); opacity: .8;
}
.menu-item.is-plan-locked { justify-content: flex-start; }
.qat-btn.is-plan-locked::after { content: none; }   /* icon-only, no room */
.qat-btn.is-plan-locked { opacity: .45; }
/* A locked control must not also look disabled-because-you-have-not-run-yet:
   the plan lock wins, and it stays clickable so it can explain itself. */
.is-plan-locked:disabled { cursor: pointer; pointer-events: auto; }

/* The Run button when the day's analyses are spent. Quiet, not alarming, and
   it states the number: a disabled Run with no explanation is indistinguishable
   from a broken one. */
.btn.run-spent, .btn.run-spent:disabled {
  background: var(--btn-bg); color: var(--muted);
  border: 1px solid var(--border-strong); cursor: not-allowed;
  font-variant-numeric: tabular-nums;
}
.btn.run-spent svg { display: none; }
