/* ===========================================================================
   Design tokens
   ---------------------------------------------------------------------------
   Every value the UI is allowed to use. If a rule below needs a colour, size,
   radius or spacing that isn't in here, the right move is almost always to
   pick the nearest token rather than to add a new one -- the app used to
   carry 19 distinct font sizes, 11 radii and 63 padding shorthands, which is
   how two panels that were meant to look identical drifted apart.

   The primitives that consume these (buttons, inputs, dialogs, rows, bars)
   live in ui.css; this file is layout + the app's own features.
   ========================================================================= */
:root {
  /* ---- Layout ------------------------------------------------------------
     The three tracks of the app grid: the left dock (layers + save file), the
     map, and the right tool dock. Panels no longer float over the map, so
     these widths are also literally how much room the map has -- see the
     #appShell grid in this file. */
  --dock-left-width: clamp(248px, 20vw, 340px);
  --dock-right-width: 320px;
  /* The tool dock collapsed to just the altitude rail. */
  --rail-width: 64px;
  --appbar-height: 48px;

  /* How much of the window each dock is currently covering. The docks overlay
     the map rather than taking a column of the grid (see the app-shell block
     below for why), so the layout no longer derives these from content and
     they have to be stated. Everything that needs to sit clear of a dock --
     the map's floating overlay layer, Leaflet's own controls -- insets by
     these rather than by guessing. */
  --dock-left-inset: var(--dock-left-width);
  --dock-right-inset: 0px;


  /* One shared feel for anything that moves, and the lifted shadow the
     genuinely floating things (hints, popovers, tooltip) share. Docked
     surfaces get a border instead -- they are attached, not hovering. */
  --panel-ease: cubic-bezier(0.2, 0, 0, 1);
  --float-shadow: 0 4px 18px rgba(0, 0, 0, 0.45);
  --popover-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);

  /* ---- Colour ------------------------------------------------------------
     Slate surfaces, one blue accent for everything interactive, FICSIT orange
     reserved for the brand + the "get a save onto the map" path (logo, load,
     progress), pink as the "something is hidden from view" signal, amber for
     pending save edits + warnings, cyan owned by the network tool. */
  --surface-0: #131519;   /* deepest chrome: app bar, dock footers */
  --surface-1: #1a1d22;   /* docks */
  --surface-2: #21242b;   /* popups: dialogs, dropdowns, tooltip, menus */
  --raised: #23262c;      /* controls: buttons, fields, cards, stat tiles */
  --raised-hover: #2b2f37;
  --inset: #171a20;       /* wells sunk into a surface (icon frames, tracks) */
  --border: #30333b;      /* inside a dock */
  --border-popup: #3a3f4a;/* around/inside a popup */
  --border-sub: #2c2f37;  /* hairline between list rows */
  --border-hover: #4a505c;

  --text: #d9dde4;
  --text-bright: #fff;
  --text-dim: #c3c9d4;    /* secondary values inside a row */
  --muted: #8a92a3;       /* labels, captions */
  --faint: #6a7180;       /* placeholder, disabled glyphs, section kickers */

  --accent: #5ba3e0;
  --accent-soft: #8ab4f8;
  --accent-bright: #aecbfa;
  --accent-bg: #2a3545;
  --accent-bg-hover: #33435a;
  --accent-border: #3a5070;
  /* Solid selection fill -- the keyboard-highlighted suggestion row and the
     hovered context-menu item. Deliberately louder than --accent-bg: those
     two are "the thing you are about to commit to", not a tint. */
  --select-bg: #2f6bd8;

  --brand: #f2913d;       /* FICSIT orange */
  --brand-bright: #ffa552;
  --brand-bg: #3d2d1c;
  --brand-border: #8a5e2e;

  --hidden-pink: #ff3b81;
  --hidden-pink-soft: #ff9ec2;
  --hidden-pink-bg: #3a2330;
  --hidden-pink-bg-hover: #4a2a3c;
  --hidden-pink-border: #6b2b47;

  --warn: #ffb020;        /* bottleneck / mixed-mark warnings */
  --warn-soft: #ffd48a;
  --warn-text: #ffd7ae;   /* pending save edits */
  --ok: #58a565;
  --ok-soft: #7fd18b;
  --danger: #ff5f56;      /* destructive actions (Delete) */
  --danger-soft: #ff9a94;
  --danger-bg: #3a2222;
  --danger-border: #7a3a36;

  --tool-cyan: #25e0ff;   /* the network tool's own signal colour */
  --tool-cyan-bg: rgba(37, 224, 255, 0.14);
  --tool-cyan-border: rgba(37, 224, 255, 0.45);

  --vehicle: #f39c12;     /* matches filters.js's VEHICLE_COLOR */

  /* ---- Type --------------------------------------------------------------
     Eight steps. --fs-md is the body default. */
  --fs-2xs: 10px;  /* small-caps kickers, meta lines */
  --fs-xs: 11px;   /* captions, section labels, counts */
  --fs-sm: 12px;   /* dense controls, buttons, hints */
  --fs-md: 13px;   /* body */
  --fs-lg: 14px;   /* list rows, nav labels */
  --fs-xl: 15px;   /* search field, emphasis */
  --fs-2xl: 18px;  /* dialog titles */
  --fs-3xl: 21px;  /* headline numbers */
  --font: "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif;
  --font-mono: "Consolas", "SF Mono", monospace;

  /* ---- Spacing (4px base) ------------------------------------------------- */
  --sp-05: 2px;
  --sp-1: 4px;
  --sp-15: 6px;
  --sp-2: 8px;
  --sp-25: 10px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;

  /* ---- Radius ------------------------------------------------------------- */
  --r-xs: 4px;     /* swatches, bar tracks, tiny chips */
  --r-sm: 6px;     /* inputs, list rows, compact buttons */
  --r-md: 8px;     /* buttons, wells, cards */
  --r-lg: 12px;    /* dialogs, popovers, docked panel corners */
  --r-pill: 999px;
}

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  font-family: var(--font);
  font-size: var(--fs-md);
  background: #000;
}

/* ===========================================================================
   App shell
   ---------------------------------------------------------------------------
   An app bar, and the map filling everything below it. The docks are pinned to
   the window's edges ON TOP of the map:

       +--------------------------------------------------+
       |                    app bar                       |
       +------------+------------------------+------------+
       | left dock  |          map           | tool dock  |
       | (layers)   |  (full width, beneath) | (+ rail)   |
       +------------+------------------------+------------+

   They still read as attached -- flush to the edge, square, full height, one
   border -- but they are out of flow, which is the important part: THE MAP'S
   BOX NEVER CHANGES when a dock opens or closes.

   That is a deliberate reversal. The docks used to be real grid columns, so
   the map was literally the space between them. It looked right and it cost
   too much: every dock toggle resized the map, and Leaflet's invalidateSize
   preserves the map's CENTRE, so the world slid sideways by half the width
   delta (~141px for the layers dock, ~161px for a tool dock). Two attempts at
   compensating for that were written and reverted -- see
   docs/dock-map-anchoring.md. Not resizing the map at all removes the problem
   rather than correcting for it: nothing to compensate, nothing to repaint,
   nothing to get wrong.

   What it costs: the map extends underneath the docks, so a sliver of world is
   hidden behind them. Panning still reaches it, and the map keeps its full
   width when both docks are closed.
   ========================================================================= */
body {
  display: grid;
  /* minmax(0, 1fr), not a bare 1fr: a bare `1fr` is minmax(AUTO, 1fr), so the
     row floors at its content's min-content size and tall content pushes the
     grid past the viewport instead of scrolling inside it. */
  grid-template-rows: var(--appbar-height) minmax(0, 1fr);
  background: var(--surface-0);
  color: var(--text);
}

/* ---- App bar -------------------------------------------------------------
   A real bar across the window. The two side sections grow equally from a
   zero basis, which is what centres the search field regardless of what
   either side weighs; max-content floors stop the pills being squashed, and
   the search field gives way first on a narrow window. */
#topBar {
  grid-row: 1;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 0 var(--sp-3);
  box-sizing: border-box;
  background: var(--surface-0);
  border-bottom: 1px solid var(--border);
  /* Above both docks so the search suggestions drop over them. */
  z-index: 10;
}

#topBarLeft, #topBarActions {
  flex: 1 1 0;
  min-width: max-content;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

#topBarActions {
  justify-content: flex-end;
}

#brandCluster {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-1);
}

#mainSearchWrap {
  flex: 0 1 560px;
  min-width: 0;
  display: flex;
  justify-content: center;
}

/* ---- Left dock: layers ---------------------------------------------------- */
#sidebar {
  position: fixed;
  top: var(--appbar-height);
  left: 0;
  bottom: 0;
  width: var(--dock-left-width);
  z-index: 20;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--surface-1);
  border-right: 1px solid var(--border);
}

body.dock-hidden {
  --dock-left-inset: 0px;
}

body.dock-hidden #sidebar {
  display: none;
}

/* The two panes sit side by side in a double-width strip; showing the detail
   pane slides the strip by half its width. Nothing resizes, so this is the
   one layout animation the shell can afford. */
#dockPager {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  width: 200%;
  transform: translateX(0);
  transition: transform 0.22s var(--panel-ease);
}

body.category-open #dockPager {
  transform: translateX(-50%);
}

@media (prefers-reduced-motion: reduce) {
  #dockPager { transition: none; }
}

.dockPane {
  width: 50%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Header of the detail pane: the way back, the category's colour, its name. */
#detailHeader {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-15) var(--sp-2);
  border-bottom: 1px solid var(--border);
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--text-bright);
}

#detailSwatch {
  width: 12px;
  height: 12px;
  border-radius: var(--r-xs);
}

#categoryNavHeader {
  flex: none;
  display: flex;
  gap: var(--sp-2);
  padding: var(--sp-25);
  border-bottom: 1px solid var(--border);
}

#categoryNavColumn {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: var(--sp-2) var(--sp-15);
  box-sizing: border-box;
}

#categoryDetailPane {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: var(--sp-15) var(--sp-2);
  box-sizing: border-box;
}

/* Save-file controls, pinned below the pager: dock chrome, not the content of
   either pane, so they stay put while the panes slide. */
#sidebarFooter {
  flex: none;
  max-height: 46%;
  overflow-y: auto;
  padding: var(--sp-3);
  box-sizing: border-box;
  border-top: 1px solid var(--border);
  background: var(--surface-0);
}

/* Invisible grab strip on the dock's outer edge, lighting up as a thin accent
   stripe on hover/drag (see panels.js). */
.dockResizeHandle {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: 7px;
  cursor: col-resize;
  touch-action: none;
  z-index: 5;
}

.dockResizeHandle::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: 2px;
  background: transparent;
  transition: background-color 0.12s ease;
}

.dockResizeHandle:hover::before,
.dockResizeHandle.dragging::before {
  background: var(--accent);
}

body.dockResizing {
  cursor: col-resize;
  user-select: none;
}

/* ---- Map ------------------------------------------------------------------ */
/* Fills the whole area below the app bar, dock or no dock. Its box is a
   function of the WINDOW only, which is the entire point of this layout. */
#map {
  grid-row: 2;
  min-width: 0;
  background: #000;
  /* Contain Leaflet's internal stacking -- its panes/zoom controls use
     z-index values up to ~1000, which would otherwise live in the root
     stacking context and paint over the app bar's suggestions dropdown. */
  isolation: isolate;
}

/* Pointer-transparent layer sharing the map's grid cell. Everything that
   still floats lives in here, so "centred over the map" is left:50% of this
   element and nothing has to know about dock widths. Each child re-enables
   its own pointer events. */
/* The map fills the window, but the floating hints and bars should centre on
   the part of it you can actually SEE, so this layer -- unlike #map -- insets
   by whatever the docks are covering. It is the only thing that needs to know
   the dock widths, and it costs the map nothing to tell it. */
/* The layer the still-floating things live in: the hint bars, the selection
   action bar, the active-filter banner.
   
   It spans the FULL window, exactly like #map, and never changes size for any
   reason but a window resize. That is what makes its centre the window's
   centre -- the same thing the app bar centres its search field on -- so a
   hint always lines up with the search field above it, whatever the docks are
   doing. Two earlier versions insetted it by the docks so hints would centre
   on the *visible* map: that put them 109px out of line with the search field,
   and, once a dock made the layer narrower than a wide bar, the auto margins
   collapsed and the bar drifted further still.
   
   Trade-off: a bar wider than the gap between the docks runs underneath one.
   The docks paint above this layer (z-index 20 vs 15), so it is clipped rather
   than overlapping, and at any normal window width there is room to spare.
   
   Children are centred by `justify-self`, NOT by `left`/`right`/margins. That
   is not a style preference: an absolutely positioned child that is
   display:none while this layer's width changes can come back with a stale
   USED value for a percentage or resolved `left` -- measured as declared
   `left: 0px` but used `left: 800px`, which put the editor toolbar a quarter
   of the window off-centre until something forced it to reflow. Grid alignment
   never computes a `left` at all, so there is nothing to go stale. Children
   still place themselves vertically with top/bottom. */
#mapOverlays {
  position: fixed;
  inset: var(--appbar-height) 0 0 0;
  display: grid;
  pointer-events: none;
  z-index: 15;
}

#mapOverlays > * {
  justify-self: center;
  pointer-events: auto;
}

/* ---- Right dock: tools ---------------------------------------------------- */
#toolDock {
  position: fixed;
  top: var(--appbar-height);
  right: 0;
  bottom: 0;
  z-index: 20;
  display: flex;
  flex-direction: row;
  background: var(--surface-1);
}

/* Its width is content-driven (rail, tool panel, both or neither) -- these
   mirror it for everything that has to sit clear of it. */
body.has-rail {
  --dock-right-inset: var(--rail-width);
}

body.tool-open {
  --dock-right-inset: var(--dock-right-width);
}

body.tool-open.has-rail {
  --dock-right-inset: calc(var(--dock-right-width) + var(--rail-width));
}

/* Holds whichever tool panel is open (panels.js moves them in). Collapsed to
   nothing when none is -- which is what makes the `auto` grid track vanish. */
#toolPanels {
  display: none;
  width: var(--dock-right-width);
  min-width: 0;
  border-left: 1px solid var(--border);
}

body.tool-open #toolPanels {
  display: flex;
  flex-direction: column;
}

/* A tool panel is now a dock pane: it fills the dock, and drops the card
   treatment (own border radius, shadow, absolute position) it needed while it
   floated over the map. */
#toolPanels > * {
  position: static;
  width: 100%;
  max-height: none;
  box-sizing: border-box;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: var(--sp-3);
  background: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  color: var(--text);
  font-size: var(--fs-md);
}

/* Dark, slim scrollbars everywhere -- the browser's default light-gray bars
   were the single most visibly "un-themed" element left on Windows. */
* {
  scrollbar-width: thin;
  scrollbar-color: #3d424c transparent;
}

::-webkit-scrollbar {
  width: 9px;
  height: 9px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: #3d424c;
  border-radius: var(--r-pill);
  border: 2px solid transparent;
  background-clip: content-box;
}

::-webkit-scrollbar-thumb:hover {
  background-color: #4d535f;
}

::selection {
  background: rgba(91, 163, 224, 0.35);
}

/* One consistent hover/press feel for every button in the app, instead of
   each one transitioning (or snapping) on its own. */
button {
  font-family: inherit;
  transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}

button:focus-visible, select:focus-visible, input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.logoKicker {
  font-size: var(--fs-2xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--faint);
  line-height: 1;
}

.logoName {
  font-size: var(--fs-lg);
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--brand);
  line-height: 1.1;
  white-space: nowrap;
}

#searchBox {
  position: relative;
  width: 100%;
  max-width: 560px;
  display: flex;
  align-items: center;
}

#searchIcon {
  position: absolute;
  left: 16px;
  color: var(--muted);
  pointer-events: none;
}

#mainSearchInput {
  width: 100%;
  box-sizing: border-box;
  height: 42px;
  font-family: var(--font);
  font-size: var(--fs-xl);
  padding: 0 18px 0 44px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border-popup);
  background: var(--surface-2);
  color: var(--text);
  box-shadow: var(--float-shadow);
  transition: border-color 0.12s ease, background-color 0.12s ease, box-shadow 0.12s ease;
}

#mainSearchInput:hover {
  border-color: var(--border-hover);
}

#mainSearchInput:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--raised);
  box-shadow: var(--float-shadow), 0 0 0 3px rgba(91, 163, 224, 0.18);
}

#mainSearchInput::placeholder {
  color: var(--muted);
}

/* Custom suggestions dropdown -- anchored to #searchBox (position:relative),
   so it always matches the search field's width and sits just below it. */
#searchSuggestions {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  max-height: min(70vh, 440px);
  overflow-y: auto;
  background: var(--surface-2);
  border: 1px solid var(--border-popup);
  border-radius: var(--r-lg);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
  padding: 6px;
  z-index: 20;
}

/* Vehicle glyphs (icons/vehicles/*.png) are the game's white-on-transparent
   monochrome icons -- backed by the same solid vehicle-orange circle their
   map pins use (filters.js's VEHICLE_COLOR) so they hold up on any row state
   and read as vehicles next to the full-color item/building icons. */
.searchSuggestionVehicleIcon {
  background: var(--vehicle);
  border-radius: 50%;
  padding: 4px;
  box-sizing: border-box;
}

/* Show/hide eye toggle -- on building suggestion rows AND in the building
   modal's header (#buildingModalVisibilityToggle). Flips the exact same
   sidebar checkbox this building's row owns (see filters.js's
   buildingSearchEntries), so it stays in perfect sync with the sidebar no
   matter which one is clicked. stopPropagation'd on suggestion rows so it
   never also triggers the row's click-to-open-modal behavior.
   A real filled chip in both states (not a bare gray glyph): accent blue
   while shown, and the app's "hidden from view" pink while hidden, so the
   state is legible at a glance. */
.visibilityToggle {
  flex: none;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--hidden-pink-border);
  border-radius: var(--r-md);
  background: var(--hidden-pink-bg);
  color: var(--hidden-pink-soft);
  cursor: pointer;
}

.visibilityToggle:hover {
  border-color: var(--hidden-pink);
  color: var(--text-bright);
}

.visibilityToggle.isShown {
  background: var(--accent-bg);
  border-color: var(--accent-border);
  color: var(--accent-soft);
}

.visibilityToggle.isShown:hover {
  background: var(--accent-bg-hover);
  border-color: var(--accent);
  color: var(--text-bright);
}

.visibilityToggle svg {
  display: block;
}

/* The modal header's eye sits next to a 44px icon -- one size up. */
#buildingModalVisibilityToggle {
  width: 34px;
  height: 34px;
}

.searchSuggestionEmpty {
  padding: 10px 12px;
  color: var(--muted);
  font-style: italic;
  font-size: var(--fs-md);
}

/* The per-save status buttons (Dimensional Depot + the progression views --
   see index.html's #statusMenuWrap): one labeled dark button, same family
   as the search pill, opening a popover of labeled rows -- six bare icon
   tiles were unreadable no matter what tile shade sat behind the mostly-
   dark game icons; a text label next to each icon is what actually fixes
   it. panels.js drives open/close. */
#statusMenuWrap {
  flex: none;
  position: relative;
}

/* Shared pill for the top-right actions (Depot, Progression): same family
   as the search pill/menu cluster. */
.topPillButton {
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 42px;
  padding: 0 13px;
  color: var(--text);
  font: inherit;
  font-size: var(--fs-md);
  font-weight: 600;
  background: var(--surface-2);
  border: 1px solid var(--border-popup);
  border-radius: var(--r-lg);
  box-shadow: var(--float-shadow);
  cursor: pointer;
}

.topPillButton:hover {
  background: var(--raised-hover);
  border-color: var(--border-hover);
  color: var(--text-bright);
}

.topPillButton img {
  flex: none;
  width: 24px;
  height: 24px;
  object-fit: contain;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
}

/* Desktop auto-update pill: accent-colored so a waiting update is
   noticeable without nagging (no modal, no badge count). */
#updatePill {
  color: var(--accent-soft);
  background: var(--accent-bg);
  border-color: var(--accent-border);
}

#updatePill:hover {
  background: var(--accent-bg-hover);
  color: var(--text-bright);
}

#updatePill:disabled {
  opacity: 0.7;
  cursor: default;
}

#statusMenuButton .statusMenuChevron {
  color: var(--muted);
  transition: transform 0.15s ease;
}

#statusMenuButton.open .statusMenuChevron {
  transform: rotate(180deg);
}

#statusMenu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 210px;
  padding: 6px;
  background: var(--surface-2);
  border: 1px solid var(--border-popup);
  border-radius: var(--r-lg);
  box-shadow: var(--float-shadow);
  z-index: 70;
}

.statusMenuRow {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 7px 9px;
  color: var(--text);
  font: inherit;
  font-size: var(--fs-md);
  text-align: left;
  background: none;
  border: none;
  border-radius: var(--r-sm);
  cursor: pointer;
}

.statusMenuRow:hover {
  background: var(--raised-hover);
  color: var(--text-bright);
}

.statusMenuRow img {
  flex: none;
  width: 24px;
  height: 24px;
  object-fit: contain;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
}

.leaflet-container {
  background: #000;
}

/* Leaflet's stock CSS puts mix-blend-mode: plus-lighter on tile images (a
   Chromium tile-fade flicker workaround) -- ADDITIVE compositing. Our base
   tiles overlap on purpose (1px shared-content bleed, see MapTileLayer in
   map.js): under plus-lighter the overlap pixels sum to a bright grid, and
   the per-tile render surfaces it forces can show hairline seams on some
   GPU/driver combos. Source-over + identical-content overlap is seamless. */
.leaflet-container img.leaflet-tile {
  mix-blend-mode: normal;
}

/* Leaflet's default zoom control ships as white boxes with black glyphs --
   the one stock-Leaflet element still visible in the chrome. Re-skinned to
   match the app's raised-control treatment. */
.leaflet-touch .leaflet-bar,
.leaflet-bar {
  border: 1px solid var(--border);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}

.leaflet-bar a,
.leaflet-touch .leaflet-bar a {
  background: var(--raised);
  color: var(--text);
  border-bottom-color: var(--border);
}

.leaflet-bar a:hover,
.leaflet-touch .leaflet-bar a:hover {
  background: var(--raised-hover);
  color: var(--text-bright);
}

.leaflet-bar a.leaflet-disabled,
.leaflet-touch .leaflet-bar a.leaflet-disabled {
  background: var(--surface-1);
  color: var(--faint);
}

/* Branding/license line (map.js adds it as a Leaflet attribution control
   BEFORE the zoom control, so it sits below the +/- buttons, flush in the
   bottom-right corner against the altitude rail). Leaflet's default is a
   white strip; restyle to a quiet dark pill so it reads as part of the
   chrome. The doubled .leaflet-container selector outweighs Leaflet's own
   background rule. */
.leaflet-container .leaflet-control-attribution {
  background: rgba(19, 21, 25, 0.72);
  color: var(--faint);
  font-family: var(--font);
  font-size: var(--fs-2xs);
  padding: 2px 9px;
  margin: 0;
  border-radius: var(--r-sm) 0 0 0;
  white-space: nowrap;
}

.leaflet-container .leaflet-control-attribution a {
  color: var(--muted);
  text-decoration: none;
}

.leaflet-container .leaflet-control-attribution a:hover {
  color: var(--text);
  text-decoration: underline;
}

@media (prefers-reduced-motion: reduce) {
  #sidebar, #categoryDetailColumn { transition: none; }
}

/* One row per top-level category in the nav column -- reuses .groupTitle's
   existing flex/toggle/icon/label layout (see filters.js's
   renderTopLevelCategory, which relocates a real .groupTitle element here
   rather than rebuilding an equivalent one from scratch) with a few
   nav-specific overrides layered on top. Laid out as a self-contained card
   ([icon] label ............ [switch]) -- the colored icon and name sit on
   the left, the visibility switch is pushed to the far right (see the
   margin-left:auto below), so the row's full width is actually used
   instead of everything crowding the left edge. */
.categoryNavRow {
  display: flex;
  align-items: center;
  cursor: pointer;
  user-select: none;
  padding: 11px 14px;
  margin-bottom: 6px;
  border-radius: var(--r-md);
  background: var(--raised);
  border: 1px solid transparent;
  font-size: var(--fs-lg);
  text-transform: none;
  letter-spacing: normal;
  gap: 11px;
  transition: background-color 0.1s ease, border-color 0.1s ease;
}

.categoryNavRow .groupLabel {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.3;
  flex: 1 1 auto;
  min-width: 0;
}

.categoryNavRow:hover {
  background: var(--raised-hover);
  border-color: var(--border);
}

/* Disclosure chevron: the visual cue that a nav row opens the subcategory
   detail column (it reads as a plain toggle row otherwise). Sits after the
   switch at the far right (order chosen against .toggleSwitch's order:2),
   brightens on hover, and points back left when open ("click to close"). */
.categoryNavRow .navChevron {
  order: 3;
  flex: none;
  margin-left: 8px;
  color: var(--faint);
  font-size: var(--fs-xl);
  line-height: 1;
  transition: transform 0.15s ease, color 0.15s ease;
}

.categoryNavRow:hover .navChevron {
  color: var(--text);
  transform: translateX(2px);
}

.categoryNavRow.active .navChevron {
  color: var(--accent-soft);
  transform: rotate(180deg);
}

.categoryNavRow.active {
  background: var(--accent-bg);
  color: var(--accent-soft);
  box-shadow: inset 3px 0 0 var(--accent);
}

/* Bigger than the default .toggleSwitch (used everywhere else -- the
   detail pane's own rows, subcategories, etc.) -- these are the single
   biggest interactive control on this panel, so they get to be easy
   targets, not an afterthought next to the label. Pushed to the far right
   edge of the row (margin-left:auto) after the flex-growing label. */
.categoryNavRow .toggleSwitch {
  width: 40px;
  height: 22px;
  /* The switch is the first child in DOM order (see renderGroup), so
     order:2 moves it visually after the flex-growing label, and the label's
     flex-grow then pushes it to the row's far-right edge. */
  order: 2;
  flex: none;
}

.categoryNavRow .toggleSlider::before {
  width: 16px;
  height: 16px;
  top: 3px;
  left: 3px;
}

.categoryNavRow .toggleSwitch input:checked + .toggleSlider::before {
  transform: translateX(18px);
}

/* A slightly larger, rounded-square color chip reads more like a category
   badge than the tiny dot the detail-pane rows use. */
.categoryNavRow .icon {
  width: 16px;
  height: 16px;
  border-radius: var(--r-xs);
}

.categoryNavRow .icon-circle {
  border-radius: 50%;
}

.categoryDetailGroup {
  display: none;
}

.categoryDetailGroup.active {
  display: block;
}

#loadPanelHeader {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 7px;
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* Right edge of the Save File header: the "Unload" ejector (only while a
   save is loaded) and the "Desktop app" download link (browser only). */
#loadPanelHeaderActions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 10px;
}

/* "Desktop app" download link -- same quiet small-caps voice as the header
   itself, accent on hover. */
#desktopAppLink {
  display: flex;
  align-items: center;
  gap: 4px;
  color: var(--faint);
  text-decoration: none;
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: 0.05em;
}

#desktopAppLink:hover {
  color: var(--accent-soft);
}

/* "Unload" (data.js's clearSave) -- same quiet voice as the link beside it;
   pink on hover, the established "something leaves the view" signal, since
   it clears the whole map. Revealed by data.js while a save is loaded. */
#clearSaveBtn {
  align-items: center;
  gap: 4px;
  padding: 0;
  color: var(--faint);
  background: none;
  border: none;
  cursor: pointer;
  font: inherit;
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

#clearSaveBtn:hover {
  color: var(--hidden-pink-soft);
}

.loadPanelIcon {
  flex: none;
  color: var(--brand);
}

/* The load target (see data.js's loadLocalFile) -- dashed outline so it
   reads as a drop zone rather than yet another button, amber accent on
   hover/drag. */
#uploadDropZone {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  margin-top: 8px;
  padding: 9px 8px;
  font-size: var(--fs-sm);
  color: var(--muted);
  border: 1px dashed var(--border-hover);
  border-radius: var(--r-sm);
  cursor: pointer;
}

#uploadDropZone:hover, #uploadDropZone.drag-over {
  color: var(--warn-text);
  border-color: var(--brand-border);
  background: var(--brand-bg);
}

#uploadDropZone.uploading {
  opacity: 0.6;
  pointer-events: none;
}

/* Desktop-only "pull the newest save off a dedicated server" disclosure
   (data.js shows it inside the Tauri shell) -- same quiet toggle voice as
   #saveDetailsToggle below, form fields in the panel's input style. */
#serverFetchToggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  margin-top: 8px;
  padding: 6px 2px 0;
  background: none;
  border: none;
  border-top: 1px solid var(--border-sub);
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-sm);
  cursor: pointer;
}

#serverFetchToggle:hover {
  color: var(--text);
}

#serverFetchToggle.open .saveDetailsChevron {
  transform: rotate(180deg);
}

.serverFetchTitle {
  display: flex;
  align-items: center;
  gap: 6px;
}

#serverFetchForm {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 7px;
}

#serverFetchRemember {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-xs);
  color: var(--muted);
  cursor: pointer;
  user-select: none;
}

#serverFetchRemember input {
  width: auto;
  margin: 0;
  accent-color: var(--brand);
  cursor: pointer;
}

#loadStatus {
  margin-top: 8px;
  color: var(--muted);
  font-size: var(--fs-xs);
  min-height: 1.2em;
}

/* The "save details" disclosure row: object-count summary chip on the left,
   chevron on the right; clicking expands #saveDetails (game settings + the
   save-export button) -- see filters.js. Replaces the old always-expanded
   stack that made the footer eat half the sidebar. */
#saveDetailsToggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  margin-top: 8px;
  padding: 6px 2px 0;
  background: none;
  border: none;
  border-top: 1px solid var(--border-sub);
  color: var(--muted);
  font: inherit;
  cursor: pointer;
}

#saveDetailsToggle:hover {
  color: var(--text);
}

.saveDetailsChevron {
  flex: none;
  transition: transform 0.15s ease;
}

#saveDetailsToggle.open .saveDetailsChevron {
  transform: rotate(180deg);
}

#saveDetails {
  padding-top: 2px;
}

/* The one number this whole panel exists to report -- given its own small
   stat-chip treatment (bold accent value + muted label, same pairing as
   .buildingStatValue/.buildingStatLabel in the building modal) instead of a
   single flat gray line, so it actually reads as a result rather than a
   caption. */
#totalObjectCount {
  min-height: 1.2em;
}

.totalObjectCountValue {
  color: var(--accent-soft);
  font-weight: 700;
  font-family: var(--font-mono);
  font-size: var(--fs-md);
}

.totalObjectCountLabel {
  color: var(--faint);
  font-size: var(--fs-xs);
}

/* Game-mode settings (Power Cost Multiplier / Node Purity / Node
   Randomization) -- see data.js's showGameSettings(). Hidden via inline
   display:none until a save with at least one of these set is loaded. */
#gameSettingsPanel {
  margin-top: 6px;
  font-size: var(--fs-sm);
}

.gameSettingRow {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 1px 0;
}

.gameSettingLabel {
  color: var(--muted);
}

.gameSettingValue {
  color: var(--text);
  font-weight: 500;
}

/* The Depot row has no position to fly to, so it gets no button -- this
   holds the column open so its label/count still line up with every other
   row's. */
.itemLocationLocateSpacer {
  flex: none;
  width: 24px;
}

.itemLocationChildren {
  background: rgba(0, 0, 0, 0.18);
  border-bottom: 1px solid var(--border-sub);
}

/* Child rows show only position + count (the type is the header's job);
   the extra left padding indents them under the header's label column. */
.itemLocationChildRow {
  padding-left: 48px;
  font-size: var(--fs-md);
}

@media (prefers-reduced-motion: reduce) {
  #itemModal, #selectionModal { animation: none; transform: none; opacity: 1; }
}

@keyframes buildingModalIn {
  to { transform: scale(1); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  #buildingModal { animation: none; transform: none; opacity: 1; }
}

/* Vehicle-orange circle behind the white monochrome vehicle glyphs -- same
   reasoning as .searchSuggestionVehicleIcon above. */
#buildingModalIcon.vehicleModalIcon {
  background: var(--vehicle);
  border-radius: 50%;
  padding: 7px;
}

/* Background/text color set inline per-category (see finditem.js's
   Filters.buildingCategoryColor) -- ties this chip back to the exact color
   the same building uses in the sidebar. */
#buildingModalCategory {
  display: inline-flex;
  align-self: flex-start;
  padding: 2px 9px;
  border-radius: var(--r-pill);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
}

#buildingModalRecipes {
  margin-bottom: 14px;
}

.recipeBarRow {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 3px 0;
}

.recipeBarLabel {
  flex: 0 0 42%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
  font-size: var(--fs-sm);
}

.recipeBarCount {
  flex: none;
  width: 36px;
  text-align: right;
  color: var(--accent-soft);
  font-size: var(--fs-sm);
  font-weight: 600;
}

/* ---- Right-click context menu -- see contextmenu.js. -------------------- */

#contextMenu {
  position: fixed;
  z-index: 1700; /* Above modals (1500) and the tooltip (1000) -- the most immediate, transient UI on screen. */
  min-width: 190px;
  max-width: 280px;
  background: var(--surface-2);
  border: 1px solid var(--border-popup);
  border-radius: var(--r-md);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
  padding: 6px;
  font-size: var(--fs-sm);
  color: var(--text);
}

.contextMenuItem {
  padding: 8px 10px;
  border-radius: var(--r-sm);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.contextMenuItem:hover {
  background: var(--select-bg);
  color: var(--text-bright);
}

/* Non-actionable hint rows (e.g. "Paste here" with an empty clipboard). */
.contextMenuItemDisabled,
.contextMenuItemDisabled:hover {
  color: var(--faint);
  background: none;
  cursor: default;
}

.contextMenuDivider {
  height: 1px;
  margin: 4px 2px;
  background: #333;
}

/* ---- Rectangle selection (right-drag) -- see selection.js. -------------- */

/* The drag rectangle itself: a fixed-position box tracking the pointer while
   the right button is held. */
#selectionRect {
  position: fixed;
  z-index: 1400;
  border: 1.5px solid var(--accent);
  background: rgba(91, 163, 224, 0.15);
  pointer-events: none;
}

/* Floating panel shown after a selection is made -- object count plus the
   "List objects" / "Total inventory" actions. Bottom-centre of the map so it
   never sits under the top bar or the just-drawn rectangle. */
#selectionPanel {
  position: absolute;
  bottom: 22px;
  z-index: 600;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 10px 10px 16px;
  background: var(--surface-2);
  border: 1px solid var(--border-popup);
  border-radius: var(--r-lg);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.55);
  color: var(--text);
  font-size: var(--fs-md);
}

#selectionCount {
  font-weight: 600;
  white-space: nowrap;
}

#selectionPanelButtons {
  display: flex;
  gap: 8px;
  align-items: center;
}

/* ---- Save editor (editor.js): pending-edit toolbar, ghost placement,
   offset dialog. Same surface treatment as the selection panel. */
#editorToolbar {
  position: absolute;
  top: 58px;
  z-index: 600;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border-popup);
  border-radius: var(--r-lg);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.55);
  color: var(--text);
  font-size: var(--fs-sm);
}

#editorEditCount {
  font-weight: 600;
  color: var(--warn-text);
  white-space: nowrap;
}

/* The placement ghost: selection bbox outline following the cursor. Inside
   the map pane so latLngToContainerPoint coordinates apply directly. */
#editorHint {
  position: absolute;
  bottom: 64px;
  z-index: 1401;
  padding: 7px 14px;
  background: var(--surface-2);
  border: 1px solid var(--brand-border);
  border-radius: var(--r-md);
  color: var(--warn-text);
  font-size: var(--fs-sm);
  pointer-events: none;
  white-space: nowrap;
}

#busySpinner {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 3px solid var(--accent-bg);
  border-top-color: var(--accent);
  animation: busySpin 0.8s linear infinite;
}

@keyframes busySpin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  #busySpinner { animation-duration: 2s; }
}

#busyLabel {
  font-size: var(--fs-lg);
  font-weight: 600;
  text-align: center;
}

#busyPhase {
  font-size: var(--fs-sm);
  color: var(--muted);
  min-height: 15px;
  text-align: center;
}

#pastePanelTitle {
  font-weight: 700;
  margin-bottom: 12px;
}

#pastePosModes {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 10px;
  color: var(--muted);
}

#pastePosModes label {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
}

.pastePanelDivider {
  border-top: 1px solid var(--border-popup);
  margin: 10px 0;
}

#pasteResult {
  margin-top: 10px;
  color: var(--accent-soft);
  font-size: var(--fs-sm);
  min-height: 15px;
}

#pastePanelButtons {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 10px;
}

#pastePanelApplyBtn {
  font-weight: 600;
}

/* Download button turns amber once edits are pending. */
#downloadSaveBtn.edited {
  color: var(--warn-text);
  border-color: var(--brand-border);
}

/* Floating "a find-item filter is active" banner -- shown over the map (the
   modal is dismissed so the highlighted results are actually visible, see
   finditem.js) once "Show only these on map" is clicked. Lets the user
   navigate the filtered map and revert ("Show all") or reopen the list
   ("Details") without the modal covering everything. Centered near the top
   of the map, above it (isolation:isolate on #map keeps its Leaflet z-index
   contained, so a plain z-index here sits cleanly on top). */
#activeFilterBanner {
  position: absolute;
  top: 68px; /* Just below the floating search pill. */
  z-index: 500;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: 90vw;
  padding: 8px 8px 8px 14px;
  background: var(--surface-2);
  border: 1px solid var(--hidden-pink);
  border-radius: var(--r-pill);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);
  color: var(--text);
  font-size: var(--fs-md);
}

#activeFilterLabel {
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#activeFilterClear {
  border-color: var(--hidden-pink);
  background: var(--hidden-pink-bg);
  color: var(--hidden-pink-soft);
}

#activeFilterClear:hover {
  background: var(--hidden-pink-bg-hover);
  color: var(--text-bright);
}

.altitudeTitle {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}

.altitudeTitleIcon {
  color: var(--accent);
}

.altitudeValue {
  font-size: var(--fs-xs);
  color: var(--muted);
  margin: 2px 0;
  flex: none;
}

/* A single visual track (.altitudeTrackBg + .altitudeTrackFill) with two
   real <input type="range"> elements stacked exactly on top of it -- rather
   than two side-by-side sliders -- so it reads as one ruler with two handles
   instead of two independent controls that happen to be neighbors. */
.altitudeTrackWrap {
  position: relative;
  flex: 1 1 auto;
  width: 100%;
  min-height: 0;
}

/* Inset by half the thumb box (altitude.js's THUMB_PX / 2) so the track's
   ends sit where the thumb centers actually stop, matching the fill. */
.altitudeTrackBg {
  position: absolute;
  top: 11.5px;
  bottom: 11.5px;
  left: 50%;
  transform: translateX(-50%);
  width: 4px;
  background: var(--border);
  border-radius: var(--r-xs);
  pointer-events: none;
}

/* top/height are set inline by altitude.js to span exactly the currently
   selected min..max range. Draggable (see altitude.js's pointerdown handler
   on this element) to shift the whole [min, max] window at once, keeping
   its span fixed, instead of only being able to move one handle at a time.
   The actual colored bar stays a slim 4px (background-clip: content-box
   confines the fill color to the width box), but the invisible padding on
   either side widens the *hit target* to something actually grabbable --
   a bare 4px-wide strip would be nearly impossible to grab precisely. */
.altitudeTrackFill {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: 4px;
  padding: 0 8px;
  background-color: var(--accent);
  background-clip: content-box;
  border-radius: var(--r-xs);
  pointer-events: auto;
  cursor: grab;
  touch-action: none;
}

/* altitude.js toggles .dragging for the duration of a fill-bar drag (pointer
   capture keeps events flowing to the element, so the class -- not :hover
   state -- is the reliable signal that a grab is in progress). */
.altitudeTrackFill.dragging {
  cursor: grabbing;
}

/* writing-mode is the standards-track way to get a vertical range input --
   unlike -webkit-appearance:slider-vertical, width/height keep their normal
   meaning (no axis swap from rotation), so height:100% reliably fills all
   available vertical space. direction:rtl flips the default min-at-top
   orientation so the top of the track is the maximum, matching the usual
   "up = higher altitude" expectation.
   Both inputs are stacked exactly on top of each other (position:absolute,
   inset 0) rather than placed side by side, with their own native track
   hidden (transparent) since .altitudeTrackBg/.altitudeTrackFill draw the
   one shared visual track underneath. pointer-events:none on the control
   itself -- re-enabled only on the thumb pseudo-elements below -- is what
   lets a click reach whichever handle is actually under the cursor instead
   of the topmost of the two stacked <input>s swallowing every click across
   its entire (invisible) track. */
.altitudeRangeInput {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  writing-mode: vertical-lr;
  direction: rtl;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  pointer-events: none;
}

.altitudeRangeInput::-webkit-slider-runnable-track {
  background: transparent;
}

.altitudeRangeInput::-moz-range-track {
  background: transparent;
  border: none;
}

/* The visible handle stays a 20x11 pill, but a thick transparent border
   (excluded from the paint via background-clip: padding-box) inflates the
   pointer target to 32x23 -- an 11px-tall handle on its own is a fiddly
   thing to hit. The old 1px solid #555 outline can't coexist with the
   transparent hit-area border, so an inset box-shadow (drawn at the padding
   edge, i.e. around the visible pill) redraws it. */
.altitudeRangeInput::-webkit-slider-thumb {
  -webkit-appearance: none;
  pointer-events: auto;
  box-sizing: border-box;
  width: 32px;
  height: 23px;
  border: 6px solid transparent;
  border-radius: var(--r-md);
  background: #ddd;
  background-clip: padding-box;
  box-shadow: inset 0 0 0 1px #555;
  cursor: ns-resize;
}

.altitudeRangeInput::-moz-range-thumb {
  pointer-events: auto;
  box-sizing: border-box;
  width: 32px;
  height: 23px;
  border: 6px solid transparent;
  border-radius: var(--r-md);
  background: #ddd;
  background-clip: padding-box;
  box-shadow: inset 0 0 0 1px #555;
  cursor: ns-resize;
}

/* Previously a bare, unstyled native <button> (rendered with the browser's
   default light-gray chrome, the one visibly "un-themed" control against
   this app's otherwise all-dark UI) squeezed into a 64px column as a text
   label. A compact icon button reads as a deliberate control at this width
   and matches the icon-button treatment used elsewhere (#depotIconButton). */
#altitudeResetButton {
  flex: none;
  width: 28px;
  height: 28px;
  margin-top: 8px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  background: var(--raised);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  cursor: pointer;
}

#altitudeResetButton:hover {
  color: var(--text-bright);
  border-color: var(--faint);
}

#altitudeResetButton svg {
  display: block;
}

.filterGroup {
  margin-bottom: 2px;
}

/* Not scoped to `.filterGroup >`: renderTopLevelCategory RELOCATES a group's
   title row out of its .filterGroup and into the dock's category list, and a
   child combinator would stop matching the moment it did -- which is exactly
   why .categoryNavRow below used to re-force every property with !important. */
.groupTitle {
  font-weight: bold;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 4px 3px;
  border-radius: var(--r-xs);
  user-select: none;
}

.categoryNavRow .toggleSwitch input:indeterminate + .toggleSlider::before {
  transform: translateX(9px);
}

#categoryDetailPane .toggleSwitch input:indeterminate + .toggleSlider::before {
  transform: translateX(7.5px);
}

.expandToggle {
  display: inline-block;
  width: 10px;
  color: var(--muted);
  cursor: pointer;
  flex: none;
}

.groupLabel {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.icon {
  display: inline-block;
  width: 10px;
  height: 10px;
  flex: none;
}

.icon-circle {
  border-radius: 50%;
}

.icon-rect {
  border-radius: var(--r-xs);
}

.icon-line {
  height: 0;
  width: 14px;
  margin-top: 5px;
}

/* Thin left guide line marks nesting depth at a glance (resource type ->
   mined/unmined -> purity, subcategory -> merged building rows, etc.)
   without costing extra horizontal indent on top of the existing margin. */
.filterChildren {
  margin-left: 14px;
  padding-left: 7px;
  border-left: 1px solid var(--border);
}

.filterRow {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 3px;
  border-radius: var(--r-xs);
}

.filterRow:hover, .groupTitle:hover {
  background: rgba(255, 255, 255, 0.05);
}

.filterRow label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: default;
}

.filterRow .count {
  color: var(--muted);
  margin-left: auto;
  padding-left: 4px;
  flex: none;
}

/* ---- Detail-pane rows: same clean feel as the nav column's category
   cards, at a smaller scale -- roomier rows and the visibility switch
   pushed to the far-right edge (instead of crammed on the left next to
   the label), so the pane reads as a proper list that actually uses its
   width. Scoped to #categoryDetailPane so the nav column's own
   .categoryNavRow overrides and the modal/tooltip toggles are untouched. */
#categoryDetailPane .groupTitle {
  padding: 7px 9px;
  border-radius: var(--r-sm);
  font-size: var(--fs-md);
  gap: 9px;
}

#categoryDetailPane .filterRow {
  padding: 6px 9px;
  border-radius: var(--r-sm);
  gap: 9px;
  font-size: var(--fs-md);
}

/* The label grows to fill so whatever comes after it (the switch, and for
   leaf rows the count) is pushed to the right edge; order:5 on the switch
   moves it past the icon/label/count despite being first in DOM order (see
   renderGroup's append order). NB: a leaf row's text label is a bare
   <label>, but the toggle switch is ALSO a <label> (class toggleSwitch, see
   makeToggle) -- so :not(.toggleSwitch) is essential here, otherwise the
   flex-grow would land on the switch and stretch it into a wide pill. */
#categoryDetailPane .groupTitle .groupLabel,
#categoryDetailPane .filterRow > label:not(.toggleSwitch) {
  flex: 1 1 auto;
  min-width: 0;
}

#categoryDetailPane .toggleSwitch {
  order: 5;
  flex: none;
  width: 34px;
  height: 19px;
}

#categoryDetailPane .toggleSlider::before {
  width: 13px;
  height: 13px;
  top: 3px;
  left: 3px;
}

#categoryDetailPane .toggleSwitch input:checked + .toggleSlider::before {
  transform: translateX(15px);
}

/* Count sits just before the switch at the right edge (order:4, one less
   than the switch) rather than tight against the label -- with the label
   flex-growing above, both end up right-aligned as a "1069  [switch]" pair. */
#categoryDetailPane .filterRow .count {
  order: 4;
  margin-left: 0;
}

/* :not(.icon-line) -- line icons (belts/power lines) are a zero-height
   element with just a top border, so forcing a height would break them. */
#categoryDetailPane .icon:not(.icon-line) {
  width: 13px;
  height: 13px;
}

/* The detail pane's top-level content IS a .filterChildren (the childrenDiv
   renderGroup built, relocated here by renderTopLevelCategory) -- but at the
   top level its nesting-guide border-left/indent is just a stray vertical
   line down the left edge of the panel. Strip it here; genuinely nested
   .filterChildren (subcategories, resource mined/unmined, etc.) keep theirs. */
#categoryDetailPane > .categoryDetailGroup > .filterChildren {
  margin-left: 0;
  padding-left: 0;
  border-left: none;
}

/* Hover tooltip -- a plain fixed-position div that follows the cursor (see
   tooltip.js); not a Leaflet popup, since popups are anchored/click-oriented. */
/* The tooltip is a top-layer popover (see tooltip.js's ensureElement): it has
   to be able to paint above a modal <dialog>, and no z-index can do that.
   The overrides below undo the UA's [popover] defaults -- it centres popovers
   with inset:0 + margin:auto, and tooltip.js positions this one itself. */
#tt-tooltip {
  position: fixed;
  inset: auto;
  margin: 0;
  padding: var(--sp-3) var(--sp-3);
  width: max-content;
  min-width: 260px;
  max-width: 380px;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border-popup);
  border-radius: var(--r-md);
  box-shadow: var(--popover-shadow);
  pointer-events: none;
  font-family: var(--font);
  overflow: visible;
}

.tt-popup {
  font-size: var(--fs-md);
  line-height: 1.5;
  max-height: 70vh;
  overflow-y: auto;
}

.tt-title {
  font-weight: bold;
  font-size: var(--fs-xl);
  color: var(--text-bright);
  margin-bottom: 8px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border-popup);
}

/* The find-item "quantity in here" callout (see tooltip.js's
   highlightSection) -- the one number the search was about, so it's louder
   than every other row: the highlight bucket's pink, tinted background,
   larger text. */
.tt-highlight {
  margin-top: 8px;
}

.tt-highlight-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  background: rgba(255, 59, 129, 0.13);
  border: 1px solid rgba(255, 59, 129, 0.4);
  border-radius: var(--r-sm);
  padding: 5px 9px;
  margin-top: 4px;
  font-size: var(--fs-md);
}

.tt-highlight-row .tt-row-label {
  color: var(--hidden-pink-soft);
  font-weight: 600;
}

.tt-highlight-row .tt-row-value {
  color: var(--text-bright);
  font-weight: 700;
  font-size: var(--fs-lg);
}

.tt-row {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 2px 0;
}

.tt-row-label {
  color: var(--muted);
}

.tt-row-value {
  text-align: right;
  overflow-wrap: anywhere;
  color: var(--text);
  font-weight: 500;
}

/* Wraps a row's label alongside a copy icon (see tooltip.js's
   coordinatesRow) -- only the Coordinates row uses this instead of plain
   .tt-row-label. Keeping the icon on the label side (not the value side)
   is what keeps the value column aligned with the Altitude row above it. */
.tt-row-label-with-icon {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--muted);
}

.tt-copy-icon-btn {
  display: inline-flex;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  color: var(--accent-soft);
  cursor: pointer;
  line-height: 0;
  /* Tooltips are pointer-events:none until pinned (see #tt-tooltip) -- this
     button is only ever actually clickable once that's true, same as
     "All properties" below, so it doesn't need its own pointer-events rule. */
}

.tt-copy-icon-btn:hover {
  color: var(--accent-bright);
}

.tt-copy-icon-btn-done {
  color: var(--ok-soft);
}

.tt-section {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--border-sub);
}

.tt-section-title {
  color: var(--accent-soft);
  font-weight: bold;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 4px;
}

/* Mixed-mark conveyor line callout (see tooltip.js's bottleneckSection) --
   a genuine "your factory is silently underperforming" warning, so amber
   and tinted louder than a regular section, matching bottleneck.js's
   marker color. The .tt-section border-top is overridden by the box's own
   full border. */
.tt-warning {
  background: rgba(255, 176, 32, 0.10);
  border: 1px solid rgba(255, 176, 32, 0.45);
  border-radius: var(--r-sm);
  padding: 7px 8px 8px;
}

.tt-warning .tt-section-title {
  color: var(--warn);
}

.tt-warning-text {
  color: var(--warn-soft);
  font-size: var(--fs-sm);
  line-height: 1.4;
}

.tt-warning-btn {
  display: block;
  margin-top: 7px;
  padding: 4px 9px;
  background: rgba(255, 176, 32, 0.16);
  border: 1px solid rgba(255, 176, 32, 0.55);
  border-radius: var(--r-xs);
  color: #ffcf7d;
  font-size: var(--fs-sm);
  cursor: pointer;
  /* Inherits the tooltip's own pointer-events (none until pinned) -- the
     button is only ever actually clickable once pinned, same as the copy
     icon and "All properties". */
}

.tt-warning-btn:hover {
  background: rgba(255, 176, 32, 0.28);
  color: #ffe0a3;
}

.tt-loading, .tt-error {
  color: var(--muted);
  font-style: italic;
}

.tt-error {
  color: var(--danger-soft);
}

.tt-raw {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--border-sub);
}

.tt-raw summary {
  cursor: pointer;
  color: var(--accent-soft);
  font-size: var(--fs-sm);
  font-weight: 500;
}

.tt-raw summary:hover {
  color: var(--accent-bright);
}

.tt-raw-list {
  margin-top: 6px;
  max-height: 280px;
  overflow-y: auto;
  padding-right: 2px;
}

.tt-raw-row {
  padding: 5px 0;
  border-bottom: 1px solid var(--border-sub);
}

.tt-raw-row:last-child {
  border-bottom: none;
}

.tt-raw-row-label {
  color: var(--accent-soft);
  font-size: var(--fs-xs);
  font-weight: 600;
  margin-bottom: 2px;
}

.tt-raw-row-value {
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1.4;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

@media (prefers-reduced-motion: reduce) {
  #progressionModal { animation: none; transform: none; opacity: 1; }
}

.progressionSection {
  margin-bottom: 12px;
  /* Some views hold 500+ rows across their sections; skip layout/paint for
     the offscreen ones or scrolling the modal visibly stutters. The
     intrinsic-size fallback only matters before a section's real size has
     been measured once. */
  content-visibility: auto;
  contain-intrinsic-size: auto 320px;
}

.progressionSectionHeader {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 5px;
}

.progressionSectionTitle {
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--text);
}

/* Small "n / total" tally on a section header; turns green when complete. */
.progressionSectionCount {
  font-size: var(--fs-sm);
  color: var(--muted);
  flex: none;
  margin-left: auto;
}

.progressionSectionCount.complete {
  color: var(--ok-soft);
}

/* e.g. the MAM's "not discovered yet" tag on a research tree the save has
   never unlocked (tree header still listed so the gap is visible). */
.progressionSectionTag {
  font-size: var(--fs-xs);
  color: #b48a5a;
  border: 1px solid #4a3b28;
  background: rgba(180, 138, 90, 0.12);
  border-radius: var(--r-xs);
  padding: 1px 6px;
}

.progressionRowMain {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

/* Entries the save hasn't unlocked/purchased yet -- kept in the list (seeing
   what's missing is the point of a progression view) but visibly dimmed.
   Only the icon + name dim: the right-hand cost/status text and the cost
   expander are exactly what a locked row exists to communicate, so they
   stay at full opacity and readable. */
.progressionRow.locked .progressionRowIcon,
.progressionRow.locked .progressionRowLabel {
  opacity: 0.55;
}

.progressionRowIcon {
  width: 20px;
  height: 20px;
  flex: none;
  object-fit: contain;
}

.progressionRowLabel {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Right-hand side of a row: a green check for done entries, or a short
   dimmed note (research/coupon cost, "imported x / y") for pending ones.
   Multi-item costs never render here (they get the expander below); the
   ellipsis is just a guard so one long single-item label can't overflow. */
.progressionRowStatus {
  flex: none;
  max-width: 45%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-md);
  color: var(--text-dim);
}

.progressionCostList {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin: 6px 0 2px 30px; /* left-aligns with the row label (icon width + gap) */
}

.progressionCostLine {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: var(--fs-md);
  color: var(--text-dim);
}

.progressionCostIcon {
  width: 18px;
  height: 18px;
  flex: none;
  object-fit: contain;
}

.progressionCostAmount {
  flex: none;
  color: var(--accent-soft);
  font-weight: 600;
}

.progressionRowStatus.done {
  color: var(--ok-soft);
  font-size: var(--fs-md);
  font-weight: 600;
}

.progressionRowStatus.countValue {
  color: var(--accent-soft);
  font-weight: 500;
  font-size: var(--fs-md);
}

/* Space Elevator view: headline phase status + per-part delivery rows with
   their own mini progress bars. */
.progressionPhaseBanner {
  border: 1px solid var(--border-popup);
  background: var(--inset);
  border-radius: var(--r-md);
  padding: 9px 12px;
  margin-bottom: 10px;
  color: var(--text);
}

.progressionPhaseBanner .phaseTitle {
  font-weight: 600;
  color: var(--text-bright);
  font-size: var(--fs-lg);
}

.progressionPhaseBanner .phaseNote {
  font-size: var(--fs-sm);
  color: var(--muted);
  margin-top: 2px;
}

.progressionPhaseBanner.complete {
  border-color: #3b5a41;
  background: rgba(88, 165, 101, 0.10);
}

/* The Space Elevator part rows stack a delivery bar under the label
   (their main line is a regular .progressionRowMain), so they're taller
   than plain progression rows. */
.sePartRow {
  padding: 7px 8px;
}

#networkPanelHeader {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
  flex: none; /* The title bar stays put; the body below it is what scrolls. */
}

/* The scrolling region. The two lists shrink first (see .networkList's flex),
   and only once they are at their minimum does this scroll -- so on a normal
   window nothing scrolls, and on a short one every control is still
   reachable instead of being clipped off the bottom. */
#networkPanelBody {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  /* Room for the scrollbar when it does appear, so it never sits on top of
     the list borders. */
  padding-right: 2px;
}

/* Only the two lists give way when space runs short. Everything else --
   labels, buttons, the slider, the summary -- is stated as unshrinkable, so
   the browser has exactly one place to take the space from instead of
   distributing a few pixels across a dozen controls. */
#networkPanelBody > * {
  flex: none;
}

/* No min-height override on the point list: it keeps .networkList's floor, so
   it shrinks to a couple of rows and then the body scrolls, rather than
   collapsing to a sliver. #networkResult is a bare wrapper, so it may shrink
   to nothing -- the list inside it has the same floor of its own. */
#networkPointList {
  flex: 0 1 auto;
}

#networkResult {
  flex: 0 1 auto;
  min-height: 0;
}

#networkPanelIcon {
  flex: none;
}

#networkPanelHeadings {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}

#networkPanelTitle {
  font-weight: 700;
  color: var(--text-bright);
}

#networkPanelSubtitle {
  font-size: var(--fs-xs);
  color: var(--muted);
}

.kicker #networkPointCount {
  letter-spacing: 0;
  text-transform: none;
  color: var(--muted);
  font-size: var(--fs-xs);
  margin-left: 6px;
}

#networkModeHint {
  margin: 7px 0 4px;
  font-size: var(--fs-xs);
  line-height: 1.45;
  color: var(--muted);
}

.networkButtonRow {
  display: flex;
  gap: 6px;
  margin-bottom: 6px;
}

/* The map-picking toggle is a mode, not an action -- lit in the tool's own
   cyan for as long as clicks are being captured, matching the hint bar at
   the bottom of the map. */
#networkPickBtn.active {
  border-color: rgba(37, 224, 255, 0.55);
  background: rgba(37, 224, 255, 0.14);
  color: var(--tool-cyan);
  font-weight: 600;
}

#networkComputeBtn {
  font-weight: 600;
}

#networkCoordRow {
  display: flex;
  gap: 6px;
  margin-bottom: 8px;
}

.networkList:empty {
  display: none;
}

.networkRow:hover,
.networkRow.hover {
  background: rgba(37, 224, 255, 0.09);
}

/* Clicking an object that is already a point adds nothing -- this pulse (and
   the enlarged dot on the map, see network.js's flashPoint) is what says the
   click landed on a point that already exists rather than doing nothing. */
.networkRow.flash {
  animation: networkRowFlash 1s var(--panel-ease);
}

@keyframes networkRowFlash {
  0%, 55% { background: rgba(37, 224, 255, 0.32); }
  100% { background: transparent; }
}

.networkRowIndex {
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: rgba(37, 224, 255, 0.16);
  color: var(--tool-cyan);
  font-size: var(--fs-2xs);
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

.networkRowText {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* The point list stops at network.js's LIST_ROW_LIMIT -- this row is what
   says so, instead of the list just appearing to end. */
.networkOverflowRow {
  padding: 7px 10px;
  font-size: var(--fs-xs);
  color: var(--faint);
  font-style: italic;
  text-align: center;
}

#networkResult {
  border-top: 1px solid var(--border);
  padding-top: 10px;
  display: flex;
  flex-direction: column;
  /* Shares the leftover height with the point list above it; min-height: 0
     is what lets its own list actually shrink inside it. */
  flex: 1 1 auto;
  min-height: 0;
}

#networkPanelFooter {
  flex: none;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

#networkPanelFooter .networkButtonRow:last-child {
  margin-bottom: 0;
}

#networkSummary {
  margin-bottom: 9px;
}

.networkSummaryTotal {
  font-size: var(--fs-3xl);
  font-weight: 700;
  color: var(--tool-cyan);
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

.networkSummaryDetail {
  font-size: var(--fs-xs);
  color: var(--muted);
  margin-top: 2px;
}

/* Destination + the length/trip priority slider (network.js). Both are
   optional: with no destination set the tool minimises length and nothing
   else, so the slider is not even rendered -- there is nothing to trade. */
#networkDestinationRow {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 30px;
  padding: 5px 10px;
  margin-bottom: 8px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--inset);
  font-size: var(--fs-sm);
}

#networkDestinationNone {
  color: var(--faint);
  font-style: italic;
}

#networkDestinationName {
  flex: 1;
  min-width: 0;
  align-items: center;
  color: var(--tool-cyan);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#networkDestinationClear {
  flex: none;
  border: none;
  background: none;
  color: var(--faint);
  font-size: var(--fs-xl);
  line-height: 1;
  padding: 2px 4px;
  border-radius: var(--r-sm);
  cursor: pointer;
}

#networkDestinationClear:hover {
  color: var(--text-bright);
  background: var(--raised-hover);
}

.networkRow.isDestination {
  background: rgba(37, 224, 255, 0.07);
  box-shadow: inset 2px 0 0 var(--tool-cyan);
}

.networkRow.isDestination .networkRowDestination {
  opacity: 1;
  color: var(--tool-cyan);
}

#networkPriority {
  margin-bottom: 10px;
}

#networkAlpha {
  width: 100%;
  margin: 2px 0 0;
  accent-color: var(--tool-cyan);
  cursor: pointer;
}

#networkAlphaEnds {
  display: flex;
  justify-content: space-between;
  font-size: var(--fs-2xs);
  color: var(--faint);
}

#networkAlphaNote {
  margin-top: 5px;
  font-size: var(--fs-xs);
  line-height: 1.45;
  color: var(--muted);
}

/* Trip total: the number the destination exists to show, so it is sized
   between the headline network length and the detail lines. */
.networkSummaryTrips {
  margin-top: 7px;
  padding-top: 7px;
  border-top: 1px solid var(--border);
}

.networkTripsTotal {
  font-size: var(--fs-xl);
  font-weight: 700;
  color: var(--text-bright);
  font-variant-numeric: tabular-nums;
}

.networkTripsLabel {
  font-size: var(--fs-sm);
  color: var(--muted);
}

/* Same slot and shape as the editor's placement hint (#editorHint) -- only
   one of the two is ever up, since both own the map cursor. */
#networkHint {
  position: absolute;
  bottom: 22px;
  z-index: 600;
  padding: 8px 16px;
  background: var(--surface-2);
  border: 1px solid rgba(37, 224, 255, 0.45);
  border-radius: var(--r-pill);
  box-shadow: var(--float-shadow);
  color: var(--text);
  font-size: var(--fs-sm);
  white-space: nowrap;
  pointer-events: none;
}

/* Per-link hover tooltip on the map. A Leaflet tooltip, so it positions
   itself against the cursor -- restyled off Leaflet's white default onto the
   same popup surface every other floating panel uses. */
.leaflet-tooltip.networkLinkTooltip {
  background: var(--surface-2);
  border: 1px solid var(--border-popup);
  border-radius: var(--r-md);
  box-shadow: var(--float-shadow);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-sm);
  padding: 7px 10px;
  white-space: nowrap;
}

.leaflet-tooltip.networkLinkTooltip::before {
  display: none; /* No callout arrow: the tooltip tracks the cursor. */
}

.networkTooltipTitle {
  font-weight: 700;
  color: var(--tool-cyan);
  margin-bottom: 3px;
}

.networkTooltipLeg {
  font-size: var(--fs-xs);
  color: var(--muted);
  margin-bottom: 3px;
}

.networkTooltipRow {
  font-size: var(--fs-xs);
  color: var(--text);
}

.networkTooltipRow span {
  display: inline-block;
  width: 34px;
  color: var(--faint);
}

/* ===========================================================================
   Feature adjustments to the ui.css primitives
   ---------------------------------------------------------------------------
   Everything below tweaks a shared primitive for one specific place. The test
   for whether a rule belongs here rather than in ui.css: would every other
   user of that primitive want it too? If yes it goes in ui.css instead.
   ========================================================================= */

/* Search dropdown rows are a picker, not a data table -- no hairlines between
   them, and each row is its own rounded target inside the popover. */
#searchSuggestions .row {
  padding: var(--sp-2) var(--sp-25);
  border-bottom: none;
  border-radius: var(--r-md);
  content-visibility: visible; /* At most ~20 rows; skipping costs more than it saves. */
}

#searchSuggestions .kicker {
  padding: var(--sp-15) var(--sp-25) var(--sp-1);
  margin: 0;
}

/* A group header states "Constructor × 28" as one phrase, so the label stops
   growing and the badge sits right against it; margin-right:auto on the badge
   takes over the gap-filling, keeping the count in the same right-hand column
   as every ungrouped row's. */
.itemLocationGroupHeader .row-label {
  flex: 0 1 auto;
}

.itemLocationGroupBadge {
  margin-right: auto;
}

/* Child rows show position + count only (the type is the header's job). The
   extra left padding indents them under the header's label column. */
.itemLocationChildRow {
  padding-left: 48px;
  font-size: var(--fs-md);
}

.itemLocationChildLabel {
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
}

.itemLocationShowMore {
  width: 100%;
  justify-content: flex-start;
  padding-left: 48px;
  color: var(--accent-soft);
  border: none;
  border-bottom: 1px solid var(--border-sub);
  border-radius: 0;
}

/* "Show on map" sits on every row of a list that can run to hundreds, so it
   stays quiet until its row is hovered rather than competing with the label
   and count that are the row's actual content. */
.itemLocationLocate {
  opacity: 0;
  transition: opacity 0.1s ease;
}

.row:hover .itemLocationLocate,
.itemLocationLocate:focus-visible {
  opacity: 1;
}

/* A progression row stacks its always-visible main line over an optional
   expandable cost list, so the row itself is a column and .progressionRowMain
   is the flex line the icon/label/status sit on. */
.progressionRow {
  flex-direction: column;
  align-items: stretch;
  gap: 0;
  padding: var(--sp-15) var(--sp-2);
}

/* The tool's own cyan, not the generic hover tint -- these rows are points in
   a network drawn in that colour on the map. */
.networkRow:hover,
.networkRow.hover {
  background: rgba(37, 224, 255, 0.09);
}

.networkSegment.active {
  color: var(--tool-cyan);
  background: var(--tool-cyan-bg);
}

#networkPickBtn.active {
  border-color: var(--tool-cyan-border);
  background: var(--tool-cyan-bg);
  color: var(--tool-cyan);
}

#networkCoordRow .field {
  flex: 1;
  min-width: 0;
}

/* The busy dialog is a centred stack, not the usual head/body/foot. */
#busyBox {
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-6);
  text-align: center;
}

/* align-items:center above would otherwise shrink the progress bar to its
   (zero) content width. */
#busyBox .bar {
  align-self: stretch;
}

#loadProgressBar {
  margin-top: var(--sp-2);
}

#categoryNavHeader .btn {
  flex: 1 1 50%;
}

/* ===========================================================================
   Docked-layout adjustments
   ---------------------------------------------------------------------------
   What the panels needed once they stopped floating: the altitude rail became
   a flex child of the tool dock rather than a strip pinned to the window, the
   tool panels became dock panes, and the surviving floating elements are now
   positioned inside #mapOverlays (which IS the map), so their offsets are
   measured from the map rather than from the window.
   ========================================================================= */

/* ---- Altitude rail: the tool dock's permanent right-hand strip ------------
   Hidden until a save is loaded (altitude.js). Because the dock's grid track
   is `auto`, hiding it costs zero width -- which is what removed the dead
   64px black strip that used to sit beside the map on first load. */
#altitudePanel {
  flex: none;
  width: var(--rail-width);
  box-sizing: border-box;
  padding: var(--sp-1);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  overflow: hidden;
  background: var(--surface-1);
  border-left: 1px solid var(--border);
  color: var(--text);
}

/* ---- Tool panels as dock panes ------------------------------------------- */

/* The network finder already split itself into header / scrolling body /
   pinned footer, which is exactly the shape a dock pane wants. */
#networkPanel {
  font-size: var(--fs-md);
}

/* The paste panel is a single short form, so it scrolls as one piece rather
   than growing a pinned footer it does not need. */
#pastePanel {
  overflow-y: auto;
}

#pastePanelTitle {
  font-size: var(--fs-lg);
  font-weight: 700;
  color: var(--text-bright);
  margin-bottom: var(--sp-3);
}

/* ---- Floating overlays, now measured from the map ------------------------- */

/* Just under the app bar rather than 68px down the window. */
#activeFilterBanner {
  top: var(--sp-3);
}

#editorToolbar {
  top: var(--sp-3);
}

/* ---- App-bar controls ----------------------------------------------------
   Everything in the bar is 34px tall so the 48px bar has an even 7px of air
   above and below, and the bar reads as one row rather than a stack of
   differently sized islands. */
#mainSearchInput {
  height: 34px;
  font-size: var(--fs-lg);
  box-shadow: none;
}

.topPillButton {
  height: 34px;
  box-shadow: none;
}

#menuButton {
  width: 34px;
  min-width: 34px;
  height: 34px;
}

/* The one brand moment in the chrome: a two-line FICSIT-orange wordmark
   (small-caps kicker over the name). No longer a card of its own -- it is
   part of the bar. */
#logoButton {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 1px;
  height: 34px;
  padding: 0 var(--sp-2);
  background: none;
  border: none;
  border-left: 3px solid var(--brand);
  border-radius: 0;
  cursor: pointer;
  text-align: left;
}

#logoButton:hover .logoName { color: var(--brand-bright); }
#logoButton:hover .logoKicker { color: var(--muted); }

/* ---- Dock empty state ----------------------------------------------------- */
#dockEmptyState {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-6) var(--sp-4);
  text-align: center;
  color: var(--faint);
}

#dockEmptyState svg {
  color: var(--brand);
  opacity: 0.65;
}

.dockEmptyTitle {
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--muted);
}

.dockEmptyText {
  margin: 0;
  max-width: 26ch;
  font-size: var(--fs-sm);
  line-height: 1.5;
}

.dockEmptyText strong {
  color: var(--brand);
  font-weight: 600;
}

/* While the layers dock is collapsed the hamburger stays quietly accented, so
   it reads as "the way to get the panel back" rather than just a menu icon.
   (panels.js toggles .is-active; this is the rule that got lost when the
   button became a .btn and the class was renamed from .panelHidden.) */
#menuButton.is-active {
  color: var(--accent-soft);
  background: var(--accent-bg);
}

#menuButton.is-active:hover {
  color: var(--text-bright);
  background: var(--accent-bg-hover);
}

/* Brand mark (not a control -- see index.html). */
#logoMark {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1px;
  height: 34px;
  padding: 0 var(--sp-2);
  border-left: 3px solid var(--brand);
  user-select: none;
}

/* ===========================================================================
   Responsive
   ---------------------------------------------------------------------------
   The app had no layout media queries at all, so at 1024px the left dock ate
   a third of the window and at 800px the search field was crushed to nothing.
   Three steps, each removing whatever is costing the map the most room at
   that width.
   ========================================================================= */

/* The dock's remembered width is a preference, not a promise: cap it as a
   share of the window so a 340px dock does not eat half a 900px screen. */
@media (max-width: 1200px) {
  #sidebar {
    width: min(var(--dock-left-width), 32vw);
  }
}

/* Below this the pills lose their labels -- the icons are unambiguous next to
   each other, and the search field is worth more than the words.
   The threshold is set by geometry, not taste: the app bar centres its search
   field by giving both side sections an equal share of the leftover space, and
   that only holds while each side's content FITS its share. With labels the
   right-hand cluster needs ~260px, and it stops fitting at about this width --
   below which the search field would start drifting off-centre. */
@media (max-width: 1150px) {
  #mainSearchWrap {
    flex: 1 1 auto;
  }

  .topPillButton > span:not(#updatePillLabel) {
    display: none;
  }

  .topPillButton {
    padding: 0 var(--sp-2);
  }
}

/* Narrow enough that a dock covering a third of the window leaves the map
   unusable. The docks already overlay at every size, so this only caps how
   much they may cover -- and panels.js starts the left one collapsed here, so
   the map is what you land on. */
@media (max-width: 820px) {
  #sidebar {
    width: min(var(--dock-left-width), 78vw);
    box-shadow: 8px 0 24px rgba(0, 0, 0, 0.45);
  }

  #toolDock {
    box-shadow: -8px 0 24px rgba(0, 0, 0, 0.45);
  }

  #toolPanels {
    width: min(var(--dock-right-width), 78vw);
  }

  /* Dragging a width is a pointer-and-space affordance; neither applies here. */
  .dockResizeHandle {
    display: none;
  }

  /* The brand wordmark is the first thing worth dropping: the favicon and the
     page title already say what this is. */
  #logoMark {
    display: none;
  }
}

@media (max-width: 620px) {
  #mainSearchInput {
    font-size: var(--fs-md);
  }
}

/* Leaflet's controls live inside #map, which runs under the docks -- push them
   clear. Each side insets by its own dock (unlike #mapOverlays above): these
   hug an edge rather than centring on anything, so there is nothing to keep
   symmetric. Untransitioned for the same reason as #mapOverlays. */
.leaflet-right {
  right: var(--dock-right-inset);
}

.leaflet-left {
  left: var(--dock-left-inset);
}
