/* ==========================================================================
   Wave Drop Maps — "Telemetry" design system
   --------------------------------------------------------------------------
   One stylesheet, shared by every page. Aesthetic: cold aerospace instrument.
   Near-black with a blue-green cast, ONE hot aqua signal colour, amber for
   live-hotfix state, coral for alerts.

   Two rules that are not cosmetic and must survive any future edit:

   1. THE CHROME MUST STAY NEUTRAL. The map canvas draws 77 layer colours on
      top of this UI. Every one of them has to read as itself. That is why the
      surface ramp is desaturated and why there is exactly one accent hue —
      a second saturated chrome colour would start competing with the data.

   2. THE MUTED TOKENS ARE CONTRAST-CHECKED. --muted carries group headers,
      counts and status text at 0.68-0.74rem. It is measured against the
      DARKEST surface it can sit on (--raised), not against --base, because
      that is the worst case. Measured 5.19:1 (AA needs 4.5:1). Do not darken
      it without re-measuring; the previous olive theme shipped a 4.11:1 token
      once and it had to be fixed after the fact.
   ========================================================================== */

/* --------------------------------------------------------------- tokens -- */
:root {
  /* Surface ramp — cold, desaturated, four steps of depth. */
  --base:      #070b0e;
  --surface:   #0d1317;
  --raised:    #131c21;
  --raised-2:  #19242a;

  /* Glass: floating HUD elements that sit ON the map. */
  --glass:     rgba(11, 17, 21, 0.82);
  --glass-hi:  rgba(24, 34, 40, 0.86);

  /* Hairlines. --line is visible structure, --line-soft is a whisper. */
  --line:      #1e2c34;
  --line-soft: #162127;

  /* Type. --muted is the AA floor — see rule 2 above. */
  --ink:       #e6f0f2;
  --ink-dim:   #9fb3b9;
  --muted:     #78909a;

  /* The one accent. "Wave" → water → aqua; also reads as radar/telemetry. */
  --signal:      #3befc8;
  --signal-ink:  #03150f;
  --signal-soft: rgba(59, 239, 200, 0.12);
  --signal-line: rgba(59, 239, 200, 0.34);
  --signal-glow: rgba(59, 239, 200, 0.45);

  /* Semantic. Amber = a LIVE value overriding the shipped one. Coral = wrong. */
  --amber:      #ffb020;
  --amber-soft: rgba(255, 176, 32, 0.13);
  --amber-line: rgba(255, 176, 32, 0.32);
  --alert:      #ff5c5c;
  --alert-soft: rgba(255, 92, 92, 0.13);
  --alert-line: rgba(255, 92, 92, 0.32);
  /* Violet marks data that is NOT ours — borrowed reference pins. It
     must never be mistaken for a measurement we made. */
  --borrowed:      #8b9bff;
  --borrowed-soft: rgba(139, 155, 255, 0.13);

  /* Type stacks. Display has personality; body is engineered for small sizes;
     mono carries every number so columns align. Each has a real fallback. */
  --display: "Space Grotesk", "Segoe UI", system-ui, sans-serif;
  --sans:    "IBM Plex Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
  --mono:    "IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;

  /* Radii: small and deliberate. An instrument is not a pill. */
  --r-xs: 3px;
  --r-sm: 5px;
  --r:    8px;
  --r-lg: 14px;

  --shadow:    0 10px 30px -14px rgba(0, 0, 0, 0.8), 0 2px 6px rgba(0, 0, 0, 0.4);
  --shadow-lg: 0 30px 70px -24px rgba(0, 0, 0, 0.9), 0 4px 14px rgba(0, 0, 0, 0.5);

  --rail-w: 60px;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* ------------------------------------------------------------- baseline -- */
* { box-sizing: border-box; }

html, body {
  margin: 0;
  min-height: 100%;
  background: var(--base);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 15px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Layered background: flat black reads as "unstyled". A hairline technical
   grid plus two very faint aqua auras give the page depth without ever
   competing with content. Fixed so it does not scroll away. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  background:
    radial-gradient(1100px 620px at 12% -12%, rgba(59, 239, 200, 0.055), transparent 62%),
    radial-gradient(900px 700px at 104% 8%, rgba(59, 239, 200, 0.028), transparent 60%),
    var(--base);
}
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.5;
  background-image:
    linear-gradient(to right, rgba(120, 144, 154, 0.045) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(120, 144, 154, 0.045) 1px, transparent 1px);
  background-size: 56px 56px;
  /* Fade the grid out toward the bottom so it reads as an origin, not wallpaper. */
  mask-image: linear-gradient(to bottom, #000 0%, #000 34%, transparent 88%);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 34%, transparent 88%);
}

::selection { background: var(--signal-soft); color: var(--ink); }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--line);
  border-radius: 10px;
  border: 2px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: #2b3d47; background-clip: content-box; }
* { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }

/* The chrome is custom, so the cues a keyboard user navigates by are gone
   unless we put them back. :focus-visible only fires for keyboard/AT focus,
   so pointer users never see a ring. */
:focus-visible {
  outline: 2px solid var(--signal);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

a { color: var(--signal); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 2px; }

h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 700;
  letter-spacing: -0.022em;
  margin: 0;
  line-height: 1.12;
}

/* Aggressive hierarchy — the display sizes are far apart on purpose. */
.t-hero  { font-size: clamp(2.4rem, 5.2vw, 4.1rem); letter-spacing: -0.038em; line-height: 0.98; }
.t-h1    { font-size: clamp(1.6rem, 3vw, 2.3rem); }
.t-h2    { font-size: 1.22rem; }
.t-h3    { font-size: 0.98rem; }

/* The section eyebrow. Small, wide-tracked, mono — the label on an instrument. */
.eyebrow {
  font-family: var(--mono);
  font-size: 0.67rem;
  font-weight: 600;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--muted);
}
.eyebrow .tick { color: var(--signal); }

.lede { color: var(--ink-dim); font-size: 1.02rem; line-height: 1.62; }
.sub  { color: var(--muted); font-size: 0.8rem; line-height: 1.5; }

/* Every number on the site is mono + tabular so columns line up and a value
   does not reflow as it ticks. */
.num {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* ---------------------------------------------------------- beta notice -- */
/* A standing accuracy caveat on every page. Amber, because it qualifies the
   data rather than reporting a failure — coral is reserved for "this is
   wrong". It sits above everything including the map, so it is never something
   you have to scroll to find. */
.beta-bar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 120;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.42rem 0.85rem;
  background: linear-gradient(to bottom, rgba(38, 27, 6, 0.97), rgba(30, 22, 6, 0.97));
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--amber-line);
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--ink-dim);
}
.beta-bar.gone { display: none; }
.beta-tag {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--amber);
  border: 1px solid var(--amber-line);
  background: var(--amber-soft);
  border-radius: var(--r-xs);
  padding: 0.1rem 0.4rem;
  white-space: nowrap;
}
.beta-text { flex: 1; min-width: 0; }
.beta-bar a { color: var(--amber); text-decoration: underline; text-underline-offset: 2px; }
.beta-bar a:hover { color: #ffc65a; }
.beta-x {
  flex: 0 0 auto;
  background: none;
  border: 1px solid transparent;
  color: var(--muted);
  cursor: pointer;
  border-radius: var(--r-xs);
  padding: 0.2rem;
  line-height: 0;
}
.beta-x:hover { color: var(--ink); border-color: var(--line); }

/* The bar is fixed, so every full-height layout has to give back the space it
   covers. --beta is 0 until the bar is actually mounted, which keeps the
   dismissed case exact rather than approximately right. */
:root { --beta: 0px; }
body.has-beta { --beta: 34px; }

@media (max-width: 640px) {
  /* Two lines on a phone; the tag stays on its own row so the sentence is not
     squeezed into a 2-word column. */
  .beta-bar { flex-wrap: wrap; padding: 0.4rem 0.7rem; font-size: 0.73rem; }
  .beta-text { flex: 1 1 100%; order: 3; }
  body.has-beta { --beta: 58px; }
}

/* ----------------------------------------------------------- app shell -- */
/* Rail + content. The rail is a fixed icon strip like a professional tool;
   it never scrolls and never changes width, so the app has a spine. */
.app {
  display: grid;
  grid-template-columns: var(--rail-w) 1fr;
  min-height: 100vh;
  padding-top: var(--beta);
}
/* dvh, not vh: on mobile Safari 100vh is taller than the visible viewport, so
   a full-height map page pushes its own bottom controls under the browser
   chrome. The vh line is the fallback for anything without dvh. */
.app.is-map { height: calc(100vh - var(--beta)); overflow: hidden; }
@supports (height: 100dvh) {
  .app.is-map { height: calc(100dvh - var(--beta)); }
}

.rail {
  position: relative;
  z-index: 40;
  background: linear-gradient(to bottom, var(--surface), #0a1014);
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  padding: 0.7rem 0 0.9rem;
}

.rail-mark {
  width: 34px; height: 34px;
  border-radius: 9px;
  display: grid; place-items: center;
  background: linear-gradient(155deg, var(--signal), #17a98a);
  box-shadow: 0 4px 16px -4px var(--signal-glow);
  margin-bottom: 0.7rem;
  flex: 0 0 auto;
}
.rail-mark svg { width: 19px; height: 19px; display: block; }
/* When the real logo is present it replaces the generated tile entirely: no
   gradient, no padding, no shadow tint fighting the artwork. */
.rail-mark.has-logo { background: none; box-shadow: none; overflow: hidden; }
.rail-mark.has-logo img { width: 100%; height: 100%; object-fit: cover; display: block; }

.rail-link {
  position: relative;
  width: 40px; height: 38px;
  display: grid; place-items: center;
  border-radius: var(--r-sm);
  color: var(--muted);
  transition: color 0.16s var(--ease), background 0.16s var(--ease);
  flex: 0 0 auto;
}
.rail-link svg { width: 18px; height: 18px; }
.rail-link:hover { color: var(--ink); background: var(--raised); text-decoration: none; }
.rail-link.current { color: var(--signal); background: var(--signal-soft); }
/* The active marker is a bar on the rail edge, not a border on the button —
   it reads as a physical indicator light. */
.rail-link.current::before {
  content: "";
  position: absolute;
  left: -10px; top: 50%;
  transform: translateY(-50%);
  width: 3px; height: 20px;
  border-radius: 0 3px 3px 0;
  background: var(--signal);
  box-shadow: 0 0 10px var(--signal-glow);
}
/* Rail labels appear on hover as a floating tag, so the rail stays narrow. */
.rail-link::after {
  content: attr(data-label);
  position: absolute;
  left: calc(100% + 10px);
  white-space: nowrap;
  background: var(--glass-hi);
  backdrop-filter: blur(10px);
  border: 1px solid var(--line);
  color: var(--ink);
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.28rem 0.55rem;
  border-radius: var(--r-sm);
  box-shadow: var(--shadow);
  opacity: 0;
  transform: translateX(-4px);
  pointer-events: none;
  transition: opacity 0.14s var(--ease), transform 0.14s var(--ease);
  z-index: 60;
}
.rail-link:hover::after { opacity: 1; transform: translateX(0); }
.rail-spacer { flex: 1 1 auto; }
/* Desktop shows labels on hover via ::after; this inline label is for the
   mobile bottom bar, where hover does not exist and seven unlabelled 18px
   glyphs are not navigation. */
.rail-text { display: none; }

/* -------------------------------------------------------------- topbar -- */
.topbar {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.62rem 1.1rem;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
  min-height: 52px;
}
.topbar-title { display: flex; align-items: baseline; gap: 0.6rem; min-width: 0; }
.topbar-title h1 { font-size: 0.98rem; }
.topbar-title .sub { white-space: nowrap; }
.topbar-actions { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }

/* The patch pill. A live dot that actually pulses — this is the one place a
   loop is justified, because "is this data current?" is the first question
   anyone asks of a spawn map. */
.patch-pill {
  display: inline-flex; align-items: center; gap: 0.42rem;
  font-family: var(--mono);
  font-size: 0.71rem;
  font-weight: 500;
  color: var(--ink-dim);
  background: var(--raised);
  border: 1px solid var(--line);
  padding: 0.24rem 0.6rem;
  border-radius: 999px;
  white-space: nowrap;
}
.live-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--signal);
  box-shadow: 0 0 0 0 var(--signal-glow);
  animation: pulse 2.6s ease-out infinite;
  flex: 0 0 auto;
}
@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 var(--signal-glow); }
  70%  { box-shadow: 0 0 0 6px rgba(59, 239, 200, 0); }
  100% { box-shadow: 0 0 0 0 rgba(59, 239, 200, 0); }
}

/* --------------------------------------------------------- form/buttons -- */
.btn {
  display: inline-flex; align-items: center; gap: 0.42rem;
  font: inherit;
  font-size: 0.79rem;
  font-weight: 600;
  color: var(--ink-dim);
  background: var(--raised);
  border: 1px solid var(--line);
  padding: 0.4rem 0.7rem;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: color 0.15s var(--ease), border-color 0.15s var(--ease), background 0.15s var(--ease);
  white-space: nowrap;
}
.btn:hover { color: var(--ink); border-color: #2b3d47; background: var(--raised-2); }
.btn svg { width: 14px; height: 14px; flex: 0 0 auto; }
.btn.primary {
  color: var(--signal-ink);
  background: var(--signal);
  border-color: var(--signal);
}
.btn.primary:hover { background: #5ff6d6; border-color: #5ff6d6; color: var(--signal-ink); }
.btn.ghost { background: transparent; }
.btn.icon { padding: 0.4rem; }
.btn[aria-pressed="true"] {
  color: var(--signal);
  border-color: var(--signal-line);
  background: var(--signal-soft);
}

kbd {
  font-family: var(--mono);
  font-size: 0.68rem;
  color: var(--muted);
  background: var(--base);
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: var(--r-xs);
  padding: 0.08rem 0.3rem;
  white-space: nowrap;
}

.field {
  position: relative;
  display: flex;
  align-items: center;
}
.field > svg:first-child {
  position: absolute; left: 0.6rem;
  width: 14px; height: 14px;
  color: var(--muted);
  pointer-events: none;
}
.field input {
  width: 100%;
  font: inherit;
  font-size: 0.83rem;
  color: var(--ink);
  background: var(--base);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 0.46rem 0.6rem 0.46rem 2rem;
  outline: none;
  transition: border-color 0.15s var(--ease), background 0.15s var(--ease);
}
.field input::placeholder { color: var(--muted); }
.field input:focus { border-color: var(--signal-line); background: var(--surface); }
.field .field-count {
  position: absolute; right: 0.6rem;
  font-family: var(--mono);
  font-size: 0.68rem;
  color: var(--muted);
  pointer-events: none;
}

select.btn {
  appearance: none;
  padding-right: 1.6rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2378909a' stroke-width='3'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.5rem center;
  background-size: 11px;
}

/* ---------------------------------------------------------------- chips -- */
/* Provenance chips. This is the product's moat made visible: every number on
   the site says where it came from and how sure we are. The variants are
   semantic, never decorative — do not add one without a meaning.
     .exact    a measured position from the game files
     .approx   a zone centroid; the object is somewhere inside, not here
     .live     an EMS hotfix is overriding the shipped value RIGHT NOW
     .borrowed not our data (borrowed reference pins)
     .unknown  the sweep found nothing — which is evidence about the search,
               not proof the thing is unconditional. Never render this as 100%.
*/
.chip {
  display: inline-flex; align-items: center; gap: 0.28rem;
  font-family: var(--mono);
  font-size: 0.65rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--muted);
  background: var(--raised);
  border: 1px solid var(--line);
  padding: 0.1rem 0.36rem;
  border-radius: var(--r-xs);
  white-space: nowrap;
  vertical-align: middle;
}
.chip.exact    { color: var(--signal);   background: var(--signal-soft);   border-color: var(--signal-line); }
.chip.approx   { color: var(--amber);    background: var(--amber-soft);    border-color: var(--amber-line); }
/* `live` is deliberately NOT amber. `approx` (amber) means "we are less sure
   where this is"; `live` means "this is MORE current than the shipped file".
   Rendering both in the same colour taught the reader that amber = doubt, and
   then every hotfix flag was read as a caveat. */
.chip.live     { color: var(--signal);   background: var(--signal-soft);   border-color: var(--signal-line); font-weight: 600; }
.chip.alert    { color: var(--alert);    background: var(--alert-soft);    border-color: var(--alert-line); }
.chip.borrowed { color: var(--borrowed); background: var(--borrowed-soft); border-color: rgba(139,155,255,0.3); }
.chip.unknown  { color: var(--muted);    border-style: dashed; }

/* --------------------------------------------------------------- panels -- */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
}
.panel-head {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.7rem 0.9rem;
  border-bottom: 1px solid var(--line-soft);
}
.panel-head h3 { flex: 1; min-width: 0; }
.panel-body { padding: 0.9rem; }

/* A card that lifts on hover — used for the overview's page links. The lift is
   tiny (2px) because this is an instrument, not a toy. */
.card {
  position: relative;
  display: block;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 1.05rem 1.1rem 1.15rem;
  color: inherit;
  overflow: hidden;
  transition: transform 0.2s var(--ease), border-color 0.2s var(--ease), background 0.2s var(--ease);
}
.card:hover {
  transform: translateY(-2px);
  border-color: var(--signal-line);
  background: var(--raised);
  text-decoration: none;
}
/* A hairline of signal along the top edge, revealed on hover. */
.card::before {
  content: "";
  position: absolute; inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(to right, transparent, var(--signal), transparent);
  opacity: 0;
  transition: opacity 0.2s var(--ease);
}
.card:hover::before { opacity: 1; }
.card h3 { margin-bottom: 0.35rem; }
.card .sub { line-height: 1.55; }
.card-icon {
  width: 30px; height: 30px;
  border-radius: var(--r-sm);
  display: grid; place-items: center;
  background: var(--signal-soft);
  color: var(--signal);
  margin-bottom: 0.7rem;
}
.card-icon svg { width: 16px; height: 16px; }
.card-meta {
  margin-top: 0.85rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--line-soft);
  display: flex; align-items: center; gap: 0.5rem;
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--muted);
}

/* ----------------------------------------------------------- stat tiles -- */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
}
.stat { background: var(--surface); padding: 0.85rem 0.95rem 0.95rem; }
.stat-label {
  font-family: var(--mono);
  font-size: 0.64rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}
.stat-value {
  font-family: var(--display);
  font-size: 1.62rem;
  font-weight: 700;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  margin-top: 0.2rem;
  line-height: 1.05;
}
.stat-value.sig { color: var(--signal); }
.stat-foot { font-size: 0.7rem; color: var(--muted); margin-top: 0.15rem; }

/* --------------------------------------------------------------- tables -- */
.tbl-wrap { overflow-x: auto; }
table.tbl {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.82rem;
}
table.tbl th {
  position: sticky; top: 0; z-index: 2;
  text-align: left;
  font-family: var(--mono);
  font-size: 0.64rem;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--raised);
  padding: 0.5rem 0.7rem;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
table.tbl th.sortable { cursor: pointer; user-select: none; }
table.tbl th.sortable:hover { color: var(--ink); }
table.tbl th[aria-sort] { color: var(--signal); }
table.tbl td {
  padding: 0.44rem 0.7rem;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
}
table.tbl tbody tr:hover { background: var(--raised); }
table.tbl td.n {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}
table.tbl tbody tr:last-child td { border-bottom: none; }

/* ------------------------------------------------------- command palette -- */
.cmdk-backdrop {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(3, 6, 8, 0.66);
  backdrop-filter: blur(3px);
  display: none;
}
.cmdk-backdrop.open { display: block; animation: fade 0.14s var(--ease); }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }

.cmdk {
  position: fixed; z-index: 201;
  top: 14vh; left: 50%;
  transform: translateX(-50%);
  width: min(620px, calc(100vw - 2rem));
  background: var(--glass-hi);
  backdrop-filter: blur(18px) saturate(1.3);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  display: none;
  flex-direction: column;
  max-height: 66vh;
}
.cmdk.open { display: flex; animation: cmdk-in 0.18s var(--ease); }
@keyframes cmdk-in {
  from { opacity: 0; transform: translateX(-50%) translateY(-8px) scale(0.985); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
}
.cmdk-input-wrap {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.85rem 1rem;
  border-bottom: 1px solid var(--line);
}
.cmdk-input-wrap > svg { width: 16px; height: 16px; color: var(--muted); flex: 0 0 auto; }
.cmdk-input-wrap input {
  flex: 1; min-width: 0;
  font: inherit; font-size: 0.98rem;
  color: var(--ink);
  background: none; border: none; outline: none;
}
.cmdk-input-wrap input::placeholder { color: var(--muted); }
.cmdk-list { overflow-y: auto; padding: 0.35rem; }
.cmdk-group {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 0.55rem 0.6rem 0.3rem;
}
.cmdk-item {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.46rem 0.6rem;
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: 0.86rem;
}
.cmdk-item .swatch,
.cmdk-item img { flex: 0 0 auto; }
.cmdk-item img { width: 18px; height: 18px; object-fit: contain; }
.cmdk-item .cmdk-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cmdk-item .cmdk-hint {
  font-family: var(--mono); font-size: 0.68rem; color: var(--muted); flex: 0 0 auto;
}
.cmdk-item[aria-selected="true"] { background: var(--signal-soft); color: var(--ink); }
.cmdk-item[aria-selected="true"] .cmdk-hint { color: var(--signal); }
.cmdk-empty { padding: 1.6rem 1rem; text-align: center; color: var(--muted); font-size: 0.85rem; }
.cmdk-foot {
  display: flex; align-items: center; gap: 0.8rem;
  padding: 0.5rem 0.9rem;
  border-top: 1px solid var(--line);
  font-size: 0.7rem;
  color: var(--muted);
}
.cmdk-foot span { display: inline-flex; align-items: center; gap: 0.3rem; }

.swatch {
  width: 11px; height: 11px;
  border-radius: 3px;
  border: 1px solid rgba(0, 0, 0, 0.45);
  display: inline-block;
}

/* --------------------------------------------------------------- toasts -- */
.toast-host {
  position: fixed; z-index: 300;
  bottom: 1.1rem; left: 50%;
  transform: translateX(-50%);
  display: flex; flex-direction: column; gap: 0.4rem;
  align-items: center;
  pointer-events: none;
}
.toast {
  background: var(--glass-hi);
  backdrop-filter: blur(12px);
  border: 1px solid var(--signal-line);
  color: var(--ink);
  font-size: 0.82rem;
  padding: 0.5rem 0.9rem;
  border-radius: 999px;
  box-shadow: var(--shadow);
  animation: toast-in 0.22s var(--ease);
}
@keyframes toast-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
.toast.out { animation: toast-out 0.22s var(--ease) forwards; }
@keyframes toast-out {
  to { opacity: 0; transform: translateY(8px); }
}

/* --------------------------------------------------------- boot sequence -- */
/* THE one coordinated motion moment on the site: a staggered reveal on load,
   like an instrument powering up. Everything else on the page is still.
   Elements opt in with .boot and stagger with --d. */
.boot {
  opacity: 0;
  transform: translateY(9px);
  animation: boot-in 0.5s var(--ease) forwards;
  animation-delay: var(--d, 0s);
}
@keyframes boot-in {
  to { opacity: 1; transform: translateY(0); }
}

/* A single scan sweep across the viewport on first paint. Runs once, then the
   element removes itself from the layout. Purely atmospheric — pointer-events
   none, and it is behind everything interactive. */
.scanline {
  position: fixed; inset: 0;
  z-index: 100;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    transparent 0%,
    rgba(59, 239, 200, 0.055) 46%,
    rgba(59, 239, 200, 0.16) 50%,
    rgba(59, 239, 200, 0.055) 54%,
    transparent 100%
  );
  transform: translateY(-100%);
  animation: sweep 1.15s var(--ease) 0.1s forwards;
}
@keyframes sweep {
  to { transform: translateY(100%); }
}

/* Anyone who has asked the OS to stop moving things gets a static page. This
   covers the boot stagger, the sweep and the live-dot pulse. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .boot { opacity: 1; transform: none; }
  .scanline { display: none; }
}

/* ------------------------------------------------------------- utilities -- */
.wrap { max-width: 1220px; margin: 0 auto; padding: 0 1.4rem; }
.stack > * + * { margin-top: var(--gap, 1rem); }
.row { display: flex; align-items: center; gap: 0.6rem; }
.row.wrap-row { flex-wrap: wrap; }
.spacer { flex: 1 1 auto; }
.hide { display: none !important; }
.trunc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.center { text-align: center; }
.grid-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(248px, 1fr));
  gap: 0.9rem;
}
.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;
}

/* Loading skeleton — a calm shimmer, not a spinner. */
.skel {
  background: linear-gradient(90deg, var(--raised) 25%, var(--raised-2) 50%, var(--raised) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s linear infinite;
  border-radius: var(--r-xs);
  color: transparent;
}
@keyframes shimmer { to { background-position: -200% 0; } }

/* Filters bottom-sheet trigger and scrim. Injected by Wave.mountFilterSheets
   (app.js) next to every .toolbar that has chip filters. Desktop never sees
   the button — the toolbar just sits there as normal; this only activates
   under the mobile breakpoint below. */
.filters-btn {
  display: none; align-items: center; justify-content: space-between; gap: 0.5rem;
  width: 100%; margin-bottom: 0.5rem;
}
.filters-btn .filters-count { font-family: var(--mono); color: var(--signal); }


/* ------------------------------------------------------------ responsive -- */
/* Below 860px the rail goes horizontal and pins to the bottom, thumb-first.
   The map page needs more than this and handles its own layout below. */
@media (max-width: 860px) {
  .app { grid-template-columns: 1fr; }
  .rail {
    position: fixed;
    bottom: 0; left: 0; right: 0;
    top: auto;
    flex-direction: row;
    justify-content: space-around;
    align-items: center;
    gap: 0;
    padding: 0.3rem 0.4rem;
    border-right: none;
    border-top: 1px solid var(--line);
    background: var(--glass-hi);
    backdrop-filter: blur(14px);
    padding-bottom: max(0.3rem, env(safe-area-inset-bottom));
  }
  .rail-mark, .rail-spacer { display: none; }
  .rail-link::after { display: none; }
  .rail-link {
    flex-direction: column;
    gap: 2px;
    width: auto;
    min-width: 44px;          /* WCAG 2.5.8 touch target */
    height: 44px;
    padding: 0 0.2rem;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .rail-link svg { width: 17px; height: 17px; }
  .rail-text {
    display: block;
    font-size: 9px;
    line-height: 1;
    letter-spacing: 0.01em;
    max-width: 56px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .rail-link.current::before {
    left: 50%; top: -5px;
    transform: translateX(-50%);
    width: 20px; height: 3px;
    border-radius: 0 0 3px 3px;
  }
  body { padding-bottom: 58px; }
  .wrap { padding: 0 1rem; }

  /* Sticky filter toolbars.
     Every filtered page stacks two or three rows of filter chips inside a
     STICKY toolbar. On a phone those rows wrap: the weapons toolbar measured
     235px tall on a 812px screen — 29% of the viewport permanently occupied by
     controls, with the actual content squeezed into what was left.
     On narrow screens each chip row becomes a single horizontally-scrollable
     line instead. The chips are all still reachable, the toolbar collapses to
     roughly one row per axis, and the content gets its screen back. */
  .chipset, .gchips {
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    /* Bleed to the screen edges so a scrolled row reads as continuing past
       them rather than being cut off inside a box. */
    margin-left: -1rem;
    margin-right: -1rem;
    padding: 0.1rem 1rem;
    scroll-padding-left: 1rem;
    -webkit-overflow-scrolling: touch;
  }
  .chipset::-webkit-scrollbar, .gchips::-webkit-scrollbar { display: none; }
  .chipset > *, .gchips > * { flex: 0 0 auto; }

  /* Filters bottom sheet — the horizontal-scroll fix above still left a
     toolbar that could eat a third of the screen before you saw any content.
     The better answer: collapse the whole toolbar to one "Filters (N)"
     button and only pay the screen-space cost while it is actually open. */
  .toolbar.has-sheet { max-height: none; overflow: visible; }
  /* Inside the media query on purpose. These used to sit outside it, so an
     open sheet's scrim survived a resize past the breakpoint and covered the
     desktop page with nothing visible to dismiss it. Now it cannot outlive
     the layout it belongs to, whatever the JS does.  */
  .filter-scrim {
    position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); z-index: 64;
    opacity: 0; pointer-events: none; transition: opacity 0.18s var(--ease);
  }
  .filter-scrim.show { opacity: 1; pointer-events: auto; }

  .filters-btn { display: flex; }
  .toolbar.has-sheet > .wrap > .row,
  .toolbar.has-sheet > .wrap > .chipset,
  .toolbar.has-sheet > .wrap > .gchips { display: none; }
  /* A row a page marks `always-on` is navigation, not a filter, and must
     survive the collapse — see the loot page's mode/format selects. */
  .toolbar.has-sheet > .wrap > .row.always-on { display: flex !important; }
  .toolbar.has-sheet.sheet-open {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto;
    max-height: 70vh; overflow-y: auto;
    border-radius: var(--r) var(--r) 0 0;
    box-shadow: var(--shadow-lg);
    z-index: 65;
    padding-bottom: env(safe-area-inset-bottom);
  }
  .toolbar.has-sheet.sheet-open > .wrap > .row,
  .toolbar.has-sheet.sheet-open > .wrap > .chipset,
  .toolbar.has-sheet.sheet-open > .wrap > .gchips { display: flex; }
  .toolbar.has-sheet.sheet-open > .wrap > .row { flex-wrap: wrap; }
  /* Inside the open sheet there is no reason to keep the horizontal-scroll
     trick from above — there is finally room to just wrap. */
  .toolbar.has-sheet.sheet-open .chipset,
  .toolbar.has-sheet.sheet-open .gchips {
    flex-wrap: wrap; overflow: visible;
    margin: 0.4rem 0 0 !important; padding: 0.1rem 0 !important;
  }
}
