/* Contract intel, built to read as an EVE station window.
 *
 * Design brief taken from CCP's own UI modernization writeup rather than from
 * impressions:
 *
 *   "Everything is made from light and behaves similar to light. Objects are
 *    dim and darkened or de-saturated when off/inactive."
 *
 * Consequences, applied literally:
 *   - Inactive things are DIMMED, not greyed with a different hue. Every
 *     resting state here is the active state at lower luminance.
 *   - Interaction is glow, never a raised or beveled button.
 *   - Panels are blurred, desaturated see-through surfaces over the void, the
 *     way station windows sit over the hangar.
 *   - Chrome iconography is monochrome. CCP: "stripping color away from such
 *     icons reduces search time, as it allows your brain to focus purely on
 *     form." Colour is therefore reserved for meaning: verdicts and security.
 *
 * Item art comes from images.evetech.net, which CCP explicitly offers as a CDN
 * for third-party apps. No CCP logo or wordmark appears anywhere, because the
 * Developer License forbids combining CCP marks with other marks.
 *
 * Orbitron (display) and Rajdhani (body) are self-hosted below, not hotlinked.
 * CCP serves their own Shentox from CCP's CDN, and hotlinking THAT would be
 * both rude and a single point of failure; these are SIL Open Font License
 * fonts served from this project's own /fonts/, so neither problem applies.
 */

:root {
  /* The one declaration that fixes the unreadable dropdown.
   *
   * A <select>'s closed box is ours to style. The OPEN popup is not: it is
   * drawn by the OS, and with no color-scheme declared the OS assumes light
   * and paints it white, while the options inherit --ink-dim (#7d94a8) from
   * the page. Pale blue-grey on white, which is roughly a 2:1 contrast ratio
   * and unreadable, and no amount of styling the select itself touches it,
   * because the popup is not in the page.
   *
   * Declaring the scheme is what tells the OS to draw its own dark popup. The
   * explicit option rules below are belt and braces for the browsers that
   * honour them; this line is the one that does the work. */
  color-scheme: dark;

  --void: #05090f;
  --hangar: #070c15;

  /* Window surfaces. Translucent so the void reads through them. */
  --win: rgba(13, 20, 28, 0.82);
  --win-head: rgba(20, 31, 43, 0.9);
  --win-inactive: rgba(11, 17, 24, 0.66);

  --line: rgba(90, 130, 160, 0.18);
  --line-hi: rgba(110, 165, 200, 0.34);

  --ink: #ccdae6;
  --ink-dim: #7d94a8;
  /* Lifted from #4e6478 (2026-09-24 typography pass): the original read
     3.0:1 against a panel surface, below WCAG AA's 4.5:1 floor for text.
     #6987a2 reads ~5.0:1 against the same panel while staying in the same
     muted blue-grey family, dimmer than --ink-dim, still dark-EVE. */
  --ink-faint: #6987a2;

  /* Capacitor cyan. The only interface colour. */
  --lume: #6fdcea;
  /* Lifted from #2c7f8c for the same reason: 4.0:1 against a panel, below
     AA. #308c9a reads ~4.75:1, same hue, same role as the borders/dim
     accents that make up most of its uses. */
  --lume-dim: #308c9a;
  --lume-wash: rgba(111, 220, 234, 0.1);

  /* Meaning colours, used ONLY for verdicts and security bands. */
  --up: #56d29a;
  --hot: #e5aa4a;
  --down: #e2615f;

  --mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Mono", Menlo, monospace;
  --sans: "Rajdhani", "Eurostile", "Bahnschrift", "DIN Alternate", ui-sans-serif, system-ui, -apple-system, sans-serif;
  --display: "Orbitron", "Eurostile", "Bahnschrift", ui-sans-serif, system-ui, sans-serif;

  /* Role aliases, adopted from Cepheus's token architecture (see
     ROADMAP-KRYPTCO-PLATFORM.md section 4): background / card / card-head /
     primary / secondary / muted / border / destructive. KryptCo keeps its
     own values (translucent glass over a near-black void, capacitor cyan,
     not Cepheus's flat navy/cyan) and wires the same ROLE NAMES onto them,
     so a future module can be built against roles instead of re-deriving
     which of the names above means what. The names above stay canonical;
     nothing here renames or removes them. */
  --background: var(--void);
  --card: var(--win);
  --card-head: var(--win-head);
  --primary: var(--lume);
  --secondary: var(--ink-dim);
  --muted: var(--ink-faint);
  --border: var(--line);
  --destructive: var(--down);

  /* Cepheus's gold "Pilot Login" CTA, adopted for our closest equivalent,
     #reauth. Its own token rather than --hot, so a verdict amber and a CTA
     amber can diverge later without one change moving both. */
  --action: #e5aa4a;

  /* Cepheus radius, documented for future modules. Every control here is
     already sharper than this (border-radius: 0, deliberately, "sharp
     corners, military feel"), so nothing below is rounded to reach it. */
  --radius: 2px;

  --rail: 180px;

  /* One horizontal-inset token for the phone shell (KrypCo session 19,
     mobile usability pass). Every phone-width padding below reads this
     instead of its own px value, so a second single-class rule with its own
     shorthand can never silently zero the edge gutter the way it has on
     other builds in this fleet. */
  --gutter: 1.25rem;

  /* UI-REFRESH.md section 4: semantic colour ALIASES, not new hues. Every
     name below points at a colour this file already had a job for
     (--up/--hot/--down for verdicts, --lume for the one interface accent,
     --ink-faint for de-emphasis), so a shared component written against
     "good/warn/danger/info/muted" stays visually identical to the existing
     .tag/.stat/.fresh-chip readouts that already use the underlying token. */
  --good: var(--up);
  --warn: var(--hot);
  --danger: var(--down);
  --info: var(--lume);
  --muted: var(--ink-faint);

  /* Sides. "Ours" reuses the interface cyan family so an owned moon or fleet
     never competes with --up/--down for the eye; "theirs" is --down, same red
     as a loss, because a hostile IS the loss case. Neutral is --ink-faint,
     already the de-emphasis colour everywhere else. Unknown gets the one net
     -new hue on the page, because "we cannot tell" must never read as either
     side, and violet has no other job here (see --group-corp, which reuses
     it deliberately -- Recruit rows are exactly the "not yet a side" case). */
  --side-ours: #5b9bd9;
  --side-theirs: var(--down);
  --side-neutral: var(--ink-faint);
  --side-unknown: #a586e0;

  /* EVE security-status bands, applied to a system's sec value wherever one is
     shown. 1.0-0.5 runs blue to green (highsec), 0.4-0.1 is orange (lowsec),
     0.0 and below is red (nullsec/wormhole). secColor()/secClass() in app.js
     compute the exact step; these four are the anchors that walk. */
  --sec-hi: #56d2ea;
  --sec-mid: var(--up);
  --sec-low: #e5a54a;
  --sec-null: var(--down);

  /* Rail group accents (UI-REFRESH.md section 2). Used ONLY for a group's
     active rail item, that item's tab-dot when lit, and the page header rule
     of a view belonging to the group -- nowhere else, so colour stays
     wayfinding rather than becoming wallpaper. Command keeps the existing
     brand cyan (--lume) rather than inventing a fifth hue for it. */
  --group-command: var(--lume);
  --group-market: #e5c24a;
  --group-war: var(--down);
  --group-corp: var(--side-unknown);

  /* Spacing scale. Every gap/padding in a NEW shared component reads one of
     these; existing per-view rules keep their own literal values rather than
     being rewritten to match (out of scope: "other tabs keep their current
     internals"). */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;

  /* Type scale. Every font-size in this file reads one of these; nothing
     declares a loose px value. --fs-xs is the ABSOLUTE FLOOR: nothing on the
     page, including uppercase labels, badges, table headers and chips, goes
     smaller than this. --fs-base is body copy. --fs-2xl is reserved for the
     ops-bar's big mono stat numbers.
     Rajdhani ships only 400/600/700 (see @font-face below) -- there is no
     500 weight file, so body copy uses 600 (the nearest AVAILABLE weight at
     or above the 500-minimum floor; requesting 500 with no 500 face present
     resolves to 400, which is the exact thin-on-dark problem this exists to
     avoid). */
  --fs-xs: 12px;
  --fs-sm: 13px;
  --fs-base: 15px;
  --fs-md: 16px;
  --fs-lg: 18px;
  --fs-xl: 22px;
  --fs-2xl: 28px;
}

/* Under 560px the floor and the body size both step up: --fs-xs from 12px to
   13px, --fs-base from 15px to 16px. Every other token is unchanged, so a
   label or a table header that already reads --fs-xs (or body copy reading
   --fs-base) gets bigger on a phone without touching the rule that uses it. */
@media (max-width: 560px) {
  :root {
    --fs-xs: 13px;
    --fs-base: 16px;
    --gutter: 1.5rem;
  }
}

/* Orbitron (variable, 400-900) and Rajdhani (400/600/700), both SIL Open
   Font License, both self-hosted from /fonts/. unicode-range is the Latin
   subset both families ship; font-display: swap shows the system fallback
   immediately rather than a blank heading while these load. */
@font-face {
  font-family: "Orbitron";
  src: url("/fonts/orbitron-var.woff2") format("woff2");
  font-weight: 400 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Rajdhani";
  src: url("/fonts/rajdhani-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Rajdhani";
  src: url("/fonts/rajdhani-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Rajdhani";
  src: url("/fonts/rajdhani-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* =========================================================================
   SHARED COMPONENTS (UI-REFRESH.md section 4)

   Built for Overview, the sample module for this phase, and documented here
   so later phases (see the two FACETS regions below) reuse these names
   instead of re-deriving them per tab. Every existing view keeps its own
   markup and class names (.win-head/.win-body/.stat/.tag/.table-wrap etc,
   defined further down this file) -- out of scope to rewrite for a
   shell-only phase -- but where a shared name is a straight synonym for one
   that already existed (.panel/.panel-head/.panel-body for section/.win-head
   /.win-body, .table-scroll for .table-wrap), the two are wired together so
   a new view can use either without a second implementation to keep in sync.

   - .panel / .panel-head / .panel-body : one bordered, blurred station-
     window surface with a header row (title + .info + actions on the right)
     and a padded body. Aliases of section/.win-head/.win-body, defined
     alongside those, further down.
   - .tile-group / .tile-group-label / .tile-row / .tile : a labelled group of
     at most 4 tiles. .tile--lead on the one headline tile in a group makes
     its number larger. Colour modifiers .tile.good/.warn/.danger/.info tint
     the number only, same palette as .chip below. .tile-hint is a second,
     muted line for context a bare number does not carry.
   - .chip : a small pill for a verdict or a category. Modifiers .good/.warn
     /.danger/.info/.muted (verdicts, same colours as .tag) and .ours/
     .theirs/.neutral/.unknown (sides). Already in use for the sidebar's ESI
     status chip (setEsiChip() in app.js).
   - .table-scroll : add alongside .table-wrap for a visible edge fade on a
     horizontally-scrollable table, defined next to .table-wrap further down.
   - .empty / .empty.loading / .empty.error : the three states every panel
     resolves to -- data, "nothing here" in words, or a failed read in
     words. A loading state must always end in one of the other two; see
     .empty.loading's keyframe further down and loadOverview()'s catch in
     app.js for the error case, since a bare try/catch that swallows a fetch
     failure and renders the empty-state copy reports a broken read as a
     quiet Tuesday. Defined alongside .empty further down.
   - .info : the round "i" tooltip badge next to a section/tile/column/chip
     (UI-REFRESH.md section 5). A real button, not just a hover target:
     `aria-label` carries the full explanation (a title attribute alone is
     invisible without a mouse), and initInfoTooltips() in app.js shows a
     floating popover on hover, focus AND tap, closing on Escape or an
     outside tap -- native `title` covers hover only, which is why the JS
     layer exists. Any element with class .info and an aria-label opts in
     automatically; existing `.info` spans elsewhere on the page that carry
     only a `title` (out of scope to rewrite this phase) are untouched and
     keep their native hover tooltip. Defined alongside the original .info
     rule further down.
   ========================================================================= */

.tile-group { margin: 0 0 var(--sp-4); }
.tile-group:last-child { margin-bottom: 0; }
.tile-group-label {
  display: block; margin: 0 0 var(--sp-1);
  font: 600 var(--fs-xs) var(--sans); text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--muted);
}
/* At most 4 per row (spec), 2 under 560px. Hairline background shows through
   the 1px gap as the grid rule, same trick .stats already uses. */
/* auto-fit, not a fixed 4: a 3-tile group used to leave an empty painted cell.
   150px floor gives 4 across on desktop and 2 across at 375px. */
.tile-row {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px; background: var(--line);
}
/* A tile whose value is a sentence ("No timer due") stays body-sized even as a lead. */
.tile .tile-value--words, .tile--lead .tile-value--words { font-size: var(--fs-md); font-weight: 600; }
.tile {
  position: relative; background: var(--win-head);
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
}
.tile-label {
  display: flex; align-items: center; gap: 4px;
  font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted);
}
.tile-value {
  font: 600 var(--fs-xl) var(--mono); font-variant-numeric: tabular-nums;
  color: var(--ink); letter-spacing: -0.02em;
}
.tile--lead .tile-value { font-size: var(--fs-2xl); }
.tile-hint { font-size: var(--fs-xs); color: var(--muted); }
.tile.good .tile-value { color: var(--good); }
.tile.warn .tile-value { color: var(--warn); }
.tile.danger .tile-value { color: var(--danger); }
.tile.info .tile-value { color: var(--info); }

/* Scoped to `.chip` PLUS a verdict/side modifier, never a bare `.chip`: this
   file already overloads the class name for an unrelated existing pattern,
   the owner/bpc filter TOGGLE buttons (`class="chip"`, `class="chip on"`,
   `data-owner`/`data-bpc-filter`, further down this file), which carry no
   modifier and lean on the generic `button` rule for their look. A bare
   `.chip{}` base rule here would win on specificity and repaint every one of
   those pill-shaped instead of the house's sharp 2px corners -- reproduced
   live on the Blueprints tab's ALL/DEALS/OVERPRICED filter row while wiring
   this up. Requiring a modifier keeps this component from touching them. */
.chip.good, .chip.warn, .chip.danger, .chip.info, .chip.muted,
.chip.ours, .chip.theirs, .chip.neutral, .chip.unknown {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 9px; border: 1px solid var(--line); border-radius: 999px;
  font: 600 var(--fs-xs) var(--sans); text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--ink-dim); background: rgba(255, 255, 255, 0.02);
}
.chip.good { color: var(--good); border-color: rgba(86, 210, 154, 0.4); background: rgba(86, 210, 154, 0.08); }
.chip.warn { color: var(--warn); border-color: rgba(229, 170, 74, 0.4); background: rgba(229, 170, 74, 0.08); }
.chip.danger { color: var(--danger); border-color: rgba(226, 97, 95, 0.4); background: rgba(226, 97, 95, 0.08); }
.chip.info { color: var(--info); border-color: var(--lume-dim); background: var(--lume-wash); }
.chip.muted { color: var(--muted); border-color: var(--line); }
.chip.ours { color: var(--side-ours); border-color: var(--side-ours); background: rgba(91, 155, 217, 0.1); }
.chip.theirs { color: var(--side-theirs); border-color: rgba(226, 97, 95, 0.4); background: rgba(226, 97, 95, 0.08); }
.chip.neutral { color: var(--side-neutral); border-color: var(--line); }
.chip.unknown { color: var(--side-unknown); border-color: var(--side-unknown); background: rgba(165, 134, 224, 0.1); }

/* EVE security-status colour on a bare sec value (secColorClass() in
   app.js). Text only, no chip chrome: this decorates a number already sitting
   in a table cell or a muted line, not a standalone badge. */
.sec-hi { color: var(--sec-hi); }
.sec-mid { color: var(--sec-mid); }
.sec-low { color: var(--sec-low); }
.sec-null { color: var(--sec-null); }

* { box-sizing: border-box; }
[hidden] { display: none !important; }
/* Explicit size, not the UA default: <small> otherwise shrinks to 83.3% of
   its parent with no rule of its own, which put dozens of secondary-text
   spots (moon event timestamps, lifecycle labels, td notes) under the 12px
   floor once the parent's own size was tokenized. */
small { font-size: var(--fs-xs); }

body {
  margin: 0;
  color: var(--ink);
  font: 600 var(--fs-base)/1.45 var(--sans);
  -webkit-text-size-adjust: 100%;
  background:
    radial-gradient(1200px 620px at 72% -14%, rgba(111, 220, 234, 0.055), transparent 62%),
    radial-gradient(820px 460px at 2% 108%, rgba(40, 95, 140, 0.06), transparent 60%),
    linear-gradient(180deg, var(--hangar), var(--void) 60%);
  background-attachment: fixed;
  min-height: 100vh;
}

/* Hangar deck: a faint horizon line low on the page, the way the station
   interior sits under the UI when docked. */
body::before {
  content: "";
  position: fixed; inset: auto 0 0 0; height: 42vh; pointer-events: none; z-index: 0;
  background:
    linear-gradient(180deg, transparent, rgba(111, 220, 234, 0.028) 62%, transparent),
    repeating-linear-gradient(0deg, transparent 0 3px, rgba(0, 0, 0, 0.16) 3px 4px);
  mask-image: linear-gradient(180deg, transparent, #000 70%);
}

main { position: relative; z-index: 1; }

.skip {
  position: absolute; left: -999px; top: 0; background: var(--win-head);
  padding: 8px 12px; z-index: 30;
}
.skip:focus { left: 8px; top: 8px; }

/* Visually hidden, still read aloud. Lost when the rail CSS was cut out, which
   dumped the page h1 and the ignore column's header into the layout as visible
   text. Restored here. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ---- sidebar shell ------------------------------------------------------

   Chrome moved from the old icon rail (removed) and the top .bar (also
   removed) into a fixed left sidebar, matching cepheuscorp.com's structure:
   logo block, module nav, status footer. --rail carries the reserved width
   for main's offset, the same job it did when it was defined at 0. */
.sidebar {
  position: fixed; inset: 0 auto 0 0; width: var(--rail); z-index: 20;
  display: flex; flex-direction: column;
  background: linear-gradient(180deg, rgba(10, 15, 22, 0.97), rgba(6, 10, 16, 0.97));
  border-right: 1px solid var(--border);
  overflow-y: auto;
}
.sidebar-logo {
  display: flex; align-items: center; gap: 9px;
  padding: 16px 14px 14px;
  border-bottom: 1px solid var(--border);
}
.sidebar-logo-text { display: flex; flex-direction: column; min-width: 0; }
.sidebar-brand {
  font-family: var(--display); font-weight: 700; font-size: var(--fs-md);
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink); line-height: 1.15;
}
.sidebar-sub {
  font-size: var(--fs-xs); font-weight: 600; color: var(--secondary);
  text-transform: uppercase; letter-spacing: 0.06em; margin-top: 3px;
}
/* The brand mark. It carries the status lamp's job: a slow pulse means the
   page is still polling. */
.mark {
  width: 7px; height: 7px; background: var(--lume); flex: none;
  box-shadow: 0 0 10px var(--lume);
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
  animation: pulse 3.6s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
@media (prefers-reduced-motion: reduce) { .mark { animation: none; } }

.sidebar-nav { display: flex; flex-direction: column; gap: 2px; padding: 10px 8px; }
.nav-tab {
  display: flex; align-items: center; gap: 9px;
  background: none; border: 0; border-left: 2px solid transparent; border-radius: 0;
  min-height: 0; padding: 9px 10px;
  font: 600 var(--fs-xs) var(--sans); text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--secondary); cursor: pointer; text-align: left; text-decoration: none;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.nav-tab:hover { color: var(--ink); background: var(--lume-wash); box-shadow: none; }
/* Exactly one rail entry carries aria-current="page" (app.js navigate()).
   The fallback colour is the brand cyan, for Settings/Admin below the
   Neocom, which sit outside every .rail-group and so get no group accent. */
.nav-tab[aria-current="page"], .nav-tab[aria-selected="true"], .nav-tab.is-current {
  color: var(--primary); border-left-color: var(--primary); background: var(--lume-wash);
}
.nav-tab:focus-visible { outline: 1px solid var(--primary); outline-offset: -1px; }
.nav-dot {
  width: 5px; height: 5px; border-radius: 50%; background: currentColor; opacity: 0.55; flex: none;
}

/* ---- rail groups (UI-REFRESH.md section 2) ------------------------------

   Command/Market/War/Corp. Each group's accent (tokens above) is used for
   exactly four things: the group label itself (dimmed, so it reads as a
   heading and not wallpaper), its active rail item (here), that item's
   tab-dot when lit (setAlert in app.js sets colour via data-level, not the
   group, so this only affects the OPEN item), and its module's page-header
   rule (.page-header-text below). Nothing else reads a group colour.
   Phase 2 review: the group label was uniform --muted grey for all four
   groups, so the accent barely showed outside the one open item -- fixed by
   giving the label itself the group's colour at reduced weight. */
.rail-group { display: flex; flex-direction: column; gap: 2px; }
.rail-group + .rail-group { margin-top: 10px; }
.rail-group-label {
  padding: 8px 10px 3px;
  font: 600 var(--fs-xs) var(--sans); text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--muted);
}
.rail-group--command .rail-group-label { color: color-mix(in srgb, var(--group-command) 65%, var(--muted)); }
.rail-group--market .rail-group-label { color: color-mix(in srgb, var(--group-market) 65%, var(--muted)); }
.rail-group--war .rail-group-label { color: color-mix(in srgb, var(--group-war) 65%, var(--muted)); }
.rail-group--corp .rail-group-label { color: color-mix(in srgb, var(--group-corp) 65%, var(--muted)); }
.rail-group--command .nav-tab[aria-current="page"] { color: var(--group-command); border-left-color: var(--group-command); }
.rail-group--market .nav-tab[aria-current="page"] { color: var(--group-market); border-left-color: var(--group-market); }
.rail-group--war .nav-tab[aria-current="page"] { color: var(--group-war); border-left-color: var(--group-war); }
.rail-group--corp .nav-tab[aria-current="page"] { color: var(--group-corp); border-left-color: var(--group-corp); }

.sidebar-footer {
  margin-top: auto; padding: 12px 14px 16px; border-top: 1px solid var(--border);
  display: flex; flex-direction: column; align-items: flex-start; gap: 7px;
}
.sidebar-footer .who { font-size: var(--fs-xs); }
.sidebar-footer .btn { min-height: 26px; padding: 4px 9px; font-size: var(--fs-xs); }
.who { color: var(--lume); font-family: var(--mono); font-size: var(--fs-xs); letter-spacing: 0.05em; }
/* Cepheus's gold "Pilot Login" gets its own accent so it reads as THE action
   on a page that is otherwise entirely cyan. Was #login; the control is now
   always present (reconnect, not just first login), renamed to match. */
#reauth {
  color: var(--action); border-color: var(--action);
  background: rgba(229, 170, 74, 0.12);
}
#reauth:hover {
  color: var(--action); border-color: var(--action);
  background: rgba(229, 170, 74, 0.22);
  box-shadow: 0 0 0 1px rgba(229, 170, 74, 0.15), 0 0 14px rgba(229, 170, 74, 0.18);
}

/* ---- contract search -------------------------------------------------

   UI-REFRESH.md section 3: this used to be part of the top bar and sat over
   every tab even though only Contracts (and, via the same #q element,
   Build's product filter) reads it. Moved into Contracts's own view; the
   old sticky full-width top bar (".topstrip") is gone with it. */
.contracts-search { margin: 0 0 var(--sp-4); max-width: 360px; }
.contracts-search input[type="text"] { width: 100%; }

/* ---- controls: light, not relief -------------------------------------- */

select, .btn, input[type="text"], button {
  background: rgba(20, 31, 43, 0.72);
  color: var(--ink-dim);
  border: 1px solid var(--line);
  border-radius: 0;
  padding: 6px 10px;
  font: inherit; font-size: var(--fs-sm);
  min-height: 32px;
  letter-spacing: 0.03em;
  transition: color 0.15s, border-color 0.15s, background 0.15s, box-shadow 0.15s;
}
select {
  padding-right: 26px;
  /* Custom caret, so the control keeps its own box while the POPUP stays
   * native. appearance:none on the select does not restyle the popup, which is
   * why color-scheme on :root is the actual fix and this is only cosmetics. */
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-dim) 50%),
                    linear-gradient(135deg, var(--ink-dim) 50%, transparent 50%);
  background-position: right 12px center, right 7px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
select:hover {
  background-image: linear-gradient(45deg, transparent 50%, var(--lume) 50%),
                    linear-gradient(135deg, var(--lume) 50%, transparent 50%);
}
/* Honoured by Firefox and by Chromium on Windows and Linux. macOS Chromium and
 * Safari ignore these and use the OS popup, which is what color-scheme is for.
 * Both paths are covered rather than either one assumed. */
select option, select optgroup {
  background: #0d141c;
  color: var(--ink);
}
select option:checked { color: var(--lume); }
.btn, button { cursor: pointer; text-transform: uppercase; letter-spacing: 0.06em; font-size: var(--fs-xs); }
.btn { text-decoration: none; display: inline-flex; align-items: center; }

/* "burning light and glow to indicate button presses" */
select:hover, .btn:hover, input[type="text"]:hover, button:hover:not(:disabled) {
  color: var(--lume); border-color: var(--lume-dim);
  background: var(--lume-wash);
  box-shadow: 0 0 0 1px rgba(111, 220, 234, 0.09), 0 0 14px rgba(111, 220, 234, 0.11);
}
button:active:not(:disabled) { box-shadow: 0 0 18px rgba(111, 220, 234, 0.26) inset; }
button:disabled { opacity: 0.4; cursor: default; }
#refresh { color: var(--lume); border-color: var(--lume-dim); }

/* ---- layout ----------------------------------------------------------- */

main { max-width: 1260px; margin-left: var(--rail); padding: 22px 20px 80px; }

/* ---- page header (UI-REFRESH.md section 3) -----------------------------

   Replaces the old top bar (search over every tab, a duplicate Settings
   link, the "4:27 / 0:33 old" clock). One block per view: title, a
   one-sentence purpose, and on the right how fresh the data is in words and
   colour plus Scan now. app.js's renderPageHeader()/renderPageFreshness()
   keep it in sync with WIN_META and the refresh clock; navigate() sets
   [data-group] to the active module's rail group so the title rule below
   picks up that group's accent, the ONE other place (besides the rail
   itself) a group colour is allowed to appear. */
.page-header {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--sp-4); flex-wrap: wrap;
  margin: 0 0 var(--sp-5); padding: 0 0 var(--sp-3);
  border-bottom: 1px solid var(--line);
}
.page-header-text { min-width: 0; }
.page-title {
  margin: 0 0 4px; padding-left: 12px; position: relative;
  font-family: var(--display); font-weight: 700; font-size: var(--fs-xl);
  letter-spacing: 0.02em; color: var(--ink); text-transform: none;
}
.page-title::before {
  content: ""; position: absolute; left: 0; top: 3px; bottom: 3px; width: 3px;
  background: var(--group-command); box-shadow: 0 0 8px currentColor;
}
.page-header[data-group="market"] .page-title::before { background: var(--group-market); }
.page-header[data-group="war"] .page-title::before { background: var(--group-war); }
.page-header[data-group="corp"] .page-title::before { background: var(--group-corp); }
.page-purpose { margin: 0; padding-left: 12px; color: var(--ink-dim); font-size: var(--fs-sm); max-width: 62ch; }
.page-header-actions { display: flex; align-items: center; gap: var(--sp-3); flex: none; }
/* State lives in data-level, never a class: `.info` is the tooltip badge and
   a `page-freshness info` class turned the text into a badge mid-load. */
.page-freshness {
  font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.03em;
  color: var(--muted); cursor: help; white-space: nowrap;
}
.page-freshness[data-level="good"] { color: var(--good); }
.page-freshness[data-level="warn"] { color: var(--warn); }
.page-freshness[data-level="danger"] { color: var(--danger); }
.page-freshness[data-level="info"] { color: var(--info); }
@media (max-width: 560px) {
  .page-header { flex-direction: column; align-items: stretch; }
  .page-header-actions { justify-content: space-between; }
}
section + section { margin-top: 22px; }

/* ---- station windows -------------------------------------------------- */

/* Each section is a window: header bar, translucent blurred body, hairline
   frame. Inactive windows sit dimmer until hovered, per the light principle.
   `.panel:not(rect)` because `.panel` is ALSO an existing SVG class on the
   Threat map (`.map-svg .panel`, further down, sets its fill/stroke); this
   CSS-box styling (border/background/backdrop-filter) means nothing on an
   SVG shape in practice, but excluding it is the correct, guaranteed-safe
   fix rather than relying on that being true in every engine. */
section, .panel:not(rect) {
  border: 1px solid var(--line);
  background: var(--win-inactive);
  backdrop-filter: blur(11px) saturate(0.72) brightness(0.92);
  -webkit-backdrop-filter: blur(11px) saturate(0.72) brightness(0.92);
  transition: background 0.2s, border-color 0.2s;
}
section:hover, section:focus-within, .panel:not(rect):hover, .panel:not(rect):focus-within { background: var(--win); border-color: var(--line-hi); }

/* .panel-head/.panel-body are the UI-REFRESH.md section 4 names for this same
   header-row-plus-body shape; every existing view keeps its own .win-head/
   .win-body markup (out of scope to rename ~40 ids for a class name alone),
   and both names are kept equivalent here so neither reads as the "real" one. */
.win-head, .panel-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; flex-wrap: wrap;
  padding: 8px 14px;
  background: var(--win-head);
  border-bottom: 1px solid var(--line);
}
.win-body, .panel-body { padding: 14px; }

h1, h2, h3, h4 { font-family: var(--display); }

/* Phase 2 review: every section title was --fs-xs (12px, the type floor)
   spaced-out capitals ("NEXT TIMERS"), so a heading, a tile label and a
   column header all read as the same weight of text. UI-REFRESH.md section 4:
   uppercase is reserved for small labels (tile labels, column headers) from
   here on; a section title is sentence case (the h2 markup already IS
   sentence case, "Next timers"/"Last battle" -- only this rule was
   uppercasing it) at a clear step below the page title (--fs-xl). */
h2 {
  font-size: var(--fs-md); font-weight: 700; margin: 0;
  text-transform: none; letter-spacing: 0.01em; color: var(--lume);
  display: flex; align-items: center; gap: 9px;
}
/* Window title tick, the small lit mark EVE puts before a window name. */
h2::before {
  content: ""; width: 3px; height: 13px; background: var(--lume);
  box-shadow: 0 0 7px var(--lume-dim); flex: none;
}
.sub {
  font-size: var(--fs-xs); font-weight: 600; color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: 0.2em; margin: 24px 0 8px;
}
.note { margin: 0 0 12px; color: var(--ink-dim); font-size: var(--fs-sm); max-width: 76ch; }
.note strong { color: var(--ink); font-weight: 600; }
.inline {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--fs-xs); color: var(--ink-faint); white-space: nowrap;
  text-transform: uppercase; letter-spacing: 0.06em;
}
/* The one `.inline` on the page that is a full sentence rather than a short
   "Label [control]" pair (Moons > Assumptions). `.inline`'s nowrap is right
   for every other use of the class -- it is what keeps a filter's label
   glued to its select -- but forcing one un-wrapping line onto this sentence
   is what pushed the page's scrollWidth to 619px against a 375px phone
   viewport (KrypCo mobile pass, session 19): nothing else on the classic
   shell overflowed. Scoped to this element by id-sibling match rather than
   editing `.inline` itself, so every short label+control pair elsewhere
   keeps its nowrap. */
#h-moons-assumptions ~ .inline { white-space: normal; }

/* Same fix, same reason: REVIEW-C4-PRODUCT.md item 9's window note turned
   #hunt-snapshot-age into a full sentence too (was a short "Xm ago" before),
   and `.inline`'s nowrap pushed the Hunt tab's scrollWidth past 375px on
   phone the same way. */
#hunt-snapshot-age { white-space: normal; }

/* ---- readouts --------------------------------------------------------- */

/* Overview, tightened.
   The tiles were stacked (label above value) at 12-13px padding with a 23px
   number, so four short numbers occupied a full-width band with most of each
   tile empty. Label and value now share one baseline, which roughly halves the
   height and puts the number where the eye already is: hard right, against the
   next tile's rule. */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1px; background: var(--line); }
.stat {
  position: relative; background: var(--win-head);
  padding: 7px 12px 8px;
  display: flex; flex-direction: row; align-items: baseline;
  justify-content: space-between; gap: 10px;
}
.stat::before {
  content: ""; position: absolute; left: 0; top: 8px;
  width: 2px; height: 15px; background: var(--lume); box-shadow: 0 0 8px var(--lume-dim);
}
.stat .k { font-size: var(--fs-xs); color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.06em; }
.stat .v {
  font-size: var(--fs-xl); font-family: var(--mono); font-variant-numeric: tabular-nums;
  color: var(--ink); letter-spacing: -0.02em;
}

/* Tooltip affordance. The standing explanations used to sit above each table
   as a three-line paragraph, pushing the data itself below the fold. They are
   read once and then never again, so they live on this instead.
   Focusable and aria-labelled, because a title attribute alone is invisible to
   anyone not holding a mouse. */
.info {
  display: inline-flex; align-items: center; justify-content: center;
  width: 13px; height: 13px; border: 1px solid var(--lume-dim); border-radius: 50%;
  font-family: var(--mono); font-size: var(--fs-xs); line-height: 1; letter-spacing: 0;
  color: var(--lume-dim); cursor: help; text-transform: none; flex: none;
  background: none; padding: 0; min-height: 0; position: relative;
}
.info:hover, .info:focus { color: var(--lume); border-color: var(--lume); outline: none; }
.info:focus-visible { outline: 1px solid var(--lume); outline-offset: 2px; }

/* The floating explanation itself (UI-REFRESH.md section 5), built by
   initInfoTooltips() in app.js on hover, focus and tap of any `.info[aria-
   label]`, and removed on Escape, an outside tap, or the trigger losing
   focus/hover. Positioned in JS (fixed, clamped to the viewport) rather than
   with pure CSS ::after, because a button near the right edge of a 375px
   panel would otherwise push its tooltip off-screen. */
.info-tip {
  position: fixed; z-index: 50; max-width: 260px;
  padding: 7px 10px; border: 1px solid var(--line-hi);
  background: var(--win-head); color: var(--ink);
  font: 600 var(--fs-xs)/1.5 var(--sans); text-transform: none; letter-spacing: 0.01em;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.5);
  pointer-events: none;
}

/* ---- tables ----------------------------------------------------------- */

.table-wrap, .table-scroll { overflow-x: auto; border: 1px solid var(--line); }
/* .table-scroll (UI-REFRESH.md section 4 name) adds the visible edge fade a
   bare overflow-x scroller does not give you: a hairline gradient over the
   last few px on each side, present only while that side actually has more
   to scroll to (background-attachment: local scrolls the gradient WITH the
   content, so it tracks the true edge rather than sitting fixed over the
   viewport). Opt-in via the second class name so no existing .table-wrap
   changes appearance. */
.table-scroll {
  background:
    linear-gradient(90deg, var(--win-inactive), transparent 24px) left / 24px 100% no-repeat local,
    linear-gradient(270deg, var(--win-inactive), transparent 24px) right / 24px 100% no-repeat local;
}

/* Capped panes. Three region panels only beat one switched table if all three
   fit on screen at once, so each scrolls inside a fixed height instead of
   pushing the next one below the fold. The header sticks, which is only
   possible now that the pane is the vertical scroll container. */
/* Pane height comes from the viewport, not a pixel constant, so a pane uses
   more of a tall window and never turns into a short scrollbox on a big
   monitor. dvh accounts for mobile browser chrome showing/hiding; declared
   after the vh line so an engine that does not understand dvh just keeps the
   vh value instead of dropping the whole rule. min-height keeps a pane from
   collapsing to nothing on a short window. */
.table-wrap.capped {
  min-height: 240px;
  max-height: 52vh;
  max-height: 52dvh;
  overflow-y: auto;
}
/* Flagged is the headline list ("enlarge this list so we can see way more
   rows"), so it gets more of the viewport than the three region panels,
   search and market share. */
.table-wrap.capped.pane-primary {
  min-height: 320px;
  max-height: 68vh;
  max-height: 68dvh;
}
.table-wrap.capped thead th { position: sticky; top: 0; z-index: 2; }

/* REVIEW-C1-PRODUCT.md item 16: .capped's 240px min-height was sized for the
   sector/region panes it was built for, which always have rows. Applied to
   the Moons tab's Timerboard/Planner/Structures-destroyed too, it forced a
   "No timers" empty state to sit in a 300px+ box - Andy's own complaint
   ("taking way too much space"). .compact drops both the floor and the
   ceiling so these size to their actual content; still horizontally
   scrollable like every other dense table if a row ever needs it. */
.table-wrap.compact { min-height: 0; max-height: none; overflow-y: visible; }

/* Priority label on each panel head. The ranking between these three does not
   change, so it is written down rather than left to be remembered. */
.rank {
  font-size: var(--fs-xs); letter-spacing: 0.06em; text-transform: uppercase;
  padding: 2px 6px; border: 1px solid currentColor;
}
.rank-high { color: var(--up); }
.rank-mid { color: var(--hot); }
.rank-low { color: var(--ink-faint); }
.panel-primary { border-color: var(--line-hi); }
table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); line-height: 1.25; }
th {
  text-align: left; padding: 6px 9px;
  font-weight: 600; font-size: var(--fs-xs); color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: 0.06em;
  background: rgba(4, 7, 10, 0.6);
  border-bottom: 1px solid var(--line-hi);
  white-space: nowrap;
}
td { padding: 4px 9px; border-bottom: 1px solid rgba(90, 130, 160, 0.09); vertical-align: middle; }
tbody tr:last-child td { border-bottom: 0; }

/* Read as a matrix, not a stack of sentences.
   Column rules plus row banding give the eye two axes to track instead of one.
   Seven columns of mixed text and numbers are hard to follow across a wide row
   without a vertical rule to land on, and hard to follow DOWN a column without
   banding. Both are kept faint: the grid should be felt, not read. */
th, td { border-right: 1px solid rgba(90, 130, 160, 0.08); }
th:last-child, td:last-child { border-right: 0; }
tbody tr:nth-child(even) { background: rgba(90, 130, 160, 0.032); }

tbody tr { transition: background 0.12s, box-shadow 0.12s; }
/* After the banding so hover wins on both odd and even rows. */
tbody tr:hover { background: var(--lume-wash); box-shadow: inset 2px 0 0 var(--lume-dim); }

/* Numbers are the point of this table, so they get the monospace grid: equal
   digit widths mean a column of ISK lines up by magnitude and you can compare
   two rows by shape without reading either. */
td.num, th.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: var(--fs-sm); }
td.num { color: var(--ink); }

/* A good margin that has sat for a week keeps its number and loses its
   confident green. Outlined, no fill, ink-dim: still legible, no longer
   claiming to be easy money. Deliberately NOT hidden or sorted away, because
   it may still be worth taking if you have the hauler for it. */
.tag.stale-good {
  color: var(--ink-dim); background: none; border-style: dashed;
}
/* Age rides beside the margin rather than taking a column of its own. */
.age {
  margin-left: 5px; font-family: var(--mono); font-size: var(--fs-xs);
  color: var(--ink-faint); letter-spacing: 0;
}
.age.old { color: var(--hot); }

/* Ignore control. Its own narrow column at the far left, a full table away from
   OPEN at the far right, because one hides a row and the other puts a window on
   a live game client. Faint until the row is hovered: used rarely, and it must
   never compete with the numbers. */
.ignore-col { width: 20px; padding-left: 6px !important; padding-right: 0 !important; }
button.ignore {
  min-height: 0; padding: 0; width: 14px; height: 14px; line-height: 1;
  font-size: var(--fs-sm); letter-spacing: 0; border: 0; background: none;
  color: transparent; cursor: pointer;
}
tbody tr:hover button.ignore { color: var(--ink-faint); }
button.ignore:hover, button.ignore:focus-visible {
  color: var(--down); background: none; box-shadow: none; outline: none;
}
button.ignore.failed { color: var(--down); }
.empty { color: var(--ink-faint); padding: 20px 12px; }

/* A panel that is WORKING, as opposed to one that has nothing to show. The two
   were indistinguishable: Buy locally and List for sale each warm prices inline
   and take 6 to 10 seconds on a cold cache, and a motionless "Loading." for
   that long was reported as the dashboard being broken. The breathing opacity
   is the whole point, so reduced-motion gets a fixed brighter tone rather than
   dropping back to the identical dead grey. */
.empty.loading { color: var(--ink-dim); }
@keyframes empty-breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
.empty.loading { animation: empty-breathe 1.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .empty.loading { animation-name: none; opacity: 0.8; }
}
/* A panel that tried to load and failed, distinct from one that loaded and
   genuinely has nothing to show: "Couldn't read this" is not the same claim
   as "No timers running", and reporting the wrong one of the two is how a
   real outage reads as calm. */
.empty.error { color: var(--danger); }

/* ---- item art --------------------------------------------------------- */

/* The single strongest "this is EVE" signal available, and it is real game
   art served from CCP's own image CDN rather than anything reproduced here. */
.row-item { display: flex; align-items: center; gap: 10px; min-width: 0; }
.icon {
  width: 32px; height: 32px; flex: none;
  border: 1px solid var(--line);
  background: rgba(4, 7, 10, 0.55);
  object-fit: contain;
  image-rendering: -webkit-optimize-contrast;
}
tbody tr:hover .icon { border-color: var(--lume-dim); }
.row-text { min-width: 0; }

.contents { display: block; color: var(--ink-faint); font-size: var(--fs-xs); margin-top: 2px; line-height: 1.4; }
.cid { font-family: var(--mono); font-size: var(--fs-xs); color: var(--ink-faint); }

/* ---- tags ------------------------------------------------------------- */

.tag {
  display: inline-block; font-family: var(--mono); font-size: var(--fs-xs);
  padding: 2px 6px; letter-spacing: 0.03em;
  border: 1px solid currentColor; white-space: nowrap;
}
.tag.up   { color: var(--up);   background: rgba(86, 210, 154, 0.08); }
.tag.hot  { color: var(--hot);  background: rgba(229, 170, 74, 0.08); }
.tag.down { color: var(--down); background: rgba(226, 97, 95, 0.08); }
/* Was --ink-faint, which measures 2.89:1 against its own chip and fails WCAG AA
   for text this size. These are the neutral verdicts ("gates", "no route",
   "structure"), so they carry real meaning and cannot be the hardest thing on
   the page to read. --ink-dim measures 5.46:1. */
.tag:not(.up):not(.hot):not(.down) { color: var(--ink-dim); background: rgba(125, 148, 168, 0.08); }

/* ---- feeds ------------------------------------------------------------ */

.feed { list-style: none; margin: 0; padding: 0; border: 1px solid var(--line); }
.feed li {
  display: flex; align-items: center; gap: 11px; flex-wrap: wrap;
  padding: 9px 12px; border-bottom: 1px solid rgba(90, 130, 160, 0.09);
  transition: background 0.12s, box-shadow 0.12s;
}
.feed li:last-child { border-bottom: 0; }
.feed li:hover { background: var(--lume-wash); box-shadow: inset 2px 0 0 var(--lume-dim); }
.feed .name { flex: 1 1 240px; min-width: 0; }
.feed .meta { font-family: var(--mono); font-size: var(--fs-xs); color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.feed li.empty { display: block; }
.feed .del { background: none; border: 1px solid var(--line); color: var(--ink-faint); padding: 3px 8px; font-size: var(--fs-xs); min-height: 26px; }
.feed .del:hover { border-color: var(--down); color: var(--down); background: rgba(226, 97, 95, 0.08); box-shadow: none; }

/* ---- forms ------------------------------------------------------------ */

.rule-form { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; margin-top: 12px; }
.rule-form .note { margin: 0; flex: 1 1 100%; font-size: var(--fs-xs); }
input[type="text"] { min-width: 140px; font-family: var(--mono); color: var(--ink); }
input[type="text"]::placeholder { color: var(--ink-faint); }

/* ---- responsive ------------------------------------------------------- */

@media (max-width: 900px) {
  :root { --rail: 0px; }

  /* Phone shell (KrypCo session 19). Below this width, and whenever
     app.js's isPhoneUI() gate kept the window manager from ever building,
     body never carries `.wm`, so this is the ONLY nav these rules need to
     style -- there is no second, wm-scoped copy of the phone rail to keep in
     sync. The old fallback here (flex-wrap: wrap on twelve nav-tabs) is what
     produced the ~360px-tall rail Andy measured: replaced with one
     horizontally-scrollable row that holds its height. */
  .sidebar {
    position: sticky; top: 0; z-index: 25;
    width: auto; flex-direction: column; align-items: stretch;
    border-right: 0; border-bottom: 1px solid var(--border);
  }
  .sidebar-logo {
    border-bottom: 1px solid var(--border); border-right: 0;
    padding: 6px var(--gutter);
  }
  /* The module rail: one row, ~48px, scrolls sideways instead of wrapping.
     `.neocom` (the module list) is targeted specifically so the second
     `.sidebar-nav` below it -- the Settings link -- is not forced into the
     same scroll strip. */
  .sidebar-nav.neocom {
    flex-direction: row; flex-wrap: nowrap; gap: 0; padding: 0;
    overflow-x: auto; overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    height: 48px;
    border-bottom: 1px solid var(--border);
    /* Phase 2 review: at 375px the Market/War/Corp modules sit past the
       visible strip with no hint there is more to scroll to. Same edge-fade
       trick as .table-scroll, tracking the true scroll edge rather than a
       fixed overlay, so the fade only shows on the side that still has
       content past it. */
    background:
      linear-gradient(90deg, rgba(111, 220, 234, 0.16), transparent 20px) left / 20px 100% no-repeat local,
      linear-gradient(270deg, rgba(111, 220, 234, 0.16), transparent 20px) right / 20px 100% no-repeat local,
      rgba(8, 13, 20, 0.94);
  }
  .sidebar-nav.neocom .nav-tab {
    flex: none; height: 48px; min-height: 44px;
    padding: 0 14px; border-left: 0; border-bottom: 2px solid transparent;
    white-space: nowrap;
  }
  .sidebar-nav.neocom .nav-tab[aria-current="page"],
  .sidebar-nav.neocom .nav-tab[aria-selected="true"], .sidebar-nav.neocom .nav-tab.is-current {
    border-left-color: transparent; border-bottom-color: var(--primary);
  }
  /* UI-REFRESH.md section 2 grouped the rail into .rail-group wrapper divs.
     `display: contents` un-boxes each group here so its .nav-tab children
     become direct flex items of .sidebar-nav.neocom again, same as before
     grouping existed -- otherwise the `order` rules below (which target
     #tab-NAME directly, assuming that flat structure) would only reorder a
     module within its own two-to-six-item group instead of across the whole
     strip, and each group's own column layout would stack inside the 48px
     row instead of scrolling in it. The group label has no room in a 48px
     horizontal strip, so it is hidden rather than fighting the icons for
     space; the group's rail-group--X colour on the active item still shows
     via the border-bottom rule above. */
  .rail-group { display: contents; }
  .rail-group-label { display: none; }
  /* MUST (mobile pass): Overview, Moons, Intel and Ops read first on phone,
     the four modules the field actually opens on a phone (role full lands on
     Overview, role moon_member on Moons -- see applyRoleRail() in app.js).
     `order` only, not a DOM move: settings.html keeps its own separate copy
     of this rail and a source-order change here would have to be repeated
     there to avoid the two drifting apart again. */
  #tab-overview { order: 1; }
  #tab-moons { order: 2; }
  #tab-intel { order: 3; }
  #tab-ops { order: 4; }
  #tab-dash { order: 5; }
  #tab-contracts { order: 6; }
  #tab-market { order: 7; }
  #tab-sell { order: 8; }
  #tab-bpc { order: 9; }
  #tab-hangar { order: 10; }
  #tab-build { order: 11; }
  #tab-hunt { order: 12; }
  #tab-recruit { order: 13; }

  /* Settings stays a real nav row (removing it would cut phone's only route
     to it), but it is its own thin strip under the module rail rather than a
     tab sharing the scroll strip's height. */
  nav.sidebar-nav[aria-label="Sections"] {
    flex-direction: row; padding: 0 var(--gutter); min-height: 0;
    border-bottom: 1px solid var(--border);
  }
  nav.sidebar-nav[aria-label="Sections"] .nav-tab { padding: 6px 0; min-height: 32px; }

  /* Character / Reconnect ESI / scan status: one short line, not the
     desktop's stacked footer block. */
  .sidebar-footer {
    margin-top: 0; flex-direction: row; flex-wrap: wrap;
    align-items: center; gap: 6px 10px;
    border-top: 0; padding: 6px var(--gutter);
  }
  .sidebar-footer .chip { max-width: 55vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  main { margin-left: 0; padding: 16px var(--gutter) 56px; }
  .contracts-search { max-width: none; }
  .win-body { padding: 11px; }
  .stat .v { font-size: var(--fs-md); }
  th, td { padding: 7px 9px; }
  .icon { width: 26px; height: 26px; }
}

/* backdrop-filter on a scrolling ancestor is expensive and can trap fixed
   children, so it is dropped where it buys least. */
@media (prefers-reduced-transparency: reduce) {
  section { backdrop-filter: none; -webkit-backdrop-filter: none; background: #0b1119; }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* ---- sortable headers and in-game actions ----------------------------- */

button.sort {
  background: none; border: 0; padding: 0; min-height: 0;
  font: inherit; font-size: var(--fs-xs); letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-faint); cursor: pointer;
}
button.sort:hover { color: var(--lume); box-shadow: none; background: none; }
button.sort.on { color: var(--lume); }
button.sort.on::after { content: " \2193"; }

.ingame { display: inline-flex; gap: 4px; }
.mini {
  min-height: 24px; padding: 2px 7px; font-size: var(--fs-xs);
  border: 1px solid var(--line); color: var(--ink-faint); background: none;
}
.mini:hover:not(:disabled) { color: var(--lume); border-color: var(--lume-dim); }

/* When the logged-in character is not the pinned hand-off target, the buttons
   cannot work. Dim them rather than letting them look live and fail. */
.note.warn { color: var(--hot); }
body.ui-disabled .mini { opacity: 0.35; pointer-events: none; }

/* ---------------------------------------------------------------------------
   The threat map.

   Tab styling itself now lives with the rest of the sidebar shell
   (.sidebar-nav / .nav-tab, near the top of this file). This section keeps
   only what is specific to the map: the live-fight dot and the map layout.
--------------------------------------------------------------------------- */
/* ---- the alert channel -------------------------------------------------
 *
 * Something on a tab you are not looking at is the one thing that should pull
 * you back to it. Three levels, and the distinction is the whole design:
 *
 *   info      there is something here, worth a look       dot only, steady
 *   warn      this is time-sensitive                      dot + label warms
 *   critical  read this now                               dot + label + pulse
 *
 * ONLY critical animates. A sidebar with three things gently pulsing at all
 * times is wallpaper within a day, and then the one time it matters it is
 * wallpaper too. Capitals on a field is the standing critical case: it does
 * not matter what else is on the board, that is the row you read first.
 *
 * Movement is also the accessibility hazard here, so reduced-motion keeps the
 * colour and drops the animation. The level is still legible without it,
 * which is the test: the pulse is emphasis, never the only carrier. */
/* Admin pending-approval counter: a count, not a dot, because the number is
   the point. Warn colour, never animated. */
.nav-count {
  margin-left: auto; min-width: 18px; padding: 0 6px; border-radius: 9px;
  font: 700 11px/18px var(--mono); text-align: center; font-variant-numeric: tabular-nums;
  color: var(--void); background: var(--warn);
}
.tab-dot {
  display: inline-block; width: 5px; height: 5px; border-radius: 50%;
  background: var(--lume); margin-left: 6px; vertical-align: 1px;
  box-shadow: 0 0 7px var(--lume);
}
.tab-dot[data-level="warn"] { background: var(--hot); box-shadow: 0 0 7px var(--hot); }
.tab-dot[data-level="critical"] {
  background: var(--down); box-shadow: 0 0 8px var(--down);
  animation: pulse 1.6s ease-in-out infinite;
}
.nav-tab.alert-warn { color: var(--hot); }
.nav-tab.alert-critical {
  color: var(--down);
  animation: navburn 1.6s ease-in-out infinite;
}
@keyframes navburn {
  0%, 100% { color: var(--down); text-shadow: none; }
  50% { color: #ff9b99; text-shadow: 0 0 12px rgba(226, 97, 95, 0.75); }
}
@media (prefers-reduced-motion: reduce) {
  .tab-dot[data-level="critical"], .nav-tab.alert-critical { animation: none; }
  .nav-tab.alert-critical { text-shadow: 0 0 10px rgba(226, 97, 95, 0.6); }
}

/* The banner above a board when something overrides its ranking. Hidden when
   empty, because a banner that is always present is not a banner. */
.alert-strip { display: flex; flex-direction: column; gap: 1px; margin: 10px 0 0; }
.alert-line {
  display: flex; align-items: center; gap: 10px; padding: 8px 14px;
  border-left: 2px solid var(--down); background: rgba(226, 97, 95, 0.09);
  font: 600 var(--fs-xs) var(--sans); letter-spacing: 0.05em; color: #ffb4b2;
}
.alert-line b {
  font-family: var(--mono); font-size: var(--fs-sm); color: var(--down);
  text-transform: uppercase; letter-spacing: 0.06em; flex: none;
}
.alert-line .why { color: var(--ink-dim); font-weight: 400; letter-spacing: 0.02em; }
.alert-line button {
  margin-left: auto; flex: none; border-color: rgba(226, 97, 95, 0.5); color: #ffb4b2;
}

/* ---- the refresh clock -------------------------------------------------
 *
 * Two numbers, not one. `next` counts down to the next automatic re-read;
 * `age` counts up from the last one that succeeded. A countdown on its own
 * cannot express "the last three refreshes failed", which is exactly the state
 * where a confident-looking page is most dangerous. */
.clock {
  display: inline-flex; align-items: center; gap: 5px; margin-left: auto;
  font: var(--fs-xs) var(--mono); font-variant-numeric: tabular-nums; letter-spacing: 0.04em;
  color: var(--ink-faint); white-space: nowrap;
}
.clock-sep { color: var(--line-hi); }
.clock-next { color: var(--ink-dim); }
.clock-age { color: var(--ink-dim); }
/* Amber past one refresh interval, red past two. The colour is on the AGE,
   never on the countdown: a countdown at zero is normal, a page that has not
   refreshed in six minutes is not. */
.clock-age.stale { color: var(--hot); }
.clock-age.dead { color: var(--down); }
.clock.working .clock-next { color: var(--lume); }

/* The map wants more width than the contract tables do. Widen only for it. */
body.map-wide main { max-width: 1640px; }

.map-canvas { padding: 8px 4px 4px 8px; min-width: 0; }
.map-svg svg { width: 100%; height: auto; display: block; }
.map-h3 {
  margin: 14px 0 7px; font: 600 var(--fs-xs)/1 var(--mono); letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-faint);
}
.map-legend { margin-top: 13px; font: var(--fs-xs)/1.75 var(--mono); color: var(--ink-dim); }
.map-legend i {
  display: inline-block; width: 9px; height: 9px; border-radius: 50%;
  margin-right: 7px; vertical-align: -1px;
}
.map-detail {
  margin-top: 13px; border: 1px solid var(--line); border-left: 2px solid var(--lume-dim);
  padding: 9px 10px; font: var(--fs-xs)/1.5 var(--mono);
}
.map-detail h4 {
  margin: 0 0 6px; font-size: var(--fs-sm); color: var(--lume); letter-spacing: 0.05em;
}
.map-detail dl { margin: 0; display: grid; grid-template-columns: auto auto; gap: 1px 10px; }
.map-detail dt { color: var(--ink-faint); }
.map-detail dd { margin: 0; text-align: right; }
.map-detail .ingame { margin-top: 9px; }

/* Part 1.3: hour-of-day profile, 24 bars on npc kills, peaks marked. Height
   alone carries the value; samples (n=) live in the tooltip only, because a
   bar chart with 24 numbers stamped under it is not glanceable and this is
   meant to answer "when is this system busy" in about one second. */
.prof-strip {
  display: flex; align-items: flex-end; gap: 1px; height: 24px;
  margin: 9px 0 3px; padding-top: 2px;
}
.prof-bar {
  flex: 1 1 0; min-width: 2px; background: rgba(90, 130, 160, 0.35); cursor: help;
}
.prof-bar.peak { background: var(--hot); }
.prof-caption {
  margin: 0; font-size: var(--fs-xs); color: var(--ink-faint); letter-spacing: 0.02em; line-height: 1.4;
}

/* Map marks. Colour means recency, size means how many hostiles, the ring
   means security band. Three channels, three questions, no overlap. */
.map-svg .e { stroke: rgba(110, 160, 195, 0.34); stroke-width: 1.1; }
.map-svg .e-choke {
  stroke: var(--hot); stroke-width: 2.4; stroke-dasharray: 7 4; opacity: 0.85;
}
.map-svg .n { stroke-width: 1.4; cursor: pointer; transition: opacity 0.18s; }
.map-svg .b-hi { stroke: rgba(86, 210, 154, 0.55); }
.map-svg .b-lo { stroke: rgba(229, 170, 74, 0.55); }
.map-svg .b-null { stroke: rgba(226, 97, 95, 0.5); }
.map-svg .cold { fill: rgba(58, 82, 104, 0.9); }
.map-svg .warm { fill: rgba(60, 165, 182, 0.8); }
.map-svg .recent { fill: rgba(229, 170, 74, 0.8); }
.map-svg .live { fill: var(--down); }
.map-svg .n:hover { opacity: 0.75; }
.map-svg .n.on { stroke: var(--lume); stroke-width: 2.4; }
.map-svg .ring { fill: none; stroke: var(--down); stroke-width: 1.2; opacity: 0.5; }
.map-svg .pulse { animation: mapping 2.4s ease-out infinite; transform-box: fill-box; transform-origin: center; }
@keyframes mapping {
  0% { opacity: 0.55; transform: scale(0.72); }
  70% { opacity: 0; transform: scale(1.5); }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .map-svg .pulse { animation: none; opacity: 0.45; } }
.map-svg .struct { fill: none; stroke: var(--lume); stroke-width: 1.3; opacity: 0.9; }
.map-svg .lbl {
  fill: var(--ink); font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.03em;
  paint-order: stroke; stroke: var(--void); stroke-width: 3.5; pointer-events: none;
}
.map-svg .leader { stroke: var(--ink-faint); stroke-width: 0.9; opacity: 0.55; }
.map-svg .panel { fill: rgba(255, 255, 255, 0.022); stroke: var(--line-hi); stroke-width: 1; }
.map-svg .ptitle {
  fill: var(--ink-faint); font: 600 var(--fs-xs) var(--mono);
  letter-spacing: 0.06em; text-transform: uppercase;
}

/* Response-range tiers. Colour runs cold to hot with distance, so the shortest
   reach (a titan bridge) reads as the most available and the Black Ops-only
   ring reads as the reach of last resort. Same three meaning colours the rest
   of the app uses; nothing new is introduced. */
.t-titan { color: var(--up); }
.t-dread { color: var(--lume); }
.t-blops { color: var(--hot); }
.t-none { color: var(--ink-faint); }
.t-dot {
  display: inline-block; width: 9px; height: 9px; border-radius: 50%;
  margin-right: 7px; vertical-align: -1px; background: currentColor;
}
.reg-inline { color: var(--ink-faint); font-size: var(--fs-xs); letter-spacing: 0.08em; }
.map-detail .ingame { display: flex; gap: 5px; flex-wrap: wrap; }
.map-detail .zk { font-size: var(--fs-xs); letter-spacing: 0.04em; text-decoration: none; }
.map-detail .zk:hover { color: var(--lume); border-color: var(--lume-dim); }

/* ---------------------------------------------------------------------------
   Ops view: an art-led fight feed.

   The first version of this was 10px mono and no imagery, and it was correct
   and unreadable. What makes a killboard scannable is the art: a Sin render is
   recognised before any label is read, and "ship_type_id 22430" never is. So
   every fight leads with the hull that died, the attacking alliance flies its
   own logo, and the numbers are big enough to read from a chair.

   Art comes from images.evetech.net, which CCP explicitly offers as a CDN for
   third-party apps and asks not to be cached locally. No CCP logo or wordmark
   appears anywhere: the Developer License forbids combining CCP marks with
   other marks.
--------------------------------------------------------------------------- */
/* Evidence age, rendered before the counts it qualifies. Deliberately NOT
   red or green: freshness is not a verdict, it is how much to trust one, so
   it never borrows the two colours this file reserves for hostile and
   favourable. Amber's one job here, same as everywhere else it already
   appears on this page (WATCH, tackle present, stale-good margins,
   incomplete or truncated data warnings), is "pause and read before you
   treat this as current." Actionable data gets no colour at all, because a
   fresh fact needs no flag; only hypothesis, context and history reuse
   amber, distinguished by their label text rather than a second hue. */
.fresh-bar { display: flex; gap: 7px; flex-wrap: wrap; padding: 9px 18px 0; }
.fresh-chip {
  font: 600 var(--fs-xs)/1.4 var(--mono); letter-spacing: 0.03em;
  padding: 3px 8px; border: 1px solid var(--line); color: var(--ink-dim);
  cursor: help;
}
.fresh-chip.f-actionable { color: var(--ink); border-color: var(--line-hi); }
.fresh-chip.f-hypothesis, .fresh-chip.f-context, .fresh-chip.f-history {
  color: var(--hot); border-color: rgba(229, 170, 74, 0.4); background: rgba(229, 170, 74, 0.06);
}
.fresh-chip.f-unknown { color: var(--ink-faint); border-style: dashed; }

.ops-bar {
  display: flex; gap: 30px; flex-wrap: wrap;
  padding: 13px 18px; border-bottom: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.015);
}
.ops-bar div { min-width: 58px; }
.ops-bar b {
  display: block; font: 600 var(--fs-2xl)/1.05 var(--mono); font-variant-numeric: tabular-nums;
  color: var(--lume);
}
.ops-bar span {
  color: var(--ink-faint); letter-spacing: 0.11em;
  text-transform: uppercase; font-size: var(--fs-xs);
}
.ops-bar b.v-go { color: var(--up); }
.ops-bar b.v-tackle { color: var(--hot); }
.tick { font: var(--fs-xs) var(--mono); color: var(--ink-faint); letter-spacing: 0.05em; }

/* ---------------------------------------------------------------------------
   Region pulse strip. "Is the neighbourhood hot right now", one compact block
   per region above the fight board. Colour is INVERTED from the win-tier
   norm on purpose: hot means dangerous here, not winning, so red is busy and
   there is no green at all, only a cooler grey when a region is running
   under its own mean.
--------------------------------------------------------------------------- */
.pulse-strip { display: flex; gap: 10px; flex-wrap: wrap; padding: 10px 18px; border-bottom: 1px solid var(--line); }
.pulse-region {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 10px; border: 1px solid var(--line); min-width: 0;
}
.pulse-region.pulse-quiet { opacity: 0.7; }
.pulse-name {
  font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink); flex: none;
}
.pulse-kills {
  font: 600 var(--fs-sm) var(--mono); font-variant-numeric: tabular-nums;
  color: var(--ink-dim); cursor: help; flex: none;
}
.pulse-kills.pulse-hot { color: var(--down); }
.pulse-kills.pulse-cool { color: var(--ink-faint); }
.pulse-quiet-label {
  font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-faint); cursor: help; flex: none;
}
.pulse-jumps { font-size: var(--fs-xs); color: var(--ink-faint); flex: none; white-space: nowrap; }
.pulse-spark { flex: none; }
.pulse-spark rect { fill: var(--lume-dim); }
.pulse-thin {
  font-size: var(--fs-xs); color: var(--hot); letter-spacing: 0.03em; cursor: help; flex: none;
}
@media (max-width: 640px) {
  .pulse-region { flex-wrap: wrap; }
  .pulse-spark { display: none; }
}

/* Feed first and wide, map demoted to a rail.
 *
 * This was 486px of feed against an open-ended map pane, which is backwards:
 * the map was the largest thing on screen and the only one nobody was reading.
 * The feed is what gets decided from, so it takes the width, and the rail is
 * fixed at the width its widest content (an alliance name) actually needs. */
.ops { display: grid; grid-template-columns: minmax(0, 1fr) 316px; gap: 0; padding: 0; }
@media (max-width: 1180px) { .ops { grid-template-columns: 1fr; } }
.ops-feed {
  border-right: 1px solid var(--line); padding: 13px 14px 18px; min-width: 0;
  min-height: 320px;
  max-height: 82vh;
  max-height: 82dvh;
  overflow-y: auto;
}
@media (max-width: 1180px) {
  .ops-feed { border-right: 0; border-bottom: 1px solid var(--line); max-height: none; }
}
.ops-rail { padding: 13px 12px 16px; min-width: 0; }
.rail-block { margin-bottom: 4px; }
.rail-block .map-h3 { margin-top: 4px; }
.rail-sum {
  cursor: pointer; list-style: none; margin: 12px 0 4px;
  font: 600 var(--fs-xs)/1 var(--mono); letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-faint);
}
.rail-sum::-webkit-details-marker { display: none; }
.rail-sum::before { content: "+ "; color: var(--lume-dim); }
details[open] > .rail-sum::before { content: "- "; }
.rail-sum:hover { color: var(--ink); }
.ops-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.ops-head .map-h3 { margin: 2px 0 9px; font-size: var(--fs-xs); }
/* flex-wrap: a chip row with enough chips (Moons' six-way owner filter is the
   worst case) has no shrink room of its own, so on a phone it either wraps
   here or pushes the WHOLE PAGE's scrollWidth out past the viewport --
   confirmed by measurement, not by inspection (KrypCo mobile pass, session
   19: #moons-owner-filters alone read 423px wide against a 375px viewport). */
.ops-filters { display: flex; gap: 4px; flex-wrap: wrap; }
.ops-filters .chip {
  background: none; border: 1px solid var(--line); color: var(--ink-faint);
  min-height: 22px; padding: 2px 9px; font: 600 var(--fs-xs) var(--mono);
  letter-spacing: 0.1em; text-transform: uppercase; cursor: pointer; box-shadow: none;
  transition: color 0.18s, border-color 0.18s;
}
.ops-filters .chip:hover { color: var(--ink); border-color: var(--line-hi); background: none; }
.ops-filters .chip.on { color: var(--lume); border-color: var(--lume-dim); background: var(--lume-wash); }
/* ---- the dense feed ----------------------------------------------------
 *
 * One grid, shared by the column header and every row, so the columns line up
 * without either side hardcoding a width. Changing a track here moves the
 * header and the rows together; that is the entire reason this is a grid on
 * the row rather than a flex line per row.
 *
 * Column order is the order the questions get asked: how old, where, what
 * died, how many of them, who they are, how much ISK, what we can send. */
.feed-head, .fight {
  display: grid;
  grid-template-columns:
    52px            /* age, ticking */
    minmax(96px, 1.1fr)  /* system */
    minmax(88px, 1fr)    /* what died */
    64px            /* hostiles + tackle */
    minmax(104px, 1.2fr) /* who is shooting */
    62px            /* isk */
    76px;           /* response */
  gap: 0 10px;
  align-items: center;
}
.feed-head {
  padding: 0 10px 5px; margin-top: 4px;
  font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.15em;
  text-transform: uppercase; color: var(--ink-faint);
  border-bottom: 1px solid var(--line);
}
.feed-head span:nth-child(4), .feed-head span:nth-child(6) { text-align: right; }

.fights { display: flex; flex-direction: column; }
.fight {
  padding: 7px 10px; cursor: pointer;
  border-left: 2px solid transparent;
  border-bottom: 1px solid rgba(90, 130, 160, 0.09);
  font: var(--fs-xs) var(--mono); font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
  transition: background 0.15s, border-color 0.15s;
}
.fight:hover { background: rgba(255, 255, 255, 0.035); }
.fight.on { background: var(--lume-wash); border-left-color: var(--lume); }
.fight.v-go { border-left-color: var(--up); }
.fight.v-avoid { border-left-color: var(--down); }
.fight.v-watch { border-left-color: var(--lume-dim); }
.fight.v-over { border-left-color: rgba(90, 130, 160, 0.3); opacity: 0.62; }
/* Capitals override the verdict colour. Whatever else the row says, this is
   the one a pilot has to see, so it gets the loudest treatment on the board
   and it is not subtle about it. */
.fight.has-cap { background: rgba(226, 97, 95, 0.07); border-left-color: var(--down); }
.fight.has-cap:hover { background: rgba(226, 97, 95, 0.12); }

.f-age { color: var(--ink-faint); font-size: var(--fs-xs); }
.fight.is-live .f-age { color: var(--down); }
/* The capital banner's age. Same ticking contract as .f-age, deliberately NOT
 * the same type: the row class is faint 10.5px, which inside a red alert strip
 * would make the one number deciding whether to undock the dimmest thing on it.
 * Tabular figures so a seconds counter cannot reflow the sentence around it. */
.a-age { font-variant-numeric: tabular-nums; }

.f-age .pip, .a-age .pip {
  display: inline-block; width: 4px; height: 4px; border-radius: 50%;
  background: var(--down); margin-right: 4px; vertical-align: 1px;
  animation: pulse 1.5s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .f-age .pip, .a-age .pip { animation: none; }
}

.f-sys { min-width: 0; }
.f-sys b {
  display: block; color: var(--ink); font-weight: 600; font-size: var(--fs-sm);
  letter-spacing: 0.02em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.f-sys em {
  font-style: normal; font-size: var(--fs-xs); letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--ink-faint);
}
.f-sys em .s-hi { color: var(--up); }
.f-sys em .s-lo { color: var(--hot); }
.f-sys em .s-null { color: var(--down); }

/* Part 1: escalation-risk chip. Force Recon, HIC or Black Ops seen near this
   system in the last six hours, the hulls that open a cyno. Own line under
   the region badge rather than a new grid column, so the feed's seven-track
   grid never has to reflow for a chip that most rows do not carry. */
.f-sys .esc-chip {
  display: block; margin-top: 2px; font: 600 var(--fs-xs) var(--mono);
  letter-spacing: 0.04em; text-transform: uppercase; cursor: help;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.f-sys .esc-chip.esc-y { color: var(--hot); }
.f-sys .esc-chip.esc-r { color: var(--down); }

/* What died. Icons, because a Rattlesnake is recognised in a glance and
   "ship_type_id 17918" is not. The victim name under them is what turns a
   count into a fight someone can place. */
.f-lost { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.f-icons { display: flex; gap: 2px; align-items: center; }
.f-icons img {
  width: 20px; height: 20px; display: block;
  border: 1px solid rgba(90, 130, 160, 0.22); background: rgba(0, 0, 0, 0.3);
}
.f-icons .more { font-size: var(--fs-xs); color: var(--ink-faint); margin-left: 2px; }
.f-vic {
  font-size: var(--fs-xs); color: var(--ink-faint); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}

.f-host { text-align: right; }
.f-host b { color: var(--ink); font-size: var(--fs-sm); font-weight: 600; }
.f-host em {
  display: block; font-style: normal; font-size: var(--fs-xs); color: var(--hot);
  letter-spacing: 0.04em;
}

/* Who is shooting. The banner and how much of the field it accounts for:
   23 of 41 in one alliance is a fleet, 3 of 41 is a coincidence. */
.f-who { min-width: 0; display: flex; align-items: center; gap: 5px; }
.f-who img { width: 18px; height: 18px; flex: none; }
.f-who span {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-xs); color: var(--ink);
}
.f-who i {
  font-style: normal; font-size: var(--fs-xs); color: var(--ink-faint); flex: none;
}

.f-isk { text-align: right; color: var(--ink); font-size: var(--fs-xs); }
.f-isk.big { color: var(--hot); font-weight: 600; }

.f-resp {
  font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.06em; text-transform: uppercase;
  border: 1px solid var(--line); padding: 3px 5px; text-align: center;
  color: var(--ink-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.f-resp.r-dread { color: var(--down); border-color: rgba(226, 97, 95, 0.45); }
.f-resp.r-bridge { color: var(--hot); border-color: rgba(229, 170, 74, 0.45); }
/* Carriers sit between dreads and Black Ops in loudness, because that is what
   the call means: heavier than a gank, not the capital-on-capital emergency a
   dread is. Amber like bridge was, since it replaces that branch. */
.f-resp.r-carrier { color: var(--hot); border-color: rgba(229, 170, 74, 0.45); }
.f-resp.r-blops { color: var(--up); border-color: rgba(86, 210, 154, 0.4); }
.f-resp.r-none { color: var(--ink-faint); border-style: dashed; }
/* 0b: a friendly aggressor. Not a doctrine colour at all (it is not asking
   "what do we send"), so it gets its own quiet lume treatment rather than
   borrowing dread/carrier/blops' red-amber-green vocabulary. */
.f-resp.r-ours { color: var(--lume-dim); border-color: var(--lume-dim); border-style: dashed; }

/* A friendly aggressor mutes the whole row. Reuses the row-muted convention
   the Sell and Blueprints tables already use, applied here to the feed row,
   the expanded card and a dashboard line. */
.fight.row-muted, .battle.row-muted, .d-row.row-muted { opacity: 0.55; }
.fight.row-muted:hover, .battle.row-muted:hover, .d-row.row-muted:hover { opacity: 1; }
/* A muted row must not also carry the has-cap red treatment: isOurs already
   suppresses that class in app.js, this is the belt to that suspenders. */
.fight.row-muted.has-cap { background: none; border-left-color: transparent; }

/* The expanded row. Same card as before, just no longer the default state:
   it is what one chosen fight opens into, not what nine of them each occupy. */
.f-open { grid-column: 1 / -1; padding: 4px 0 10px; }

.concluded { margin-top: 14px; border-top: 1px solid var(--line); padding-top: 6px; }
.concluded > summary {
  cursor: pointer; list-style: none;
  font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.15em;
  text-transform: uppercase; color: var(--ink-faint); padding: 4px 0;
}
.concluded > summary::-webkit-details-marker { display: none; }
.concluded > summary::before { content: "+ "; color: var(--lume-dim); }
.concluded[open] > summary::before { content: "- "; }
.concluded > summary:hover { color: var(--ink); }

/* ---- who is out ---------------------------------------------------------
 * Every group on the board, summed across fights. Unique pilots, so a gang
 * that killed in three systems counts once, not three times. */
.roster { display: flex; flex-direction: column; gap: 1px; }
.roster-row {
  display: grid; grid-template-columns: 20px minmax(0, 1fr) auto;
  gap: 7px; align-items: center; padding: 4px 2px;
  font: var(--fs-xs) var(--mono); font-variant-numeric: tabular-nums;
  border-bottom: 1px solid rgba(90, 130, 160, 0.09);
}
.roster-row img { width: 20px; height: 20px; }
.roster-row .n {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink);
}
.roster-row .c { color: var(--ink-dim); font-size: var(--fs-xs); white-space: nowrap; }
.roster-row .c b { color: var(--ink); font-weight: 600; }
.roster-row.hostile-cap .n { color: var(--down); }
.roster-row .sys { font-size: var(--fs-xs); color: var(--ink-faint); }

.battles { display: flex; flex-direction: column; gap: 10px; }
.battle {
  border: 1px solid var(--line); border-left: 3px solid var(--ink-faint);
  padding: 10px 12px 11px; cursor: pointer;
  background: rgba(255, 255, 255, 0.016);
  transition: border-color 0.18s, background 0.18s;
}
.battle:hover { border-color: var(--line-hi); background: rgba(255, 255, 255, 0.032); }
.battle.on { border-color: var(--lume-dim); background: var(--lume-wash); }
.battle.v-go { border-left-color: var(--up); }
.battle.v-avoid { border-left-color: var(--down); }
.battle.v-watch { border-left-color: var(--hot); }
.battle.v-over { border-left-color: var(--ink-faint); opacity: 0.82; }
.battle.v-over:hover { opacity: 1; }

.b-top { display: flex; align-items: center; gap: 9px; }
.b-top h4 { margin: 0; font: 600 var(--fs-lg)/1.2 var(--sans); letter-spacing: 0.03em; flex: 1; min-width: 0; }
.b-reg { color: var(--ink-faint); font: var(--fs-xs) var(--mono); letter-spacing: 0.12em; margin-left: 7px; }
.verdict {
  font: 700 var(--fs-xs)/1 var(--mono); letter-spacing: 0.06em; text-transform: uppercase;
  padding: 4px 6px; border: 1px solid currentColor; flex: none;
}
.v-go .verdict { color: var(--up); background: rgba(86, 210, 154, 0.09); }
.v-avoid .verdict { color: var(--down); background: rgba(226, 97, 95, 0.1); }
.v-watch .verdict { color: var(--hot); background: rgba(229, 170, 74, 0.09); }
.v-over .verdict { color: var(--ink-faint); }
.b-age { font: var(--fs-sm) var(--mono); color: var(--ink-dim); flex: none; letter-spacing: 0.02em; }
.is-live .b-age { color: var(--down); font-weight: 600; }
.pip {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: var(--down); margin-right: 5px; vertical-align: 0;
  box-shadow: 0 0 9px var(--down); animation: pulse 1.6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .pip { animation: none; } }

/* Art and facts side by side. The hull that died carries the whole top-left
   corner, because it is the fastest read on the card. */
.b-main { display: flex; gap: 12px; margin-top: 9px; align-items: flex-start; }
.b-hero {
  position: relative; flex: none; width: 72px; height: 72px; display: block;
  border: 1px solid var(--line); background: rgba(0, 0, 0, 0.35); text-decoration: none;
  transition: border-color 0.18s;
}
.b-hero img { display: block; width: 72px; height: 72px; object-fit: cover; }
.b-hero:hover { border-color: var(--lume-dim); }
.b-hero-empty { background: rgba(255, 255, 255, 0.02); }
.b-hero-isk {
  position: absolute; left: 0; right: 0; bottom: 0;
  background: linear-gradient(180deg, transparent, rgba(4, 7, 10, 0.92) 55%);
  color: var(--ink); font: 600 var(--fs-xs) var(--mono); text-align: right;
  padding: 8px 4px 2px; letter-spacing: 0.03em;
}
.b-facts { flex: 1; min-width: 0; }

/* What to bring. Sits beside the verdict because together they are the whole
   decision, and the evidence below is only there to justify them. */
.resp {
  font: 700 var(--fs-xs)/1 var(--mono); letter-spacing: 0.06em; text-transform: uppercase;
  padding: 4px 6px; border: 1px solid currentColor; flex: none;
}
.resp.r-blops { color: var(--lume); background: rgba(111, 220, 234, 0.08); }
.resp.r-dread { color: var(--hot); background: rgba(229, 170, 74, 0.09); }
.resp.r-bridge { color: var(--hot); background: rgba(229, 170, 74, 0.09); }
.resp.r-carrier { color: var(--hot); background: rgba(229, 170, 74, 0.09); }
.resp.r-none { color: var(--ink-faint); }
.resp.r-ours { color: var(--lume-dim); }
.v-over .resp { opacity: 0.6; }

/* Part 0c/0d: who is losing, and the response vector. Quiet lines under the
   roster and the verdict reason respectively, evidence rather than a second
   headline. */
.b-losing {
  margin-top: 6px; font-size: var(--fs-xs); color: var(--ink-faint); line-height: 1.5;
}
.b-losing b { color: var(--ink-dim); font-weight: 600; }
.b-vector {
  margin: -4px 0 9px; font: var(--fs-xs)/1.4 var(--sans); color: var(--ink-faint);
}
.b-vector .sub { color: var(--ink-faint); }

/* Tackle is a property of the hostile count, not a fourth statistic. */
.b-nums em {
  display: block; font: var(--fs-xs)/1.3 var(--mono); font-style: normal;
  letter-spacing: 0.04em; text-transform: none; margin-top: 1px;
}
.b-kill { font: var(--fs-sm)/1.25 var(--sans); color: var(--ink-dim); margin-bottom: 3px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.b-kill b { color: var(--ink); font-weight: 600; }
.b-nums { display: flex; flex-wrap: wrap; gap: 2px 22px; margin-top: 7px; }
.b-nums span {
  font: var(--fs-xs) var(--mono); color: var(--ink-faint);
  letter-spacing: 0.06em; text-transform: uppercase;
}
.b-nums b {
  display: block; color: var(--ink); font: 600 var(--fs-lg)/1.15 var(--mono);
  font-variant-numeric: tabular-nums; letter-spacing: 0;
}
.b-nums b.v-tackle { color: var(--hot); }

/* Who is shooting, flying their own logo. "Snuffed Out, 14 of 17" is a
   decision; "12 corporations" is a statistic. */
.b-group { display: flex; align-items: center; gap: 7px; margin-top: 8px; min-width: 0; }
.b-group img { flex: none; width: 22px; height: 22px; }
.b-group-n {
  font: var(--fs-xs)/1.2 var(--sans); color: var(--ink); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.b-group-s { font: var(--fs-xs) var(--mono); color: var(--ink-faint); flex: none; }

/* What died, newest first. Icons not renders: at 28px a render is mush. */
.b-strip {
  display: flex; gap: 3px; flex-wrap: wrap; margin-top: 9px;
  padding-top: 9px; border-top: 1px solid rgba(90, 130, 160, 0.1);
}
.k-ico { display: block; width: 28px; height: 28px; border: 1px solid transparent; transition: border-color 0.18s; }
.k-ico img { display: block; width: 28px; height: 28px; }
.k-ico:hover { border-color: var(--lume-dim); }

.b-comp { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 9px; }
.chip-c {
  font: var(--fs-xs) var(--mono); letter-spacing: 0.03em; padding: 2px 7px;
  border: 1px solid var(--line); color: var(--ink-dim);
}
/* Only the classes that change the answer get colour. */
.chip-c.c-super, .chip-c.c-cap, .chip-c.c-blops {
  color: var(--down); border-color: rgba(226, 97, 95, 0.45); background: rgba(226, 97, 95, 0.06);
}
.chip-c.c-tackle { color: var(--hot); border-color: rgba(229, 170, 74, 0.4); }
.chip-c.c-logi { color: var(--lume); border-color: var(--lume-dim); }
.b-why { margin: 9px 0 9px; font: var(--fs-sm)/1.45 var(--sans); color: var(--ink-dim); }
.battle .ingame { display: flex; gap: 5px; flex-wrap: wrap; }
.battle .zk { font-size: var(--fs-xs); letter-spacing: 0.04em; text-decoration: none; }
.battle .zk:hover { color: var(--lume); border-color: var(--lume-dim); }

/* ---- phone width (~760px) ----------------------------------------------

   Placed at the end of the file on purpose. `button.sort` sets its own
   `min-height: 0` and `.mini` sets `min-height: 24px` further up
   (see "sortable headers and in-game actions"), same specificity as the
   overrides below; a max-width media query earlier in the cascade loses
   to those base rules and silently does nothing. Last in the file wins.

   The 8-column contract tables (search, flagged, the three region panels)
   and the 8-column market table cannot show every column on a phone without
   either a horizontal scrollbar or crushed, unreadable cells. Columns are
   hidden with `display: none`, never removed from the markup, so the `<th>`
   count a screen reader or the column-count check sees is unchanged; this is
   a visual decision only.

   UI-REFRESH.md phase 2: Listed dropped as its own column on both tables (it
   already duplicated the age chip that marginCell()/the item cell render
   inline), which shifted every position from here to "In game"/Stock down by
   one. The column counts and comments below are updated to match; the
   selectors previously here were hiding the WRONG columns for a release,
   `nth-child(3)` reading as "Local price" in the comment while the actual
   third `<th>` was Where, because nobody re-numbered them after an earlier
   edit. Positions below are verified against the current markup, not carried
   forward from the old comment.

   Contract tables (.tbl-flagged, shared by search/flagged/the three region
   panels, 8 columns: ignore, Contract, Where, Jumps, Ask, Est. value, Margin,
   In game): kept are the ignore control, the item, Where (system name, sec
   class and jump distance all in one coloured tag) and Margin (the verdict,
   with the listing's age already folded into the same cell by marginCell()),
   plus the in-game action button, since acting on a row is the point of
   having it open on a phone at all. Hidden are Jumps (the same distance
   already colours and titles the Where tag) and Ask and Est. value (their
   relationship is what Margin already states as a percentage; the exact ISK
   figures are one tap away once the contract is open in the client).

   Market table (.tbl-market, 8 columns: ignore, Item, Where, Local price,
   Reference, Discount, You save, Stock): kept are Item (the age chip rides
   inline with it, so nothing is lost by hiding a column for it), Where
   (Misaba or Sieh decide which station to fly to, not a redundant detail
   here) and Discount / You save (the two readings of the same deal, kept
   together since a percent and an ISK figure answer different-sized
   purchases). Hidden are Local price and Reference, whose only job is to
   produce the Discount column already shown, and Stock (a caveat on the
   You save ranking, not a decision input on its own). */
@media (max-width: 760px) {
  .tbl-flagged th:nth-child(4), .tbl-flagged td:nth-child(4),  /* Jumps */
  .tbl-flagged th:nth-child(5), .tbl-flagged td:nth-child(5),  /* Ask / Outlay */
  .tbl-flagged th:nth-child(6), .tbl-flagged td:nth-child(6) { /* Est. value */
    display: none;
  }
  .tbl-market th:nth-child(4), .tbl-market td:nth-child(4),  /* Local price */
  .tbl-market th:nth-child(5), .tbl-market td:nth-child(5),  /* Reference */
  .tbl-market th:nth-child(8), .tbl-market td:nth-child(8) { /* Stock */
    display: none;
  }

  /* Hiding four columns is not enough on its own: measured on a 375px phone,
     the five survivors (with the 9px horizontal cell padding and the .tag
     badge's white-space: nowrap carried over from desktop) still summed to
     397px against a ~325px pane, a horizontal scrollbar inside the table
     even though the page body stayed put. Tighter cell padding, a wrapping
     Where tag instead of a nowrap one, and the age chip dropping to its own
     line inside Margin close that gap without hiding anything further. */
  .tbl-flagged th, .tbl-flagged td, .tbl-market th, .tbl-market td { padding: 6px 5px; }
  .tbl-flagged td:nth-child(3), .tbl-flagged th:nth-child(3) { max-width: 92px; } /* Where */
  .tbl-flagged td:nth-child(3) .tag { white-space: normal; overflow-wrap: break-word; }
  .tbl-flagged td:nth-child(7) .age { display: block; margin-left: 0; margin-top: 2px; } /* Margin's age chip */

  /* Sort buttons were a bare text label with no padding: a precise-pointer
     target, not a thumb one. The hit area grows; the visible label does not. */
  button.sort {
    min-height: 30px; padding: 7px 3px;
    display: inline-flex; align-items: center;
  }

  /* The ignore control was 14x14 and invisible until a row hover, which a
     touch screen never fires. On a phone it needs to be seen without a hover
     AND be big enough to hit without also hitting the row underneath it. */
  .ignore-col { width: 28px; padding-left: 4px !important; }
  button.ignore {
    width: 24px; height: 24px; font-size: var(--fs-md);
    color: var(--ink-faint);
  }
  button.ignore:hover, button.ignore:focus-visible { color: var(--down); }
  button.ignore.failed { color: var(--down); }

  /* In-game buttons (open/warp): same reasoning, smaller margin to grow by. */
  .mini { min-height: 32px; padding: 5px 8px; font-size: var(--fs-xs); }
}

/* ---------------------------------------------------------------------------
   The dashboard.

   Four panels, one per system, each showing the top of that system's own
   ranking. Its job is to be readable in a glance and then be left, so
   everything here is tuned for density over decoration: no card art, no
   sparklines, three columns per line, and a hard cap of four lines a panel.
   A fifth line would be the start of a table, and the table already exists on
   its own tab.
--------------------------------------------------------------------------- */
.dash-heads { margin-top: 0; }
/* Three columns, explicitly, not auto-fit.
   UI-REFRESH.md final pass item 5 dropped the Threat panel (its source,
   /api/map, does not exist server-side), leaving Contracts/Buy/Sell -- three
   panels, not four, so this is a 1x3 row now rather than the old 2x2. Below
   1000px it still stacks to one column same as before. */
/* auto-fit with a 360px floor: at 1024 a fixed 3-up grid squeezed Dashboard's
   item names to "A...". min(360px, 100%) keeps it from overflowing at 375px. */
.dash-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
  gap: 14px; margin-top: 14px;
}
@media (max-width: 1000px) { .dash-grid { grid-template-columns: 1fr; } }
.dash-card { margin: 0; }
.dash-card .win-body { padding: 4px 0 6px; }
.dash-card .empty { padding: 14px; }

.d-row {
  display: grid; grid-template-columns: 74px minmax(0, 1fr) auto;
  gap: 10px; align-items: center;
  padding: 7px 14px;
  border-bottom: 1px solid rgba(90, 130, 160, 0.09);
  font: var(--fs-xs) var(--mono); font-variant-numeric: tabular-nums;
}
.d-row:last-child { border-bottom: 0; }
.d-row.clickable { cursor: pointer; }
.d-row.clickable:hover { background: rgba(255, 255, 255, 0.035); }
.d-row.has-cap { border-left: 2px solid var(--down); background: rgba(226, 97, 95, 0.06); }
.d-when {
  color: var(--ink-faint); font-size: var(--fs-xs); letter-spacing: 0.04em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.d-what { min-width: 0; }
.d-what b {
  display: block; color: var(--ink); font-weight: 600; font-size: var(--fs-sm);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.d-what em {
  font-style: normal; font-size: var(--fs-xs); color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block;
}
.d-tag {
  font: 600 var(--fs-xs) var(--mono); white-space: nowrap; text-align: right;
  color: var(--ink);
}
.d-tag.up { color: var(--up); }
.dash-card .win-head .go {
  margin-left: auto; font-size: var(--fs-xs); letter-spacing: 0.08em;
}
.ops-bar b.v-cap { color: var(--down); }

/* ---------------------------------------------------------------------------
   The sell table.

   Moves, then three price columns in a row (Jita ask, Amarr ask, local ask)
   and then what we should list at. The order is the argument: read left to
   right and the recommendation is obviously the right one, or obviously is
   not, and either way you did not have to trust it.
--------------------------------------------------------------------------- */
.tbl-sell td, .tbl-sell th { white-space: nowrap; }
.tbl-sell .i-name { vertical-align: middle; }
.tbl-sell .num em.sub {
  display: block; font-style: normal; font-size: var(--fs-xs);
  color: var(--ink-faint); letter-spacing: 0.02em;
  /* UI-REFRESH.md final pass item 3: the table is table-layout:fixed (below)
     with no explicit column widths, so a sub-line wider than its column --
     "~8.3d to clear" under Moves was the reported case -- does not grow the
     column, it spills as unwrapped text over the Reference cell next door.
     Wrapping it within its own cell instead of the next one over fixes the
     overlap without touching the fixed layout the rest of this table relies
     on for its column alignment. */
  white-space: normal; overflow-wrap: break-word;
}
.tbl-sell .muted { color: var(--ink-faint); }
.tbl-sell tr.row-muted { opacity: 0.55; }
.tbl-sell tr.row-muted:hover { opacity: 1; }
.tbl-sell .up { color: var(--up); }
.flag {
  font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.1em; text-transform: uppercase;
  border: 1px solid var(--line); padding: 1px 4px; color: var(--ink-faint);
  vertical-align: 1px;
}
.flag.own { color: var(--lume-dim); border-color: var(--lume-dim); }
/* Not at Misaba. Loud, because it is the one row where the price is right and
   the location is not where you are standing. */
.flag.away { color: var(--hot); border-color: rgba(229, 170, 74, 0.55); }
.tbl-sell .verdict {
  font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.1em;
  border: 1px solid var(--line); padding: 2px 6px; color: var(--ink-faint);
}
.tbl-sell .verdict.v-list { color: var(--up); border-color: rgba(86, 210, 154, 0.45); }
.tbl-sell .verdict.v-thin { color: var(--hot); border-color: rgba(229, 170, 74, 0.4); }
.tbl-sell .verdict.v-skip { color: var(--down); border-color: rgba(226, 97, 95, 0.35); }
.tbl-sell .verdict.v-no_book { border-style: dashed; }

/* ---------------------------------------------------------------------------
   Win-tier tokens.

   Shared across tabs: green means winning comfortably, yellow means
   marginal, red means losing (or, on the Moves column, not moving at all).
   Same three meaning colours the verdict badges above already use, just as a
   subtle background instead of a border, because a percentage chip sits
   inline in a sub-line rather than standing alone in its own column.
--------------------------------------------------------------------------- */
.pct-chip {
  display: inline-block; font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.02em;
  padding: 1px 5px; border: 1px solid transparent; vertical-align: 1px;
}
.win-g { color: var(--up); background: rgba(86, 210, 154, 0.12); border-color: rgba(86, 210, 154, 0.32); }
.win-y { color: var(--hot); background: rgba(229, 170, 74, 0.12); border-color: rgba(229, 170, 74, 0.32); }
.win-r { color: var(--down); background: rgba(226, 97, 95, 0.12); border-color: rgba(226, 97, 95, 0.32); }
/* A margin good enough to win, sitting unclaimed for a week, is suspicious
   rather than confident: same dashed ink-dim treatment .tag.stale-good always
   used, ported onto the chip so the contracts tables keep the same "something
   is off about this" signal after their margin cell moved onto the shared
   win-tier token. */
.pct-chip.stale-good { color: var(--ink-dim); background: none; border-style: dashed; }

/* The top of the margin range: the same contract valued at Jita asks instead of
   bids. Deliberately colourless. It is context for the number beside it, not a
   second verdict, and giving it a win tier of its own would let a contract read
   green twice for one opportunity. Only rendered when the two ends are more
   than three points apart, so a liquid single-hull contract never grows it. */
.pct-chip.spread {
  margin-left: 4px; color: var(--ink-dim); background: none;
  border-color: var(--line); border-style: dashed;
}

/* "n unpriced": the margin beside it is a floor, not an estimate. Quieter than
   the age chip because it qualifies the number rather than judging it. */
.pct-note {
  margin-left: 5px; font-family: var(--mono); font-size: var(--fs-xs);
  color: var(--ink-faint); letter-spacing: 0; border-bottom: 1px dotted var(--line);
  cursor: help;
}

/* ---------------------------------------------------------------------------
   Hangar: the repackage matrix.

   Same station-window vocabulary as the sell table, one addition: a row can
   open to show the rigs it is about to destroy. The twist control and the
   detail row are deliberately quiet, because the table is a work queue that
   gets scanned top to bottom and an expanded row must not become the loudest
   thing on the page.
--------------------------------------------------------------------------- */
.tbl-hangar { table-layout: fixed; width: 100%; min-width: 1020px; }
.tbl-hangar th, .tbl-hangar td { padding: 6px 7px; font-size: var(--fs-sm); }
/* Hull and Where absorb the remainder; every other column is a tabular number
   or a fixed chip, so they get fixed tracks. */
.tbl-hangar th:nth-child(3), .tbl-hangar td:nth-child(3) { width: 54px; }
.tbl-hangar th:nth-child(4), .tbl-hangar td:nth-child(4) { width: 86px; }
.tbl-hangar th:nth-child(5), .tbl-hangar td:nth-child(5) { width: 112px; }
.tbl-hangar th:nth-child(6), .tbl-hangar td:nth-child(6) { width: 90px; }
.tbl-hangar th:nth-child(7), .tbl-hangar td:nth-child(7) { width: 96px; }
.tbl-hangar th:nth-child(8), .tbl-hangar td:nth-child(8) { width: 84px; }
.tbl-hangar td.where {
  color: var(--ink-dim); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
/* Deliberately NOT `.sub`: that class is a section heading elsewhere in this
   stylesheet and carries a 24px top margin and uppercase tracking. Borrowing it
   here put a heading's margin inside a table cell. */
.tbl-hangar .hclass {
  display: block; color: var(--ink-faint); font-size: var(--fs-xs);
  letter-spacing: 0.06em; text-transform: uppercase;
}
.tbl-hangar .dimnum { color: var(--ink-faint); }

/* A sort label wider than its fixed track used to paint straight over the next
   header: "Rigs cost to replace" was 152px in a 124px cell. The label is
   shorter now, and this makes the failure mode a clip rather than an overlap if
   one ever grows again. */
.tbl-hangar thead th { overflow: hidden; }
.tbl-hangar thead th .sort { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }

.tbl-hangar tr[data-hangar] { cursor: pointer; }
.tbl-hangar tr[data-hangar]:hover { background: var(--lume-wash); }
.tbl-hangar tr.open { background: var(--lume-wash); }

.twist {
  font: 600 var(--fs-xs) var(--mono); line-height: 1;
  width: 16px; height: 16px; padding: 0;
  background: none; border: 1px solid var(--line); color: var(--ink-faint);
  cursor: pointer; vertical-align: 1px;
}
.twist:hover { color: var(--lume); border-color: var(--lume-dim); }

.hangar-detail td { background: var(--win-inactive); }
.riglist { margin: 0; padding: 2px 0 2px 26px; list-style: none; }
.riglist li { font: var(--fs-sm) var(--mono); color: var(--ink-dim); padding: 1px 0; }
.riglist b { color: var(--ink); font-weight: 600; }
.rig-t {
  display: inline-block; min-width: 56px;
  font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-faint);
}
/* Tech II is the expensive one far more often than not, so it is the one that
   carries colour. Tech I stays neutral rather than green: the tab's whole
   argument is that a Tech I rig is not automatically safe to destroy, and
   painting the tier green would say the opposite of the copy next to it. */
.rig-Tech2 { color: var(--hot); }
.tier {
  font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.06em; text-transform: uppercase;
  border: 1px solid var(--line); padding: 1px 5px; color: var(--ink-faint);
  white-space: nowrap;
}
.tier-allTech2 { color: var(--hot); border-color: rgba(229, 170, 74, 0.4); }

/* Same reasoning as .flag.away above: loud, because it is the one row where the
   number in the ISK column is not the whole story. */
.flag.legacy { color: var(--down); border-color: rgba(226, 97, 95, 0.5); }

.tbl-hangar .verdict {
  font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.1em;
  border: 1px solid var(--line); padding: 2px 6px; color: var(--ink-faint);
}
.tbl-hangar .verdict.v-free { color: var(--up); border-color: rgba(86, 210, 154, 0.55); }
.tbl-hangar .verdict.v-go { color: var(--up); border-color: rgba(86, 210, 154, 0.45); }
.tbl-hangar .verdict.v-look { color: var(--hot); border-color: rgba(229, 170, 74, 0.4); }
.tbl-hangar .verdict.v-hold { color: var(--down); border-color: rgba(226, 97, 95, 0.45); }
/* Dashed, the same way the sell table marks a row it could not price. The
   number next to it is a floor, and the border says so before the tooltip
   has to. */
.tbl-hangar .verdict.v-unpriced { border-style: dashed; }

/* The board's own columns collapse before the page does. Below this the seven
   fixed tracks stop fitting and the row wraps into unreadable fragments, so it
   drops to a two-line layout instead: the header goes, and each row keeps its
   own labels through the title attributes it already carries. */
@media (max-width: 860px) {
  .feed-head { display: none; }
  .feed-head, .fight { grid-template-columns: 46px minmax(0, 1fr) 54px 66px; }
  .fight .f-lost, .fight .f-who { display: none; }
  .dash-grid { grid-template-columns: 1fr; }
  .d-row { grid-template-columns: minmax(0, 1fr) auto; }
  .d-when { display: none; }
  .clock { font-size: var(--fs-xs); }
}

/* The expanded card lives in the rail now, not in the feed, so it inherits the
   rail's width and .map-detail's mono type. Both need undoing: the card's own
   type scale is what makes the ship art and the fleet chips readable, and it
   was designed against 486px, not 292px. */
.map-detail .battle {
  font: 600 var(--fs-base)/1.45 var(--sans);
  margin-bottom: 10px; padding: 9px 10px;
  border-left-width: 2px; cursor: default;
}
.map-detail .battle .b-main { gap: 8px; }
.map-detail .battle .b-hero img { width: 56px; height: 56px; }
.map-detail .battle .b-strip { flex-wrap: wrap; }
.map-detail .battle h4 { color: var(--ink); margin: 0; }

/* The sell table is ten columns and the last three carry the whole point: the
   price to list at, what that is realistically worth over 30 days, and the
   call. They were scrolling off the right edge, which is the one failure
   this table cannot have. Three things fix it together: the page goes wide
   like the board does, the icon comes down to row height, and the price
   columns stop reserving space they never use.

   It was eleven, at 11.5px. The Where column went (everything is at Misaba,
   so it printed the same word 300 times) and the width it gave back went into
   type size and into the Price column, which now carries a full unabbreviated
   figure rather than "341.2k". A price you cannot read is a price you cannot
   type into a market order.

   Column order after the market-velocity rework: the Jita buy column is gone
   (we never dump to buy orders, so that comparison was noise) and a Moves
   column takes its old slot, right after Held, because how fast something
   sells belongs next to how much of it we are holding. "vs dumping" is gone
   too; 30d value takes its slot before the call, because that is the number
   the call is about.
     1 ignore  2 item  3 held  4 moves  5 Jita ask
     6 Amarr ask  7 local ask  8 price  9 30d value  10 call */
/* min-width, because the nine fixed tracks below total 762px and everything
   left over goes to the item name. On a narrower window that remainder
   collapses: measured at a 1100px viewport it fell to 71px and 41 of 300 item
   names ellipsised into uselessness. `.table-wrap` already scrolls
   horizontally, so below this the table scrolls instead of crushing the one
   column you read first. */
.tbl-sell { table-layout: fixed; width: 100%; min-width: 1000px; }
.tbl-sell th, .tbl-sell td { padding: 6px 7px; font-size: var(--fs-sm); }
.tbl-sell .icon { width: 22px; height: 22px; vertical-align: -6px; margin-right: 6px; }
.tbl-sell td:nth-child(2) {
  white-space: normal; overflow: hidden; text-overflow: ellipsis;
}
/* Held, moves, the three reference prices, the price, 30d value: all tabular
   numbers (or short tier labels) of known width, so they get fixed tracks
   and the item name absorbs the remainder. */
.tbl-sell th:nth-child(1), .tbl-sell td:nth-child(1) { width: 26px; }
.tbl-sell th:nth-child(3), .tbl-sell td:nth-child(3) { width: 66px; }
.tbl-sell th:nth-child(n+4):nth-child(-n+7),
.tbl-sell td:nth-child(n+4):nth-child(-n+7) { width: 92px; }
/* Wider than the rest on purpose: this one holds 4,900,000,000.99, not 4.90b. */
.tbl-sell th:nth-child(8), .tbl-sell td:nth-child(8) { width: 132px; }
.tbl-sell th:nth-child(9), .tbl-sell td:nth-child(9) { width: 92px; }
.tbl-sell th:nth-child(10), .tbl-sell td:nth-child(10) { width: 78px; }

/* The price cell IS the copy control.
   A separate icon button next to the number would be a second target for the
   same intent, and at this row height a 12px icon is a miss waiting to happen.
   So the number itself is the button: full width of its cell, right aligned
   like the number it replaced, and it looks like text until you go near it. */
.copy-price {
  font: 600 var(--fs-sm) var(--mono);
  color: var(--ink); background: none; border: 1px solid transparent;
  padding: 1px 4px; margin: -1px -4px; cursor: pointer;
  border-radius: 2px; letter-spacing: 0.01em;
  transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}
.copy-price b { font-weight: 600; }
.copy-price:hover { border-color: var(--line); background: rgba(255, 255, 255, 0.04); }
.copy-price:focus-visible { outline: 2px solid var(--lume-dim); outline-offset: 1px; }
.copy-price.copied {
  color: var(--up); border-color: rgba(86, 210, 154, 0.45);
  background: rgba(86, 210, 154, 0.1);
}
.copy-price.copy-failed {
  color: var(--down); border-color: rgba(226, 97, 95, 0.5);
  background: rgba(226, 97, 95, 0.1);
}
/* On the dashboard the price sits inside a sentence, so it keeps the
   surrounding type rather than the table's mono figure. */
.copy-price.inline { font: inherit; font-weight: 600; color: inherit; padding: 0 3px; }

/* ---- sort affordances --------------------------------------------------
 *
 * The active header says WHICH WAY it is sorted, not just that it is sorted.
 * A highlighted column with no direction leaves the reader guessing whether
 * "Local price" means cheapest first or dearest first, which is the whole
 * question they clicked it to answer.
 *
 * The arrow is drawn from data-dir rather than injected into the label, so a
 * table body re-render never has to know about sort state and the header text
 * stays exactly the column name. */
button.sort { position: relative; }
button.sort::after {
  content: ""; display: inline-block; width: 0; height: 0; margin-left: 5px;
  vertical-align: 1px; opacity: 0;
  border-left: 3.5px solid transparent; border-right: 3.5px solid transparent;
  transition: opacity 0.15s;
}
button.sort:hover::after { opacity: 0.35; border-top: 4px solid currentColor; }
button.sort[data-dir]::after { opacity: 1; }
button.sort[data-dir="desc"]::after { border-top: 4px solid currentColor; border-bottom: 0; }
button.sort[data-dir="asc"]::after { border-bottom: 4px solid currentColor; border-top: 0; }

/* In flight. Dimmed and non-interactive rather than blanked: the previous
 * answer is still the best information on screen until the new one lands, and
 * replacing it with a spinner throws that away. A second click during the
 * request is also the thing that used to produce an out-of-order render, so
 * the pointer events go with it. */
.table-wrap.is-busy { position: relative; }
.table-wrap.is-busy tbody { opacity: 0.4; pointer-events: none; transition: opacity 0.12s; }
.table-wrap.is-busy::after {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--lume), transparent);
  animation: sortscan 0.9s linear infinite;
}
@keyframes sortscan {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .table-wrap.is-busy::after { animation: none; opacity: 0.5; }
}

/* ---- blueprint copies --------------------------------------------------
 *
 * The evidence chip is colour-coded by how much weight the match actually
 * carries. A `same_type` match compares a researched copy against an
 * unresearched one and calls them the same good, which is weak; it must not
 * look identical to an exact runs/ME/TE match at a glance, or the verdict
 * beside it reads as more certain than it is. */
.tbl-bpc td, .tbl-bpc th { white-space: nowrap; }
.tbl-bpc .i-name { vertical-align: middle; }
.tbl-bpc .icon { width: 22px; height: 22px; vertical-align: -6px; margin-right: 6px; }
.tbl-bpc em.sub {
  display: block; font-style: normal; font-size: var(--fs-xs);
  color: var(--ink-faint); letter-spacing: 0.02em;
}
.tbl-bpc .muted { color: var(--ink-faint); }
.tbl-bpc .up { color: var(--up); }
.tbl-bpc .down { color: var(--down); }
.tbl-bpc .sub { font-size: var(--fs-xs); color: var(--ink-faint); }
.flag.m-strong { color: var(--up); border-color: rgba(86, 210, 154, 0.45); }
.flag.m-ok { color: var(--lume); border-color: var(--lume-dim); }
.flag.m-weak { color: var(--hot); border-color: rgba(229, 170, 74, 0.4); }
.flag.m-none { border-style: dashed; }
.tbl-bpc .verdict {
  font: 600 var(--fs-xs) var(--mono); letter-spacing: 0.1em;
  border: 1px solid var(--line); padding: 2px 6px; color: var(--ink-faint);
}
.tbl-bpc .verdict.v-deal { color: var(--up); border-color: rgba(86, 210, 154, 0.45); }
.tbl-bpc .verdict.v-overpriced { color: var(--down); border-color: rgba(226, 97, 95, 0.45); }
.tbl-bpc .verdict.v-fair { color: var(--ink-dim); }
.tbl-bpc .verdict.v-thin { color: var(--hot); border-color: rgba(229, 170, 74, 0.4); }
.tbl-bpc .verdict.v-no_comparable { border-style: dashed; }

/* A same_type match is weak evidence (a researched copy priced against an
   unresearched one), so the whole row steps back, same rule the sell table
   already uses for its own row-muted rows. */
.tbl-bpc tr.row-muted { opacity: 0.55; }
.tbl-bpc tr.row-muted:hover { opacity: 1; }
/* The delta chip specifically, one step dimmer again on top of the row: it is
   the number carrying the weakest claim on the row, and it should read as
   the quietest thing in it even before the row-level opacity is applied. */
.pct-chip.weak-evidence { opacity: 0.6; }

/* Per-run comparison, two stacked bars scaled to whichever side is bigger, so
   over/under reads before the numbers do. `over` (ours longer than Jita's)
   reuses the down token; the other bar is a neutral track colour rather than
   "winning green", because a short Jita bar is not itself a deal, it is just
   the smaller of two lengths. */
.run-bar { display: block; margin-top: 3px; width: 100%; max-width: 64px; }
.run-bar-seg { display: block; height: 2px; margin-bottom: 1px; background: var(--lume-dim); border-radius: 1px; }
.run-bar-seg.run-bar-jita { background: rgba(90, 130, 160, 0.35); }
.run-bar-seg.run-bar-ours.over { background: var(--down); }

/* Filter chips, same visual language as the threat board's .ops-filters but a
   separate class: that block's click handler is a delegated global listener
   scoped to ".ops-filters .chip" specifically, and reusing the class would
   have these chips firing it too. Counts ride in a dimmer inline <b> so the
   label reads first and the number confirms it, not the other way round. */
.bpc-filters { display: flex; gap: 4px; flex-wrap: wrap; }
.bpc-filters .chip {
  background: none; border: 1px solid var(--line); color: var(--ink-faint);
  min-height: 22px; padding: 2px 9px; font: 600 var(--fs-xs) var(--mono);
  letter-spacing: 0.1em; text-transform: uppercase; cursor: pointer; box-shadow: none;
  transition: color 0.18s, border-color 0.18s;
}
.bpc-filters .chip b { font-weight: 600; color: inherit; opacity: 0.65; margin-left: 2px; }
.bpc-filters .chip:hover { color: var(--ink); border-color: var(--line-hi); background: none; }
.bpc-filters .chip.on { color: var(--lume); border-color: var(--lume-dim); background: var(--lume-wash); }

/* Ten columns, and as with the sell table the last three carry the answer, so
   they are not allowed to scroll off the right edge. */
.tbl-bpc { table-layout: fixed; width: 100%; }
.tbl-bpc th, .tbl-bpc td { padding: 5px 7px; font-size: var(--fs-xs); }
/* Indices are one further along than they look: column 1 is the hide control. */
.tbl-bpc th:nth-child(3), .tbl-bpc td:nth-child(3) {
  width: 150px; overflow: hidden; text-overflow: ellipsis;
}
.tbl-bpc th:nth-child(n+4):nth-child(-n+7),
.tbl-bpc td:nth-child(n+4):nth-child(-n+7) { width: 88px; }
.tbl-bpc th:nth-child(8), .tbl-bpc td:nth-child(8) { width: 108px; }
.tbl-bpc th:nth-child(9), .tbl-bpc td:nth-child(9) { width: 84px; }
.tbl-bpc th:nth-child(10), .tbl-bpc td:nth-child(10) { width: 62px; }
.tbl-bpc th:nth-child(11), .tbl-bpc td:nth-child(11) { width: 96px; }

/* A count that is also the way to undo it. Styled as text, not a button, so it
   reads as part of the sentence it lives in rather than a competing control. */
.linky {
  background: none; border: 0; padding: 0; min-height: 0;
  color: var(--lume-dim); font: inherit; font-size: inherit;
  text-transform: none; letter-spacing: inherit;
  text-decoration: underline dotted; cursor: pointer;
}
.linky:hover { color: var(--lume); background: none; box-shadow: none; }
button.ignore.failed { color: var(--down); }

/* ---- contracts: four panels, one screen --------------------------------
 *
 * Share of the remaining viewport height, per the operator's split:
 * flagged 1, Domain low 2, Derelik 1, Domain high 1. Domain low gets double
 * because it is the primary hunting ground; the other three are a glance.
 *
 * The point is that all four are on screen at once with no page scroll, so
 * each one scrolls INSIDE itself. `--share` is set per section in the markup
 * rather than by nth-child, so the ratio is visible where the panels are
 * declared and reordering them cannot silently reassign the weights.
 *
 * min-height on every flex descendant is the load-bearing part. Without it a
 * flex item refuses to shrink below its content and the stack grows past the
 * viewport instead of distributing, which is the whole thing this replaces. */
/* main's 80px bottom padding sits OUTSIDE this element's height, so
   subtracting only the header left the page scrolling by exactly that much:
   four panels sized to fit, inside a page that still did not. The compact view
   drops the padding to match. */
body.compact-contracts main { padding-bottom: 16px; }

.cstack {
  display: flex;
  flex-direction: column;
  gap: 12px;
  /* Topstrip, main padding and a little breathing room. dvh so a mobile
     browser's collapsing toolbar does not leave a dead band at the bottom. */
  height: calc(100vh - 118px);
  height: calc(100dvh - 118px);
  min-height: 460px;
}
.cstack .cpanel {
  flex: var(--share, 1) 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  margin: 0;
}
.cstack .cpanel .win-head { flex: none; }
.cstack .cpanel .win-body {
  flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0;
}
.cstack .cpanel .table-wrap {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  /* Beats the .capped max-height, which was sized for a page that scrolled. */
  max-height: none;
}
/* Sticky headers, now that each panel is its own scroll container. Scrolling
   the Domain low panel past twenty rows used to lose the column names. */
.cstack .cpanel thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--win-head);
}

/* Below this the four panels are 90px each and useless. Give up on the split
   and let the page scroll, which is the honest fallback on a short screen. */
@media (max-height: 620px), (max-width: 860px) {
  .cstack { height: auto; min-height: 0; display: block; }
  .cstack .cpanel { display: block; margin-bottom: 12px; }
  .cstack .cpanel .table-wrap { max-height: 42vh; }
}

/* The "ladder" tag on a Buy row. Small, and it has to be there: a price that
   came from Settings rather than the market looks identical otherwise, and the
   whole reason capitals were misreported was an unlabelled reference. */
.tbl-market em.sub {
  display: block; font-style: normal; font-size: var(--fs-xs);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--lume-dim);
}

/* The outlay breakdown on a trade-style contract. Two numbers because they are
   two different obligations: ISK out of the wallet, and items you must own or
   go and buy. */
.tbl-flagged em.sub {
  display: block; font-style: normal; font-size: var(--fs-xs);
  color: var(--ink-faint); letter-spacing: 0.02em; white-space: nowrap;
}

/* The empty board states its evidence, so it needs to read as a paragraph
   rather than a shrug. Wider and left aligned, unlike the terse empties in
   table bodies. */
.feed-empty {
  text-align: left; max-width: 72ch; line-height: 1.65;
  padding: 18px 4px; color: var(--ink-dim);
}
.feed-empty b { color: var(--ink); font-weight: 600; }
.feed-empty b.v-cap { color: var(--down); display: block; margin-bottom: 6px; }

/* ---- rail icons and panel body -----------------------------------------

   The floating window manager (RIFT-style drag/resize/minimise desktop,
   `WM`/`WIN_META`-driven window frames, `body.wm`/`wm-drag`/`.win-frame`/
   `.win-bar`/`.win-btn`/`.win-title`/`.win-spacer`/`.win-grip`/`.topstrip`
   chrome) is gone (UI-REFRESH.md section 1, Andy: "let's go back to each nav
   opens its own tab"). One view shows at a time as a plain stacked page; see
   `.sidebar`/`.page-header` above and app.js's navigate(). What survives
   below is the rail glyph styling; `.win-body`'s own padding is still set
   once, near `section`/`.win-head` above (that IS the shared panel style --
   see the ".panel" section of the shared-components block near the top of
   this file), kept as the plain content wrapper class every view's markup
   already used -- renaming ~40 ids across every view for a class name alone
   is out of scope for a shell-only phase. */

.nav-ico {
  width: 15px; height: 15px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.2;
  stroke-linecap: round; stroke-linejoin: round;
  opacity: 0.85;
}
/* The dashboard glyph is the one solid-filled mark in the set. */
#tab-dash .nav-ico { fill: currentColor; stroke: none; }
.nav-tab[aria-current="page"] .nav-ico { opacity: 1; }

/* The views were written as page bodies, with a top margin between sections
   and their own window headers. Inside a panel the first of those is a
   double frame, so the inner chrome stands down. */
.win-body > div > section:first-child { margin-top: 0; }
.win-body > div > .ops-bar:first-child { margin-top: 0; }

/* ---- entity chips -----------------------------------------------------

   RIFT's rows read as intel rather than as a spreadsheet because every entity
   in them carries its own art: the ship, the pilot, the corp. The Threat board
   already did this for alliances and hulls; this is the shared primitive so
   the rest of the app can too.

   Art comes from images.evetech.net, which is CCP's public image CDN: no
   auth, no key, no request against our own worker or our ESI budget. */

.ent { display: flex; align-items: center; gap: 9px; min-width: 0; }
.ent-txt { display: flex; flex-direction: column; min-width: 0; gap: 1px; }

/* Both dimensions are set here, not left to the width/height attributes. An
   attribute pair alone gives the intrinsic ratio, and the moment any inherited
   rule touches one axis the image stretches on the other. Fixing both in CSS
   is what stops a 64px render arriving as a 34x64 smear. */
.ent-art { width: 34px; height: 34px; flex: none; object-fit: contain; }
.ent-ico { width: 22px; height: 22px; flex: none; object-fit: contain;
  vertical-align: -5px; }

/* A type with no rendition of this kind. Held in the layout rather than
   removed, so a table of ships does not lose its column alignment on the one
   row whose art is missing. */
.ent-blank { visibility: hidden; }

/* The hangar's name cell is the widest thing in the row and must not be the
   thing that decides the table's width. */
#hangar .ent-txt { overflow: hidden; }
#hangar .ent-txt b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- the Map module ---------------------------------------------------

   The map used to be a collapsed <details> in the Threat rail, about 150px
   wide, rendering a 1500x880 viewBox at roughly a tenth of scale. Its own
   window now, because windows removed the reason it was ever small: it no
   longer buys width by taking it from the feed. */

/* align-items STRETCH, not flex-start. flex-start was the first thing written
   here and it silently defeated the fill-height rules further down: the wrap
   was the full height of the window, but its children aligned to the top and
   sized to their own content, so the map went on measuring 989x580 inside an
   828px-tall box. Nothing about that reads as a bug in the height chain, which
   is where the time went looking for it. */
.sector-wrap { display: flex; gap: 14px; align-items: stretch; min-width: 0; }
.sector-wrap .map-canvas { flex: 1 1 auto; min-width: 0; padding: 0; }
.sector-side {
  flex: 0 0 clamp(220px, 22%, 320px); min-width: 0;
  display: flex; flex-direction: column; gap: 10px;
}
.sector-side .map-detail { margin: 0; }

/* Under about 1100px of window the side column stops being a column and
   becomes a row underneath, rather than squeezing the map it exists to
   annotate. A container query, not a media query: this is about how wide the
   WINDOW is, and the window is dragged independently of the viewport. */
@container (max-width: 1100px) {
  .sector-wrap { flex-direction: column; }
  .sector-side { flex: 1 1 auto; width: 100%; flex-direction: row; flex-wrap: wrap; }
  .sector-side .map-detail { flex: 1 1 260px; }
}
.win-body { container-type: inline-size; }

/* ---- kill markers ----

   A crosshair over any system with a kill in the window. The node's FILL
   already carries heat, but hue on a small dot is a weak channel: an amber
   "recent" and a cyan "warm" are two similar dots at a glance. Shape is the
   channel that survives being small and being looked at for a quarter of a
   second, which is the whole job of this map. */
.map-svg .kx { pointer-events: none; }
.map-svg .kx-ring { fill: none; stroke: var(--down); stroke-width: 1.1; opacity: 0.75; }
.map-svg .kx-tick { stroke: var(--down); stroke-width: 1.4; stroke-linecap: round; opacity: 0.95; }
/* A kill that is over an hour old is history, not a threat, so its marker
   stands down to a hairline rather than competing with the live ones. */
.map-svg .kx-warm .kx-ring, .map-svg .kx-warm .kx-tick { stroke: var(--ink-faint); opacity: 0.6; }
.map-svg .kx-recent .kx-ring, .map-svg .kx-recent .kx-tick { stroke: var(--hot); }

/* ---- links, coloured by the more dangerous end ----

   Was one flat blue for every gate. The security of a connection is the first
   thing read off an in-game map and it was the one thing this one did not say. */
.map-svg .e-hi { stroke: rgba(86, 210, 154, 0.34); }
.map-svg .e-lo { stroke: rgba(229, 170, 74, 0.34); }
.map-svg .e-null { stroke: rgba(226, 97, 95, 0.30); }

/* Label SIZE is set inline by mapLabels(), which divides it by the zoom so
   names hold a constant apparent size. Deliberately no font-size rule here:
   any rule at all, however specific, outranks an SVG presentation attribute,
   and one here is what defeated the first attempt at counter-scaling. */

/* The lowsec node stroke, which has never rendered.
   map-static.json writes band "low"; the rule below was written as `.b-lo`.
   25 of 102 systems therefore carried class="b-low" and matched nothing.
   bandClass() in app.js now normalises both spellings to `lo`, so `.b-lo`
   above finally applies; this block only records why it was missing. */

/* Fill the window in BOTH axes.

   The global rule is `.map-svg svg { width: 100%; height: auto }`, which sizes
   the map off its container's WIDTH alone. In the rail that was right: width
   was the only axis that existed. In a window it wastes whatever height the
   operator dragged out, so a 1500x880 viewBox in an 850px-tall window drew at
   580px and left a third of the window empty.

   Letting the SVG take 100% of both and leaning on its own preserveAspectRatio
   (xMidYMid meet, the default) makes it as large as the window allows on
   whichever axis binds first, and centres it in the other.

   Height percentages need a definite height at every step, which is why the
   view and the wrap are pinned rather than left to shrink-wrap their content. */
.win-body > #view-sector { height: 100%; min-height: 0; }
.sector-wrap { height: 100%; min-height: 0; }
.sector-wrap .map-canvas { display: flex; min-height: 0; min-width: 0; }
.sector-wrap .map-svg { flex: 1 1 auto; display: flex; min-height: 0; min-width: 0; }
.sector-wrap .map-svg svg { width: 100%; height: 100%; }
.sector-side { overflow: auto; }

/* Stacked (narrow window) the map has no height to fill, so it goes back to
   sizing off width or it collapses to nothing. */
@container (max-width: 1100px) {
  .win-body > #view-sector, .sector-wrap { height: auto; }
  .sector-wrap .map-svg svg { height: auto; }
}

/* ---- map navigation ----

   The map pans and zooms now. Everything below is about making that legible:
   the cursor has to say "this is draggable" before anyone tries it, and the
   hint line has to be there the first time and out of the way afterwards. */
.sector-wrap .map-canvas { position: relative; }
.map-svg { cursor: grab; touch-action: none; }
.map-svg.panning { cursor: grabbing; }
.map-svg svg { user-select: none; }
/* The nodes keep their own pointer cursor: they are the one thing on the map
   that is clicked rather than dragged. */
.map-svg .n { cursor: pointer; }

.map-reset {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  min-height: 24px; padding: 3px 9px; font-size: var(--fs-xs);
}
.map-hint {
  position: absolute; left: 10px; bottom: 6px; margin: 0;
  font-size: var(--fs-xs); letter-spacing: 0.06em; color: var(--ink-faint);
  pointer-events: none; opacity: 0.75;
}

/* Labels get a backing plate so they stay readable over a gate line or a
   neighbouring node once the map is zoomed in and things overlap. RIFT boxes
   its system names for the same reason. */
.map-svg .lbl-bg {
  fill: rgba(5, 9, 15, 0.72);
  stroke: rgba(90, 130, 160, 0.16);
  stroke-width: 0.6;
}
/* Same reason: no font-size for the zoomed case either. mapLabels() owns it. */
.map-svg.zoomed .lbl-bg { fill: rgba(5, 9, 15, 0.82); }

/* ---- Build tab ---------------------------------------------------------- */
/* A notch above the app's 12px table base: this is the densest page and the
   one read longest, and 11px detail text was the first thing reported. */
/* UI-REFRESH.md phase 2: Stocked and Job fee folded into Ready/Gap ISK as
   sub-lines (see renderBuild() in app.js), so this table is 7 columns now,
   not the 9 the old 1100px min-width was sized for -- that stale floor was
   forcing a 1280px viewport into a 72px horizontal scroll to reach Time, the
   exact "columns lost, and the scrollbar to reach them is easy to miss"
   defect UI-REFRESH.md names. */
.tbl-build { table-layout: fixed; width: 100%; min-width: 880px; font-size: var(--fs-sm); }
.tbl-build th:first-child { width: 32%; }
.build-row { cursor: pointer; }
.build-detail td { background: var(--win-inactive); padding: 10px 14px; }
.detail-grid { display: flex; gap: 22px; align-items: flex-start; flex-wrap: wrap; }
.detail-grid .tbl-mini { width: auto; min-width: 560px; font-size: var(--fs-sm); }
.tbl-mini th, .tbl-mini td { padding: 4px 10px; }
.detail-grid .modes { max-width: 520px; font-size: var(--fs-sm); line-height: 1.65; }
.flag.build-flag { color: var(--up); border-color: rgba(86, 210, 154, 0.5); }
/* The winning option in the four-way buy/build matrix. */
.tbl-mini td.win { color: var(--up); font-weight: 600; background: rgba(86, 210, 154, 0.07); }
/* A material we own the blueprint for: reads as a link, jumps to its row. */
.matlink {
  background: none; border: 0; padding: 0; font: inherit; cursor: pointer;
  color: var(--lume, #7fd4ff); text-decoration: underline;
  text-underline-offset: 2px; text-decoration-color: rgba(127, 212, 255, 0.4);
}
.matlink:hover { text-decoration-color: currentColor; }
/* Where a chain jump landed. */
tr.jump-hit td { background: rgba(86, 210, 154, 0.12); transition: background 1.2s ease; }
.detail-grid .modes p { margin: 0 0 8px; }
/* Verdict colours as plain text spans, the way the build table uses them. */
#view-build .v-go { color: var(--up); }
#view-build .v-stop { color: var(--down); }
#view-build .v-hold { color: var(--hot); }

/* ---- Moons tab : Metenox moon war tracker -------------------------------- */
/* Design, model and every assumption: FACET-MOONS.md. */

#view-moons .tbl-moons-timers,
#view-moons .tbl-moons-planner,
#view-moons .tbl-moons-all { width: 100%; font-size: var(--fs-sm); }

/* A timer within the last 2h stays listed, dimmed, as "out" rather than
   vanishing: the operator asked for that history to stay visible for a
   beat, not disappear the instant it expires. */
#view-moons tr.out { opacity: 0.45; }
#view-moons tr.out .countdown { color: var(--ink-faint); }

#view-moons .countdown { font-family: var(--mono); white-space: nowrap; }

#view-moons .moons-hp { font-family: var(--mono); font-size: var(--fs-sm); color: var(--ink-dim); }

/* ---- Timerboard row redesign (Andy 2026-09-24: "hard to read, messy and
   not ideal") -----------------------------------------------------------
   One line per timer, four zones left to right: countdown, structure,
   status chips, value/actions. Grouped under "Window open" / "Next 24
   hours" / "Later" headers (.mtr-group-head); expired timers collapse last
   under "Ended recently". .tbl-moons-timers now holds one <td> per row (no
   header columns to align against), so this owns its own layout rather than
   reusing the generic table cell rules above. */
#view-moons .tbl-moons-timers td { padding: 0; border-bottom: 1px solid var(--line); }
#view-moons .tbl-moons-timers td.empty { padding: 20px 12px; border-bottom: none; }
#view-moons .tbl-moons-timers tr:last-child td { border-bottom: none; }
#view-moons .tbl-moons-timers tr.mtr-group-head td { border-bottom: none; }

.mtr {
  display: flex; align-items: center; gap: 14px;
  padding: 8px 4px; min-height: 44px;
}
.mtr-secondary { display: contents; }

.mtr-countdown { flex: 0 0 96px; display: flex; flex-direction: column; }
.mtr-countdown .countdown {
  font: 700 var(--fs-lg)/1.1 var(--mono); font-variant-numeric: tabular-nums; color: var(--ink);
}
.mtr-countdown .mtr-eve { display: block; color: var(--ink-faint); font-size: var(--fs-xs); margin-top: 2px; }
.mtr-countdown.hot .countdown { color: var(--down); }
.mtr-countdown.warm .countdown { color: var(--hot); }
#view-moons tr.out .mtr-countdown .mtr-eve { color: var(--ink-faint); }

.mtr-structure { flex: 1 1 auto; min-width: 0; }
.mtr-structure b {
  display: block; font-size: var(--fs-base); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mtr-structure small {
  display: block; color: var(--ink-dim); font-size: var(--fs-xs);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.mtr-chips { flex: 0 1 auto; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

/* Three-segment health bar, monochrome fill (colour stays reserved for
   verdicts elsewhere): each segment's height reads its own percentage via a
   custom property so no inline width math is needed per row. */
.mtr-health { display: inline-flex; align-items: flex-end; gap: 2px; width: 26px; height: 14px; vertical-align: middle; }
.mtr-health-seg {
  flex: 1; position: relative; height: 100%; background: rgba(125, 148, 168, 0.16);
}
.mtr-health-seg::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: var(--pct, 0%);
  background: var(--lume-dim);
}

.mtr-stake { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.mtr-stake-figure { font-size: var(--fs-sm); color: var(--ink); white-space: nowrap; }
.mtr-actions { display: flex; gap: 4px; }
.mtr-actions .btn.mini.icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0;
}

.mtr-group-head td { padding: 10px 4px 4px; }
.tbl-moons-timers tbody tr.mtr-group-head:first-child td { padding-top: 4px; }
.mtr-group-head h3 {
  margin: 0; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--ink-faint); display: flex; align-items: center; gap: 6px;
}
.mtr-group-count { color: var(--ink-dim); font-weight: 400; }
.mtr-ended-toggle {
  background: none; border: none; padding: 4px 0; color: var(--ink-dim);
  font: inherit; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.06em; cursor: pointer;
}
.mtr-ended-toggle:hover { color: var(--ink); }

@media (max-width: 560px) {
  /* Two lines per timer (spec item 3): line 1 is countdown + name (the
     default flex flow above already puts those together without wrapping);
     .mtr-secondary switches from display:contents (a no-op wrapper on
     desktop) to a real flex row, which is what forces chips + at-stake +
     icons onto their own second line together. */
  .mtr { flex-wrap: wrap; row-gap: 6px; padding: 10px 0; align-items: flex-start; }
  .mtr-secondary {
    display: flex; flex: 1 1 100%; align-items: center; justify-content: space-between; gap: 10px;
  }
  .mtr-chips { flex: 0 1 auto; }
  /* The stake line ("719m hull, 1.07b/30d") wraps instead of pushing the
     icon buttons past the edge. */
  .mtr-stake { flex: 1 1 auto; min-width: 0; flex-direction: row; align-items: center; justify-content: flex-end; gap: 8px; }
  .mtr-stake-figure { white-space: normal; text-align: right; min-width: 0; }
  .mtr-actions .btn.mini.icon { width: 44px; height: 44px; }
}

/* 24-cell UTC strip: our prime time green, theirs red, overlap amber, the
   best-window cells brighter, the timer's exit hour outlined, the
   reinforce hour marked with a dot. Monochrome-icon rule doesn't apply here:
   colour IS the meaning being shown (whose window, and when to hit it). */
.moons-strip { display: flex; gap: 1px; }
.moons-strip .cell {
  width: 10px; height: 16px;
  background: rgba(125, 148, 168, 0.08);
  position: relative;
}
.moons-strip .cell.green { background: rgba(86, 210, 154, 0.22); }
.moons-strip .cell.red { background: rgba(226, 97, 95, 0.22); }
.moons-strip .cell.amber { background: rgba(229, 170, 74, 0.28); }
.moons-strip .cell.bright.green { background: var(--up); }
.moons-strip .cell.bright.red { background: var(--down); }
.moons-strip .cell.bright.amber { background: var(--hot); }
.moons-strip .cell.exit { outline: 1px solid var(--lume); outline-offset: -1px; }
.moons-strip .cell.reinforce::after {
  content: ""; position: absolute; left: 50%; bottom: 1px;
  width: 3px; height: 3px; border-radius: 50%;
  background: var(--ink); transform: translateX(-50%);
}
/* Their observed STRIKE hours (item 1, FACET-MOON-INTEL.md): a hatch overlay,
   distinct from the red "their defend window" fill underneath it. */
.moons-strip .cell.strike {
  background-image: repeating-linear-gradient(
    45deg, rgba(226, 97, 95, 0.55) 0, rgba(226, 97, 95, 0.55) 2px, transparent 2px, transparent 4px
  );
}

/* ---- Intel section (FACET-MOON-INTEL.md items 1 and 4) -------------------- */

.moons-intel-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px;
}
.moons-intel-card h3 {
  margin: 0 0 8px; font-size: var(--fs-sm); color: var(--ink-dim); display: flex; align-items: center; gap: 6px;
}
.hist { display: flex; align-items: flex-end; gap: 2px; height: 64px; margin-bottom: 6px; }
.hist-bar {
  flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center;
  height: 100%; min-width: 6px;
}
.hist-bar span {
  display: block; width: 100%; min-height: 1px;
  background: rgba(125, 148, 168, 0.35); border-radius: 1px 1px 0 0;
}
.hist-bar.hi span { background: var(--hot); }
.hist-bar small { font-size: var(--fs-xs); color: var(--ink-faint); margin-top: 2px; }

.measured-row {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 4px 0; border-bottom: 1px solid var(--line);
  font-size: var(--fs-sm);
}
.measured-row:last-child { border-bottom: none; }
.measured-row span { flex: 1 1 auto; color: var(--ink-dim); }
/* Explicit size, not the UA default: <small> shrinks to 83.3% of its parent
   with no rule of its own, which put it under the 12px floor once the
   parent's own size was tokenized. */
.measured-row small { color: var(--ink-faint); font-size: var(--fs-xs); }
.btn.mini { padding: 2px 8px; font-size: var(--fs-xs); }
/* item 14 (REVIEW-C1-PRODUCT.md): "set fleet DPS in Assumptions" reads as a
   sentence fragment, not a standalone action - underlined text, not a boxed
   button, so it sits naturally inline in the planner's "to reinforce: ..."
   cell. */
.btn.mini.link {
  background: none; border: none; padding: 0; color: var(--primary);
  text-decoration: underline; font-size: inherit; cursor: pointer;
}
.btn.mini.link:hover { color: var(--lume); }

#view-moons .tbl-moons-kills { width: 100%; font-size: var(--fs-sm); }

.moon-row { cursor: pointer; }
.moon-detail td { background: var(--win-inactive); padding: 10px 14px; }
.moon-form {
  display: grid; grid-template-columns: repeat(3, minmax(160px, 1fr)); gap: 10px 16px;
  min-width: 560px; align-items: end;
}
.moon-form label { display: flex; flex-direction: column; gap: 4px; font-size: var(--fs-sm); color: var(--ink-dim); }
.moon-form label.wide { grid-column: 1 / -1; }
.moon-form input, .moon-form select, .moon-form textarea {
  background: var(--hangar); border: 1px solid var(--line); color: var(--ink);
  padding: 5px 8px; border-radius: var(--radius); font: inherit;
}
.moon-form textarea { min-height: 48px; resize: vertical; }
.moon-form button.btn { justify-self: start; }
.moon-form [data-moon-hint] { grid-column: 1 / -1; margin: 0; color: var(--hot); }
.moon-events { min-width: 320px; max-width: 480px; }

.moons-config-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px 16px;
  margin-bottom: 10px;
}
.moons-config-grid label { display: flex; flex-direction: column; gap: 4px; font-size: var(--fs-sm); color: var(--ink-dim); }
.moons-config-grid input {
  background: var(--hangar); border: 1px solid var(--line); color: var(--ink);
  padding: 5px 8px; border-radius: var(--radius); font: inherit;
}

/* Verdict colours as plain text spans, same convention as the Build tab. */
#view-moons .v-go { color: var(--up); }
#view-moons .v-stop { color: var(--down); }

/* ---- Moons UX pass -------------------------------------------------------- */
/* Design: FACET-MOONS-UX.md. */

.moons-find { padding: 4px 0 10px; position: relative; }
.moons-find input { min-width: 320px; }
.moons-find .note { margin: 4px 0 0; }

/* Live search dropdown (Andy 2026-09-24: "typing ussad 4 or just ussad
   should list results"). Tracked-moon rows and untracked-system rows share
   one list; each row is at least 44px tall for a comfortable phone tap. */
.moons-find-dropdown {
  position: absolute; top: 100%; left: 0; z-index: 20;
  width: min(420px, 100%); max-height: 340px; overflow-y: auto; overflow-x: hidden;
  background: var(--hangar); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0,0,0,.35); margin-top: 4px; box-sizing: border-box;
}
.moons-find-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px; min-height: 44px; padding: 6px 10px;
  cursor: pointer; border-bottom: 1px solid var(--line); font-size: var(--fs-sm); box-sizing: border-box;
}
.moons-find-row:last-child { border-bottom: none; }
.moons-find-row:hover, .moons-find-row.active { background: rgba(255,255,255,.06); }
.moons-find-row .mf-name { flex: 1 1 100%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.moons-find-row .mf-meta { color: var(--ink-dim); font-size: var(--fs-xs, .8em); flex: 1 1 auto; min-width: 0; text-align: right; }
.moons-find-row.mf-system .mf-name { color: var(--ink-dim); }
.moons-find-empty { padding: 10px; color: var(--ink-dim); font-size: var(--fs-sm); }
@media (min-width: 561px) {
  /* Enough room to keep the name on its own line and everything else
     trailing on the first line, same visual weight as the rest of the
     Moons tab's tag-and-meta rows. */
  .moons-find-row .mf-name { flex: 1 1 auto; }
}
@media (max-width: 560px) {
  .moons-find-dropdown { width: 100%; }
}

/* One-click lifecycle actions (section 3) and the quick health entry
   (section 1), shown above the manual form in the expanded moon row/card. */
.moons-lifecycle { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.moon-quick-health {
  display: flex; flex-wrap: wrap; align-items: end; gap: 8px;
  margin-bottom: 10px; padding: 8px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--hangar);
}
.moon-quick-health label { display: flex; flex-direction: column; gap: 4px; font-size: var(--fs-sm); color: var(--ink-dim); }
.moon-quick-health input, .moon-quick-health select {
  background: var(--hangar); border: 1px solid var(--line); color: var(--ink);
  padding: 5px 8px; border-radius: var(--radius); font: inherit;
}

/* Mobile card layout (FACET-MOONS-UX.md section 9): under 560px the All
   moons TABLE (.moons-all-wrap) hides and .moons-cards becomes the table's
   phone-width stand-in, one card per row with a single Open/Close action.
   (The scout list moved off this pattern entirely -- see the scout-* rules
   below -- because a fixed-height card per system was the "takes way too
   much space" complaint.) */
.moons-cards { display: none; flex-direction: column; gap: 8px; }
.moon-card {
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 10px 12px; background: var(--win-inactive);
}
.moon-card-top { display: flex; align-items: center; gap: 8px; font-weight: 600; margin-bottom: 4px; }
.moon-card-mid { display: flex; justify-content: space-between; gap: 8px; color: var(--ink-dim); font-size: var(--fs-sm); margin-bottom: 6px; }
.moon-card-mid .countdown { font-family: var(--mono); }
.moon-card-bottom { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.moon-card.open .detail-grid { margin-top: 10px; }

/* Scout list v2 (Andy: "taking way too much space, not great"). Collapsed to
   one line by default; a table row per system when open, same row height as
   the All moons table (.tbl-mini already sets that). No card chrome. */
.moons-scout-section { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; margin-bottom: 14px; background: var(--win); }
.scout-summary-bar {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  padding: 10px 14px; background: var(--win-head); border: none; color: var(--ink);
  font: inherit; font-weight: 600; cursor: pointer;
  /* This bar is a section title (Scout / Re-drop watch), not a control, so it
     opts out of the generic `button { text-transform: uppercase }` rule the
     same way the h2 fix above does: sentence case at a readable size, same
     reasoning as UI-REFRESH.md section 4. */
  text-transform: none; letter-spacing: normal; font-size: var(--fs-md);
}
.scout-summary-bar:hover { background: var(--win-inactive); }
.scout-summary-text { font-weight: 400; font-size: var(--fs-sm); color: var(--ink-dim); margin-left: 2px; }
.scout-summary-chevron { margin-left: auto; transition: transform 0.15s ease; }
.scout-summary-bar.open .scout-summary-chevron { transform: rotate(90deg); }
.tbl-moons-scout td, .tbl-moons-scout th { vertical-align: middle; }
.scout-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.scout-chip {
  display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-xs);
  padding: 2px 6px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--win-inactive); color: var(--ink-dim); white-space: nowrap;
}
.scout-actions { display: flex; gap: 6px; white-space: nowrap; }
.scout-form-row td { padding: 8px 10px; background: var(--win-inactive); border-top: 1px dashed var(--line); }
.scout-form { display: flex; flex-direction: column; gap: 6px; }
/* .scout-form-row names two different things on purpose: the OUTER <tr> that
   holds the inline Scouted form (table row), and each INNER line inside that
   form (one per moon). Same visual family, different elements. */
.scout-form .scout-form-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: var(--fs-sm); }
.scout-form .label { color: var(--ink-faint); font-size: var(--fs-xs); }
.scout-form .btn-group { display: inline-flex; gap: 2px; }
.scout-form .btn-group button.active { background: var(--lume); color: var(--void); border-color: transparent; }

/* Group-by-system subtotal row (section 9). */
.moons-group-head td {
  background: var(--win-inactive); font-size: var(--fs-sm); color: var(--ink-dim);
  padding: 6px 10px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
}
.moons-group-head b { color: var(--ink); }

/* ---- Intel tab (FACET-C2-INTEL.md) -------------------------------------
   Dense tables, no card chrome, matching the review-log critique of the
   Moons tab's earlier card layouts. .tbl-intel-hostiles reuses .tbl-mini's
   compact padding; expand rows reuse .moon-row's twist/detail pattern. */
.tbl-intel-hostiles th, .tbl-intel-hostiles td,
.tbl-intel-capwatch th, .tbl-intel-capwatch td,
.tbl-intel-feed th, .tbl-intel-feed td { padding: 5px 10px; font-size: var(--fs-sm); }
.intel-hostile-row.open { background: var(--win-inactive); }
.intel-hostile-detail td { background: var(--win-inactive); padding: 10px 14px; border-top: 1px dashed var(--line); }
.intel-hostile-detail .intel-detail-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px;
}
.intel-hostile-detail h4 { margin: 0 0 4px; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-faint); }
.intel-hostile-detail ul { margin: 0; padding: 0; list-style: none; }
.intel-hostile-detail li { padding: 2px 0; font-size: var(--fs-sm); }
#intel-paste-text {
  width: 100%; min-height: 140px; resize: vertical; font-family: var(--mono); font-size: var(--fs-sm);
  background: rgba(20, 31, 43, 0.72); color: var(--ink); border: 1px solid var(--line); padding: 8px 10px;
}
#intel-paste-text::placeholder { color: var(--ink-faint); }
.intel-paste-controls { display: flex; align-items: center; gap: 12px; margin-top: 8px; flex-wrap: wrap; }
#intel-paste-result { margin-top: 12px; }
#intel-paste-result .tbl-mini { width: 100%; }
.intel-in-range { color: var(--hot); font-weight: 600; }

/* ---- Ops tab (cycle 3, FACET-C3-OPS.md) ----------------------------------- */

.ops-hand-entry { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }
.ops-hand-entry summary { cursor: pointer; color: var(--ink-dim); font-size: var(--fs-sm); }
.ops-hand-entry summary:hover { color: var(--ink); }
.ops-form-row { display: flex; flex-wrap: wrap; align-items: end; gap: 10px; margin-top: 10px; }
.ops-form-row label {
  display: flex; flex-direction: column; gap: 4px; font-size: var(--fs-sm); color: var(--ink-dim);
}
.ops-form-row input, .ops-form-row select {
  background: var(--hangar); border: 1px solid var(--line); color: var(--ink);
  padding: 5px 8px; border-radius: var(--radius); font: inherit;
}
.ops-battle-detail td { padding: 10px 12px; background: rgba(20, 31, 43, 0.5); }
.ops-battle-hulls { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-top: 10px; }
.ops-battle-hulls h4 { margin: 0 0 4px; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-faint); }
.ops-battle-row td:first-child { white-space: nowrap; }

@media (max-width: 560px) {
  /* Phone width = lines, not cards (house rule). The hostile table already
     reads fine as a scroll-table at this width because it is dense (9
     columns, short values); rather than fork a second card layout, it
     drops to a horizontally scrollable .table-wrap, same as every other
     dense table in this app under 560px, and the capital-watch/feed tables
     do the same. No new card markup is introduced for this tab. */
  #intel-paste-text { min-height: 110px; }

  #view-moons .moons-all-wrap { display: none; }
  #view-moons .moons-cards { display: flex; }
  .moons-find input { min-width: 0; width: 100%; }
  .moon-form { grid-template-columns: 1fr; min-width: 0; }
  .moon-events { min-width: 0; max-width: none; }
  .detail-grid { gap: 12px; }
  .detail-grid .tbl-mini { min-width: 0; width: 100%; }

  /* Scout list v2, phone width (item 5): the collapsed bar is unchanged;
     when open, each system collapses to one flex line (system, moon count,
     value) with the chips wrapping underneath instead of a scrolling table.
     No card chrome, same "not great" complaint the desktop redesign fixed. */
  .tbl-moons-scout thead { display: none; }
  .tbl-moons-scout, .tbl-moons-scout tbody, .tbl-moons-scout tr, .tbl-moons-scout td { display: block; width: 100%; }
  .tbl-moons-scout tr:not(.scout-form-row) { border-bottom: 1px solid var(--line); padding: 8px 0; }
  .tbl-moons-scout tr:not(.scout-form-row) td:first-child { font-weight: 600; }
  .tbl-moons-scout tr:not(.scout-form-row) td:first-child::after {
    content: attr(data-moon-count) " moons"; display: inline; margin-left: 6px; color: var(--ink-dim); font-weight: 400; font-size: var(--fs-sm);
  }
  .tbl-moons-scout td.num { text-align: left; padding-top: 2px; }
  .tbl-moons-scout td.scout-actions { padding-top: 6px; }

  /* REVIEW-C1-PRODUCT.md item 17: at 424px the Planner's 24-cell attack strip
     used to overflow inside a 323px box. Same block-per-row technique as the
     Scout table above: no horizontal scroll, no cut-off column, because
     there is no wide row left to cut off. (The Timerboard's own phone layout
     is the 4-zone .mtr row below, not this table -> block conversion; it was
     redesigned to two lines per timer instead.) */
  .tbl-moons-planner thead { display: none; }
  .tbl-moons-planner, .tbl-moons-planner tbody, .tbl-moons-planner tr, .tbl-moons-planner td {
    display: block; width: 100%;
  }
  .tbl-moons-planner tr { border-bottom: 1px solid var(--line); padding: 8px 0; }
  .tbl-moons-planner td { padding: 2px 0; }
  .tbl-moons-planner td:first-child { font-weight: 600; }
  .tbl-moons-planner td.num { text-align: left; }
  #view-moons .tbl-moons-planner .table-wrap { overflow-x: visible; }
  #view-moons .tbl-moons-timers .table-wrap { overflow-x: visible; }

  /* item 17: totals used to show all 8 tiles (307px of them) on a pane where
     an FC glancing at their phone needs exactly one thing: is a timer or a
     window open right now. Everything else stays one tap away on desktop.
     UI-REFRESH.md section 5 regrouped these into .tile-group/.tile-row/.tile
     (Holdings/Money/Threats); `data-tile` stays on every `.tile` so this
     rule keeps working unchanged, it just now targets `.tile` instead of a
     bare `div` and hides the group label of a group with nothing left
     visible in it. */
  #view-moons #moons-totals .tile { display: none; }
  #view-moons #moons-totals .tile-group-label { display: none; }
  #view-moons #moons-totals .tile[data-tile="next-timer"],
  #view-moons #moons-totals .tile[data-tile="window-open"] { display: flex; }
}

/* ---------------------------------------------------------------------------
   Cycle 4: sweep planner, composition, hunt, overview (FACET-C4-DEPTH.md).
   Reuses existing tokens and row shapes (.tbl-mini, .ops-form-row, .ops-bar,
   .dash-grid, .win-head/.win-body) rather than inventing new chrome --
   REVIEW-C1-PRODUCT.md's repeated finding was wasted height from card
   chrome and empty sections, so these are lean by construction.
--------------------------------------------------------------------------- */
.moons-sweep-section { margin-bottom: 14px; }
.sweep-picker {
  display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 8px 0 4px;
  max-height: 120px; overflow-y: auto;
}
.sweep-target { font-size: var(--fs-sm); color: var(--ink-dim); white-space: nowrap; }
.sweep-target input { margin-right: 4px; }
.moons-sweep-saved, .moons-composition-section {
  border: 1px solid var(--line); border-radius: var(--radius);
  margin-top: 10px; background: var(--win);
}
.moons-sweep-saved summary, .moons-composition-section summary {
  padding: 8px 14px; cursor: pointer; font-weight: 600; background: var(--win-head);
}
.moons-composition-section textarea {
  width: 100%; max-width: 520px; margin: 8px 0;
  background: var(--void); color: var(--ink); border: 1px solid var(--line);
  border-radius: var(--radius); font: var(--fs-sm) var(--mono); padding: 8px;
}

/* Hunt tab: same table shape as every other tracker in this app. */
#view-hunt .tbl-mini td.num, #view-hunt .tbl-mini th.num { text-align: right; }

/* Recruit tab (FACET-RECRUIT.md): approach cards reuse .moons-intel-grid /
   .moons-intel-card and the .hist bar-pair chrome already built for the
   Moons Intel histograms; table buckets reuse .table-wrap / .tbl-mini. Only
   what neither already covers lives here: the bucket heading rhythm and the
   per-row status/notes/save controls. */
.recruit-bucket { margin-bottom: 18px; }
.recruit-bucket h3 { margin: 0 0 10px; font-size: var(--fs-md); }
.recruit-card .ops-form-row { margin-top: 10px; }
.recruit-status {
  background: var(--hangar); border: 1px solid var(--line); color: var(--ink);
  padding: 5px 8px; border-radius: var(--radius); font: inherit;
}
.recruit-notes {
  flex: 1 1 220px; min-width: 160px; max-width: 420px; min-height: 32px;
  resize: vertical; background: var(--hangar); border: 1px solid var(--line);
  color: var(--ink); padding: 5px 8px; border-radius: var(--radius); font: inherit;
}
#view-recruit .tbl-mini td { vertical-align: top; }
#view-recruit .tbl-mini .ops-form-row { margin-top: 0; }

@media (max-width: 560px) {
  /* Cards already stack to one column via .moons-intel-grid's auto-fit
     minmax(260px, 1fr); the notes field is the one control wide enough to
     overflow a 375px viewport if left at its desktop max-width. */
  .recruit-notes { max-width: none; flex-basis: 100%; }
}

/* Overview (UI-REFRESH.md section D): the shared .tile-group/.tile system
   above for the headline numbers, the dashboard's own .dash-grid/.win-head
   pattern reused for the panels below (out of scope to invent a second
   "landing screen" layout for one view), and its own .scout-row for Scout,
   top 3 -- see renderOverviewScout() in app.js for why that one is not
   .d-row (the 3-column grid built for the Threat feed's fixed columns, which
   is what let a longer scout reason overlap the moon count). */
#view-overview .win-body table { margin: 0; }
#view-overview .empty { padding: 12px 14px; }

.scout-row {
  padding: 8px 14px;
  border-bottom: 1px solid rgba(90, 130, 160, 0.09);
  font-size: var(--fs-xs);
  display: flex; flex-direction: column; gap: 2px;
}
.scout-row:last-child { border-bottom: 0; }
.scout-row.clickable { cursor: pointer; }
.scout-row.clickable:hover { background: rgba(255, 255, 255, 0.035); }
.scout-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  font-family: var(--mono); font-variant-numeric: tabular-nums;
}
.scout-system { color: var(--ink); font-weight: 600; }
.scout-count { color: var(--ink-faint); }
.scout-reason { color: var(--muted); }

@media (max-width: 560px) {
  .sweep-picker { max-height: 90px; }
}

/* Totals container replacing .ops-bar for Moons (UI-REFRESH.md section 5):
   just spacing and a border to read as one strip, same border-bottom the old
   .ops-bar carried. .tile-group/.tile-row/.tile do the rest. */
.moons-totals-groups {
  padding: 13px 18px 9px; border-bottom: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.015);
}

/* Moons: ONE vertical scroll, the page. Andy 2026-09-24: "scrolling is
   challenging on moons, because doing so in the middle of the page, wants to
   scroll each mini section." Every .table-wrap here was a capped 52vh scroll
   box (and the sweep picker a 120px one), so a wheel or thumb over any of them
   scrolled the box instead of the page. Sections now grow to their content
   (long lists already have "show all"); wide tables keep horizontal scroll
   only, which does not capture vertical wheel or touch. */
#view-moons .table-wrap,
#view-moons .table-wrap.capped,
#view-moons .table-wrap.capped.pane-primary {
  min-height: 0;
  max-height: none;
  overflow-y: visible;
  overflow-x: auto;
  overscroll-behavior-x: contain;
}
#view-moons .table-wrap.capped thead th { position: static; }
#view-moons .sweep-picker { max-height: none; overflow: visible; }

/* Owner source label (Discord / ESI / set by hand), quieter than the side chip. */
.tag.src-tag { color: var(--muted); background: transparent; border: 1px solid var(--line, rgba(255,255,255,0.12)); font-weight: 500; margin-left: 4px; }
/* Recruit: long corp names wrap as a line, ticker and sec chip follow inline
   instead of squeezing the name into a one-word column at 375px. */
.recruit-card h3 { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; }
#recruit-source { white-space: normal; overflow-wrap: anywhere; }

/* Overview Last battle: the battle line is a plain <p>, which had no inset
   (only .empty carried padding), so it sat against the panel edge. */
.ov-battle-body > p { margin: 0; padding: var(--sp-3) var(--sp-4); }

/* UI-REFRESH.md final pass item 2: Intel's Hostile alliances table clipped
   Caps seen at 1024px (measured: 239px of horizontal overflow) though it fit
   at 1280. Top pilot is the lowest-value column at this width -- the same
   figure is already in the expandable detail row under each alliance, and a
   title tooltip on Alliance (intelHostileRowHtml in app.js) now carries it
   too -- so it is what folds away here rather than scrolling the table
   sideways. */
@media (min-width: 901px) and (max-width: 1280px) {
  .tbl-intel-hostiles th:nth-child(7), .tbl-intel-hostiles td:nth-child(7) { display: none; }
  /* Hiding Top pilot alone left this auto-layout table 159px wider than its
     container (measured at 1024px): nine columns of real text, no explicit
     widths to shrink. Active window folds away too (title on Top doctrine
     hull, intelHostileRowHtml() in app.js) to actually close the gap. */
  .tbl-intel-hostiles th:nth-child(5), .tbl-intel-hostiles td:nth-child(5) { display: none; }
}

/* =========================================================================
   FACETS: COMMAND + WAR -- end marker.

   UI-REFRESH.md phase 2 split: this builder (A) owns the shared token/
   component block at the top of this file (see "SHARED COMPONENTS" further
   up) plus every Command (Overview, Dashboard) and War (Moons, Intel, Ops,
   Hunt) rule, which -- because this file predates the phase 2 split and was
   never reorganised for it (out of scope: "never reformat or reorder code
   you are not changing") -- live interleaved with Market/Corp rules earlier
   in the file rather than in one contiguous block. The FACET-*.md-labelled
   sections from FACET-MOONS.md onward, and everything above tied to
   Overview/Dashboard/#view-moons/#view-intel/#view-ops/#view-hunt/
   #view-recruit selectors, are this builder's. This comment plus the marker
   below exist so builder B's own region is unambiguous, per the phase 2
   split's instruction that B writes CSS only inside it.
   ========================================================================= */

/* ==================================================================   FACETS: MARKET + SETTINGS

   Builder B's region (UI-REFRESH.md phase 2 split): Contracts, Buy, Sell,
   Blueprints, Hangar, Build, and settings.html layout. New rules for those
   views/pages go here, appended below this marker; builder A does not edit
   inside it.
   ========================================================================= */
=======
   FACETS: MARKET + SETTINGS (UI-REFRESH.md phase 2, builder B)

   Contracts, Buy, Sell, Blueprints, Hangar, Build, settings.html. Reuses the
   shared token/component block at the top of this file (.tile-group/.tile-
   row/.tile, .chip, .info, .table-scroll, .empty states) as-is; everything
   below is scoped to this facet's own class names or ids, never a bare
   rewrite of a shared rule. The one exception is the .tbl-flagged/.tbl-market
   phone media query further up this file, which already belonged to these
   two views and was re-numbered in place (not moved here) because CSS media
   queries interleave with the base rules they override; duplicating it here
   would just create a second, conflicting copy.
   ========================================================================= */

/* EVE security-status banding on a sec value's NUMBER (UI-REFRESH.md section
   4: 1.0-0.5 blue to green, 0.4-0.1 orange, 0.0 and below red). Colours only
   the number secTag() wraps in app.js; the surrounding .tag keeps answering
   "how far", this answers "how safe" -- a second axis on the same chip
   rather than a fight over which one owns the colour. */
.sec-num.sec-hi { color: var(--sec-hi); }
.sec-num.sec-mid { color: var(--sec-mid); }
.sec-num.sec-low { color: var(--sec-low); }
.sec-num.sec-null { color: var(--sec-null); }

/* A secondary sort control living inside a merged header (Margin+Listed on
   Contracts, Item+Listed on Buy and Blueprints): the columns those used to
   own are gone (their data rides inline in the cell below instead, see
   contractRow()/marketRow()/bpcRow() in app.js), but the ability to sort by
   how recently something was listed is not. Smaller and dimmer than the
   primary sort button, and on its own line so a `th` does not have to widen
   to fit two labels side by side. */
button.sort.sort-sub {
  display: block; margin-top: 2px;
  min-height: 0; padding: 1px 0; border: 0; background: none;
  font-size: 10px; letter-spacing: 0.06em; color: var(--ink-faint);
}
button.sort.sort-sub:hover, button.sort.sort-sub:focus-visible {
  color: var(--lume); background: none; box-shadow: none;
}
button.sort.sort-sub.on { color: var(--lume); }

/* Totals tile walls (Sell, Blueprints, Hangar, Build): replaces the old flat
   .ops-bar six-tile row with grouped .tile-group/.tile-row/.tile from the
   shared block, laid out as a wrapping row of groups rather than one long
   row of bare numbers. */
.totals-tiles {
  display: flex; flex-wrap: wrap; gap: var(--sp-5) var(--sp-6);
  margin: 0 0 var(--sp-4);
}
.totals-tiles .tile-group { margin: 0; }
.totals-tiles .tile-row { min-width: 220px; }

/* Build's Ready/Gap ISK cells each carry a folded-in sub-line (Stocked,
   Job fee) now that those are no longer their own columns; .tbl-build had no
   em.sub rule of its own, so without this it fell through to the bare `.sub`
   heading rule further up this file (24px top margin, uppercase) instead of
   reading as a quiet second line in a table cell. */
.tbl-build em.sub {
  display: block; font-style: normal; font-size: var(--fs-xs);
  color: var(--ink-faint); letter-spacing: 0.02em; margin: 0;
}

/* Blueprints' Copy cell now carries Where and Listed as a second sub-line
   under the runs/ME/TE spec (see bpcRow() in app.js); .tbl-bpc em.sub already
   sets display:block, so two consecutive .sub lines stack without extra CSS,
   this only widens the Copy column slightly so a station name plus the spec
   line does not wrap awkwardly. */
.tbl-bpc td:nth-child(2) { min-width: 190px; }

/* Buy's Item cell now carries the age chip (.age, defined near the flagged
   contract table further up) inline after the name; a small left margin
   matches the age chip's own spacing rule so it does not sit flush against
   the item name. */
.tbl-market .row-text .age { margin-left: 6px; }

/* These count lines grow long sentences at runtime (Buy: "71 deals of 927
   local orders, 26 liquidation batches not shown · 1 hidden"; Sell adds a
   capital-exclusion and an off-station note on top of that) inside a `.inline`
   label, and `.inline`'s own white-space: nowrap is right for the short
   "Label [control]" pairs it was built for but pushes a long count sentence
   straight past a 375px viewport -- the exact page-scroll defect UI-REFRESH.md
   names, and the same bug two other views already carried a fix for (see
   `#h-moons-assumptions ~ .inline` and `#hunt-snapshot-age` above). */
#count-market, #count-sell, #count-bpc, #count-hangar, #count-build {
  white-space: normal;
}

/* Settings: the four section headers now carry .info badges (shared
   component) the same way every other view's sections do; no layout change
   needed beyond what .win-head/.info already provide. The capital hulls and
   access-people tables gained title tooltips on their bare (non-sort) column
   headers, same shared pattern, no new CSS required there either. */

/* UI-REFRESH.md final pass item 2: at 1024px (fits fine at 1280) Contracts
   clipped Margin/Listed and "-4%" margin chips got cut, Sell clipped Price,
   Hangar clipped its Rig columns and their headers. Rather than scroll each
   table sideways, the lowest-value column folds away between 901px and
   1280px; each one's value is already available elsewhere (Contracts: the
   Jumps figure already rides in the Where chip's own title, secTag() in
   app.js; Sell: Local ask now titles the Reference cell, sellRow(); Hangar:
   Rigs/Rig tech now title the Rig ISK lost chip, hangarRowHtml()) so nothing
   is lost, only deferred to a hover/tap. Below 901px .table-scroll's own
   inside-container scroll takes over, unchanged. */
@media (min-width: 901px) and (max-width: 1280px) {
  .tbl-flagged th:nth-child(4), .tbl-flagged td:nth-child(4) { display: none; }
  .tbl-sell th:nth-child(6), .tbl-sell td:nth-child(6) { display: none; }
  .tbl-hangar th:nth-child(3), .tbl-hangar td:nth-child(3),
  .tbl-hangar th:nth-child(4), .tbl-hangar td:nth-child(4) { display: none; }
  /* Sell and Hangar are table-layout:fixed with their own min-width (1000px,
     1020px, set above for the full column count) so hiding a column alone
     did nothing here: the table just stretched its remaining columns to
     fill that same min-width and the wrapper still overflowed by exactly
     as much. Dropping the min-width by roughly one column's worth in this
     band is what actually lets it fit. */
  .tbl-sell { min-width: 0; }
  .tbl-hangar { min-width: 0; }
}

/* Blueprints (item 2, "crowded" at 1024px): the Jita low sub-line and the
   Evidence badge sit in adjacent columns with no breathing room between them
   once `.tbl-bpc td, th { white-space: nowrap }` (above) forces every cell to
   its natural width at a narrow viewport. A little horizontal padding on the
   two columns either side of the seam, and a hair of top margin on the
   sub-line so it does not sit flush under the asking price, fixes the
   collision without touching the nowrap rule everything else here depends
   on. */
.tbl-bpc em.sub { margin-top: 2px; }
.tbl-bpc td:nth-child(4), .tbl-bpc td:nth-child(5) { padding-left: 10px; }
