/* Fonts are loaded in <head> (preconnect + stylesheet link) so they're
   available on first paint — no @import here, which loads fonts too late. */

* { box-sizing: border-box; }

/* ── Custom overlay scrollbar ──────────────────────────────────────────────
   Native scrollbar is hidden so it never reserves width (no page shift). A
   floating JS-driven thumb (scrollbar.js) overlays the page and is inset to
   start below the nav and end above the footer. */
html {
  scrollbar-width: none;            /* Firefox: hide native */
  -ms-overflow-style: none;         /* old Edge/IE */
}
html::-webkit-scrollbar { width: 0; height: 0; display: none; }  /* Chrome/Safari */

.ov-scroll-track {
  position: fixed; right: 4px; width: 10px;
  z-index: 200; border-radius: 100px;
  opacity: 0; transition: opacity .2s;
  background: transparent;
}
.ov-scroll-thumb {
  position: absolute; left: 1px; right: 1px; top: 0; width: 8px;
  border-radius: 100px;
  background: rgba(91,140,247,0.28);
  transition: background .15s;
  cursor: grab;
}
.ov-scroll-track:hover .ov-scroll-thumb { background: rgba(91,140,247,0.42); }
.ov-scroll-track.dragging .ov-scroll-thumb { background: rgba(91,140,247,0.6); cursor: grabbing; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--page-bg);
  color: var(--text);
  font-family: var(--font);
  /* Prevent click-drag text selection site-wide, like donutsmp.bet */
  user-select: none;
  -webkit-user-select: none;
}
/* Scrollbar only appears when the page actually overflows (auto, not scroll).
   overflow-x hidden guards against any horizontal bar from transient layout. */
html { overflow-x: hidden; }  /* vertical scroll works; native bar hidden above */

/* Full-height flex column so the footer pegs to the bottom of the viewport
   even when page content is short (matches donutsmp.bet). The element right
   before .footer flexes to fill the gap. */
body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
body > .footer { margin-top: auto; }
.container, .search-shell { flex: none; }

/* Re-enable selection where it actually makes sense (inputs) */
input, textarea { user-select: text; -webkit-user-select: text; }

/* Images shouldn't be draggable either */
img { -webkit-user-drag: none; user-drag: none; }

a { color: inherit; text-decoration: none; }

/* ── Global hover tooltip (matches donutsmp.bet .hover-tip) ── */
.hover-tip {
  position: fixed;
  z-index: 10002;
  background: #1e2130;
  border: 1px solid #2e3247;
  color: #b9c0d8;
  font-family: var(--font);
  font-size: 11px;
  font-weight: 600;
  /* pre-line renders the \n in multi-line tips (the sales feed sends item /
     price / seller on separate lines). textContent preserves them; without this
     they'd collapse into one run-on line. */
  white-space: pre-line;
  line-height: 1.5;
  padding: 5px 10px;
  border-radius: 7px;
  pointer-events: none;
  text-align: center;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.14s ease, transform 0.14s ease;
  box-shadow: 0 6px 20px rgba(0,0,0,0.4);
}
.hover-tip.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* ---------- navbar ---------- */

.navbar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;   /* left | centered links | right */
  align-items: center;
  padding: 8px 32px;
  border-bottom: 1px solid var(--border-soft);
  /* Stay pinned to the top while scrolling. */
  position: sticky; top: 0; z-index: 150;
  background: var(--page-bg);
  backdrop-filter: blur(8px);
}

.nav-left {
  display: flex;
  align-items: center;
  justify-self: start;
}

/* Brand: bigger logo than text, text grows on hover (matches donutsmp.bet) */
.brand {
  display: flex;
  align-items: center;   /* vertically centers text to the logo */
  gap: 11px;
  cursor: pointer;
  user-select: none;
  flex-shrink: 0;
}

.brand img {
  width: 48px;
  height: 48px;
  border-radius: 11px;
  flex-shrink: 0;
  display: block;
  /* logo stays fixed size — only the text scales on hover */
}

.brand-text {
  font-size: 21px;
  font-weight: 800;
  letter-spacing: -0.01em;
  /* Sora's glyphs sit optically high within their line box (lots of unused
     descender space below, since the wordmark has no descenders). Centering
     the line box therefore leaves the visible letters looking high. Fix:
     collapse the line box to the cap height and nudge down a hair so the
     VISIBLE letters — not the empty box — center against the logo. */
  display: inline-block;
  line-height: 1;
  transform: translateY(0.04em);
  transform-origin: left center;
  transition: transform 0.18s cubic-bezier(0.34, 1.3, 0.64, 1);
}

.brand:hover .brand-text { transform: translateY(0.04em) scale(1.07); }

.brand .white { color: rgba(255,255,255,0.95); }
.brand .blue  { color: var(--blue); }

.nav-links {
  display: flex;
  align-items: center;
  gap: 22px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-mid);
  justify-self: center;   /* dead-center of the page */
}

.nav-links > a,
.nav-links > span[data-href],
.nav-links > .nav-dropdown > .nav-dropdown-trigger {
  color: var(--text-mid);
  cursor: pointer;
  transition: color 0.12s;
  font-family: var(--font); font-size: 13px; font-weight: 600;
}

.nav-links a:hover,
.nav-links a.active,
.nav-links span[data-href]:hover,
.nav-links span[data-href].active,
.nav-links .nav-dropdown:hover .nav-dropdown-trigger,
.nav-links .nav-dropdown.open .nav-dropdown-trigger {
  color: var(--text);
}

/* ── Dropdown ── */
.nav-dropdown { position: relative; }
.nav-dropdown-trigger {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--font);
  font-size: 13px;
  font-weight: 600;
  background: none;
  border: none;
  padding: 0;
}
.nav-dropdown-trigger svg {
  width: 12px; height: 12px;
  transition: transform 0.18s;
}
/* chevron rotation handled by .open rule */

.nav-dropdown-menu {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%) translateY(6px);
  min-width: 180px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.15s, transform 0.15s, visibility 0.15s;
  z-index: 50;
  box-shadow: 0 12px 32px rgba(0,0,0,0.4);
}
/* Dim the page while the Other menu is open, matching the search + time menus. */
.nav-dropdown-veil {
  position: fixed; inset: 0; z-index: 140;
  /* Lighter than the search/time veils on purpose — this menu is a small nav
     affordance, not a focused task, so it shouldn't wall the page off. */
  background: rgba(10,10,14,0.18);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
  animation: ndVeil .18s ease both;
}
@keyframes ndVeil { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .nav-dropdown-veil { animation: none; } }

/* small hover bridge so the menu doesn't vanish when moving the cursor down */
.nav-dropdown::after {
  content: '';
  position: absolute;
  top: 100%; left: 0; right: 0; height: 10px;
}
.nav-dropdown.open .nav-dropdown-menu {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(6px);
}
.nav-dropdown.open .nav-dropdown-trigger svg { transform: rotate(180deg); }
.nav-dropdown-menu {
  padding: 0; overflow: hidden;
  background: #0e121b;   /* same step-darker surface as the profile menu */
}
/* .nav-soon is included explicitly: the row styling keys off [data-href], and a
   "coming soon" entry deliberately has none — so it inherited nothing and
   rendered as bare text. It should be the SAME row, just dimmed. */
.nav-dropdown-menu a,
.nav-dropdown-menu .nav-soon,
.nav-dropdown-menu span[data-href] {
  /* Full-bleed like every other dropdown: no inset pill, the menu's own
     overflow:hidden rounds the first/last rows into its corners. */
  display: block; border-radius: 0;
  padding: 10px 14px;
  border-radius: 8px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-mid);
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}
.nav-dropdown-menu a:hover,
.nav-dropdown-menu span[data-href]:hover {
  background: var(--card-bg-hover);
  color: var(--text);
}

/* stretch, not end: the track now spans the full width from the centred links to
   the page edge, so the pill has a space to be centred IN. justify-content keeps
   the account pinned right regardless. */
.nav-right {
  justify-self: stretch;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 14px;
  font-size: 13px;
}

.nav-login {
  color: var(--text-mid);
  font-weight: 600;
}

.nav-login:hover { color: var(--text); }

.btn-register {
  background: var(--blue);
  color: #042c53;
  font-weight: 700;
  padding: 8px 18px;
  border-radius: 8px;
  border: none;
  cursor: pointer;
  font-family: var(--font);
  font-size: 13px;
}

.btn-register:hover { background: var(--blue-bright); }

/* Discord sign-in button — matches donutsmp.bet's blurple + icon sizing */
.btn-discord-signin {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: #5865F2;
  color: #fff;
  font-family: var(--font);
  font-weight: 700;
  font-size: 13px;
  border: none;
  border-radius: 8px;
  padding: 8px 16px;
  cursor: pointer;
  transition: background 0.15s ease;
}

.btn-discord-signin svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  fill: #fff;
}

.btn-discord-signin:hover { background: #4752c4; }

.nav-user {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text);
  font-weight: 600;
  font-size: 13px;
}

.nav-user svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  fill: #5865F2;
}

.btn-signout {
  background: none;
  border: 1px solid var(--border-bright);
  color: var(--text-mid);
  font-family: var(--font);
  font-weight: 600;
  font-size: 12px;
  border-radius: 8px;
  padding: 7px 14px;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.btn-signout:hover {
  color: var(--text);
  border-color: var(--blue-border);
}

/* ---------- signed-in profile pill + connected expanding dropdown ----------
   Closed: a clean, flat pill (circular avatar + name + chevron).
   Open: the body drops below the head as an ABSOLUTE overlay (so it escapes
   the header and floats over the page) but is styled to read as ONE connected
   card — same background/border, joined at the seam, with the max-height
   expand/shrink animation. Plain flat colors, no color morph. */
/* ── the wallet IS the menu ──────────────────────────────────────────────────
   One bordered, rounded, clipped shell holding the head and the body; it just
   gets taller. Same construction as the AH search bar. Two elements pretending
   to be joined is what produced the corner-timing and alignment bugs there —
   with one element there's nothing to align and no radius to animate.
   #nav-auth-slot reserves the collapsed height so growth OVERLAYS the header
   instead of making the header taller. */
#nav-auth-slot { position: relative; display: inline-block; min-height: 42px; z-index: 160; }
.nav-wallet {
  position: absolute; top: 0; right: 0; min-width: 100%;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 16px;
  overflow: hidden;                 /* clips the body into the shell's corners */
  transition: border-color .15s;
}
.nav-wallet.open { border-color: var(--blue-border); }

.nav-wallet-head {
  display: flex; align-items: center; gap: 11px; width: 100%;
  /* Transparent: the shell owns the border, background and radius. */
  background: none; border: 0; border-radius: 0;
  color: var(--text);
  font-family: var(--font); font-weight: 600; font-size: 13.5px;
  padding: 5px 15px 5px 5px; cursor: pointer; text-align: left;
  position: relative; z-index: 2;
  /* Same corner timing as the search bar: instant on open, delayed until the
     body has finished collapsing on close. The old .1s ran against the body's
     .26s max-height, so the corners un-squared while the menu was still open. */
  transition: border-color .15s, border-radius 0s linear .26s;
}
/* Hover moves to the SHELL — the head has no border of its own any more. */
.nav-wallet:not(.open):hover { border-color: var(--border-bright); }
/* Nothing to do on open any more: the head has no border or radius of its own,
   and the body's own border-top is the divider. This used to flatten the head's
   corners to fake the join — that's what needed the timing hack. */

.nav-wallet-avatar {
  width: 32px; height: 32px; border-radius: 50%;
  image-rendering: pixelated; background: var(--surface-3); flex-shrink: 0;
  object-fit: cover;
  /* A background under the image so the slot is never blank while it decodes.
     The menu re-renders on every session check, and an empty box that fills in
     a moment later reads as a flash. */
  background: var(--card-bg-hover, #18203a);
  background-image: none;
}
.nav-wallet-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); letter-spacing: -0.01em; }
.nav-wallet-chevron {
  width: 14px; height: 14px; color: var(--text-dim); flex-shrink: 0;
  transition: transform .22s cubic-bezier(0.34,1.2,0.64,1);
}
.nav-wallet.open .nav-wallet-chevron { transform: rotate(180deg); }

/* Body drops as an absolute overlay just below the head — escapes the header,
   overlays the page. Same card surface + border, top seam joined to the head.
   The max-height morph gives the expand/shrink. */
.nav-wallet-body {
  /* In normal flow INSIDE the shell — growing it grows the shell. No border, no
     radius, no shadow of its own: the shell provides all of that, so there are
     no two edges to meet and nothing to keep in sync. */
  position: static; width: 100%;
  /* #0e121b — a step darker than the head, same as the AH search dropdown. */
  background: #0e121b; border: 0; border-top: 1px solid transparent;
  border-radius: 0; box-shadow: none;
  display: flex; flex-direction: column; gap: 0;
  padding: 0;
  max-height: 0; opacity: 0; overflow: hidden; pointer-events: none;
  z-index: 1;
  /* One curve, one duration — mismatched easings are what stuttered on the
     search bar. Padding no longer animates because there isn't any. */
  /* Slower on purpose — .26s felt snappy to the point of abrupt on a menu this
     small. One curve, one duration, as always. */
  transition: max-height .42s cubic-bezier(0.4,0,0.2,1),
              opacity .32s cubic-bezier(0.4,0,0.2,1),
              border-color .42s cubic-bezier(0.4,0,0.2,1);
}
.nav-wallet.open .nav-wallet-body {
  max-height: 260px; opacity: 1; pointer-events: auto;
  border-top-color: var(--border);   /* the divider between head and items */
}

.nav-wallet-item {
  display: flex; align-items: center; gap: 11px; width: 100%;
  /* Edge-to-edge and square: the shell's overflow:hidden clips the last item's
     hover into the bottom corners, so the fill is always perfect and there's no
     "last item" special case. */
  background: none; border: none; color: var(--text-mid);
  font-family: var(--font); font-weight: 600; font-size: 13px;
  padding: 10px 15px; border-radius: 0; cursor: pointer; text-align: left;
  transition: background .12s, color .12s;
}
.nav-wallet-item svg { width: 16px; height: 16px; flex-shrink: 0; }
/* The Support entry is an <a> rather than a <button>, so it needs the two things
   a button gets for free. */
a.nav-wallet-item { text-decoration: none; box-sizing: border-box; }
/* Sits at the far end of the linked row — the label carries the account name,
   this says what clicking does. */
.nav-wallet-sub {
  margin-left: auto; font-size: 10.5px; font-weight: 700; letter-spacing: .05em;
  color: var(--text-dim); text-transform: uppercase;
}
.nav-wallet-item:hover .nav-wallet-sub { color: var(--blue-bright); }
.nav-wallet-item:hover { background: var(--surface-3); color: var(--text); }
.nav-wallet-item-danger { color: rgba(239,68,68,0.7); }
.nav-wallet-item-danger:hover { background: rgba(239,68,68,0.1); color: var(--red); }




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

.search-shell {
  padding: 64px 24px 40px;
  text-align: center;
}

/* Gentle staggered entrance for the search shell so it eases in on load
   instead of hard-flashing. Each element rises + fades with a small delay. */
@keyframes shellRise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
.search-tagline,
.search-subtext,
.search-box {
  animation: shellRise .5s cubic-bezier(0.16,1,0.3,1) both;
}
.search-tagline { animation-delay: .02s; }
.search-subtext { animation-delay: .08s; }
.search-box     { animation-delay: .14s; }
@media (prefers-reduced-motion: reduce) {
  .search-tagline, .search-subtext, .search-box { animation: none; }
}

.search-tagline {
  font-size: 22px;
  font-weight: 700;
  margin: 0 0 8px;
}

.search-subtext {
  font-size: 13px;
  color: var(--text-mid);
  margin: 0 0 24px;
}

.search-box {
  max-width: 460px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 12px 20px;
  cursor: text;   /* clicking anywhere in the bar acts like clicking the input */
}

.search-box .btn-search { cursor: pointer; }

.search-box:focus-within {
  border-color: var(--blue-border);
}

.search-box input {
  flex: 1;
  background: none;
  border: none;
  outline: none;
  color: var(--text);
  font-family: var(--font);
  font-size: 14px;
}

.search-box input::placeholder { color: var(--text-dim); }

/* Inline "player doesn't exist" error state: red border + red placeholder
   text shown inside the box for a few seconds (matches donutsmp.bet). */
/* Colour only. The shake lives on its own class so it can be removed and
   re-added to replay on a second failed search — and so it doesn't clobber
   .search-box's shellRise animation (same property), which would otherwise
   re-run the entrance animation the moment the error cleared. */
.search-box.search-box-error {
  border-color: var(--red);
  background: rgba(239,68,68,0.05);
  box-shadow: 0 0 0 2px rgba(239,68,68,0.14);
}
/* The whole bar eases in and out of the error state. Without these the border,
   tint, button and placeholder all snapped — that's the "flash". */
.search-box {
  transition: border-color .25s ease, box-shadow .25s ease, background-color .25s ease;
}
.search-box input::placeholder { transition: color .25s ease; }
.search-box .btn-search {
  transition: background .25s ease, color .25s ease, box-shadow .25s ease;
}
.search-box svg { transition: color .25s ease; }
/* The button goes red with the bar. */
.search-box.search-box-error .btn-search {
  background: var(--red);
  color: #fff;
  box-shadow: 0 2px 10px rgba(239,68,68,0.28);
}
/* !important so it beats the inline `animation: none` that home-view.js pins on
   after shellRise finishes. That pin is what stops the entrance animation
   REPLAYING every time the shake class is removed: `animation` is one property,
   so dropping searchShake let the element fall back to shellRise and slide in
   again — which is the "it re-enters like a page refresh" you saw. */
.search-box.search-box-shake { animation: searchShake .4s ease !important; }
@media (prefers-reduced-motion: reduce) { .search-box.search-box-shake { animation: none !important; } }
.search-box.search-box-error input::placeholder {
  color: var(--red);
  opacity: 1;
  font-weight: 600;
}
.search-box.search-box-error svg { color: var(--red); }
@keyframes searchShake {
  0%,100% { transform: translateX(0); }
  20% { transform: translateX(-5px); }
  40% { transform: translateX(5px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(3px); }
}
@media (prefers-reduced-motion: reduce) { .search-box.search-box-error { animation: none; } }

.search-box svg { flex-shrink: 0; color: var(--text-dim); }

.btn-search:disabled { opacity: .7; cursor: default; }

.btn-search {
  background: var(--blue);
  color: #042c53;
  font-weight: 700;
  border: none;
  border-radius: var(--radius-pill);
  padding: 8px 18px;
  font-family: var(--font);
  font-size: 13px;
  cursor: pointer;
}

.btn-search:hover { background: var(--blue-bright); }

.search-error {
  max-width: 460px;
  margin: 14px auto 0;
  font-size: 12px;
  color: var(--red);
  display: none;
}

/* ---------- cards / stat grid ---------- */

.container {
  max-width: 1000px;
  margin: 0 auto;
  padding: 0 24px;
}

.card {
  background: var(--card-bg);
  border-radius: var(--radius-card);
  padding: 16px;
}

.stat-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 12px;
  /* == the grid's own gap, so the space above the stats matches the space
     between them. The header used to have 32px of padding under it, which made
     the skin viewer and vouch pills read as floating above the grid instead of
     sitting on it. */
  margin-top: 12px;
}

.stat-label {
  font-size: 11px;
  font-weight: 700;
  color: var(--text-mid);
  text-transform: uppercase;
  letter-spacing: 0.01em;
  margin: 0;
  white-space: nowrap;
}

.stat-value {
  width: fit-content;   /* tooltip only triggers on the number, not the whole row */
  margin-left: auto; margin-right: auto;   /* ...but keep it centered in the card */
  font-size: 18px;
  font-weight: 700;
  /* donutsmp.bet uses Sora for its big numbers (--font-num: Sora), NOT a mono
     font — Sora has a clean plain zero. JetBrains Mono's zero has a baked-in
     dot that no CSS/OpenType setting can remove, so we match donutsmp.bet
     and use Sora here for clean numerals. */
  font-family: var(--font);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  color: var(--text);
}

/* ---------- footer (matches donutsmp.bet) ---------- */

.footer {
  border-top: 1px solid var(--border-soft);
  margin-top: auto;
}
.footer-strip {
  display: flex;
  align-items: center;
  padding: 0 24px;
  height: 46px;
  width: 100%;
  margin: 0;
  gap: 0;
}
.footer-brand {
  flex-shrink: 0;
  font-size: 13px;
  font-weight: 900;
  color: #fff;
  letter-spacing: -0.01em;
  white-space: nowrap;
}
.footer-brand-accent { color: var(--blue); }
.footer-disclaimer {
  flex: 1;
  min-width: 0;
  font-size: 10px;
  color: rgba(255,255,255,0.2);
  margin: 0;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 0 12px;
}
.footer-disclaimer strong { color: rgba(255,255,255,0.28); font-weight: 600; }
.footer-links {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 2px;
}
.footer-link {
  background: none; border: none; cursor: pointer; font-family: var(--font);
  font-size: 11.5px; font-weight: 600; color: rgba(255,255,255,0.38);
  padding: 3px 6px; border-radius: 5px; transition: color .12s;
  text-decoration: none; display: inline-flex; align-items: center; gap: 5px;
}
.footer-link:hover { color: rgba(255,255,255,0.75); }
.footer-link-highlight { color: rgba(72,128,244,0.7); }
.footer-link-highlight:hover { color: var(--blue-bright); }
.footer-sep { color: rgba(255,255,255,0.15); font-size: 11px; }

@media (max-width: 720px) {
  .footer-disclaimer { display: none; }
  .footer-brand { display: none; }
  .footer-strip { justify-content: center; }
}

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

@media (max-width: 720px) {
  .nav-links { display: none; }
  .stat-grid { grid-template-columns: repeat(2, 1fr); }
}

/* ── Player view styles (extracted from old player.html) ── */
/* No align-items: the name and "updated Xs ago" belong at the TOP, where they
   were — flex-end dragged that whole column down with the pills, which was never
   the ask. The pills are absolutely positioned, so they bottom-align on their own
   (see .vouch-pills) without touching anything else.
   padding-bottom stays 0: a container's own padding is inside it and can't make a
   gap to the grid — that's .stat-grid's margin-top. */
.player-header {
  display: flex; gap: 24px;
  padding: 32px 0 0; position: relative;
}
  .skin-box {
    width: 150px; height: 190px; flex-shrink: 0;
    background: var(--card-bg); border: 1px solid var(--border);
    border-radius: var(--radius-card);
    display: flex; align-items: center; justify-content: center;
    overflow: hidden;
    position: relative;
    cursor: grab;
  }
  .skin-box:active { cursor: grabbing; }
  /* Canvas stays invisible until the skin texture has loaded, so we never
     flash a white/blank canvas over the card background. The card bg +
     spinner show through until then. Background:transparent guards against
     the browser's default white canvas fill showing before the first paint. */
  .skin-box canvas {
    display: block;
    opacity: 0;
    background: transparent !important;
    transition: opacity 0.25s ease;
  }
  .skin-box.skin-ready canvas { opacity: 1; }
  .skin-hint {
    position: absolute; bottom: 6px; left: 0; right: 0;
    text-align: center; font-size: 9px; color: var(--text-dim);
    pointer-events: none; opacity: 0.7; letter-spacing: 0.03em;
    text-transform: uppercase;
  }
  .skin-loading {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    color: var(--text-dim);
    /* Fade the spinner in only after a short delay, so instant cached-skin
       loads (which resolve in a few ms) never flash a spinner. */
    opacity: 0;
    animation: spinnerFadeIn 0.2s ease forwards;
    animation-delay: 0.35s;
  }
  @keyframes spinnerFadeIn { to { opacity: 1; } }
  /* Once the skin is ready, hide the loader immediately regardless of delay. */
  .skin-box.skin-ready .skin-loading { display: none; }
  .skin-loading::after {
    content: ''; width: 22px; height: 22px;
    border: 2px solid var(--surface-4); border-top-color: var(--blue);
    border-radius: 50%; animation: spin 0.8s linear infinite;
  }
  @keyframes spin { to { transform: rotate(360deg); } }
  .player-info { padding-top: 4px; }
  .meta-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
  .player-name { font-size: 24px; font-weight: 700; margin: 0; cursor: pointer; width: fit-content; }
  .player-name:hover { opacity: 0.85; }
  .freshness { font-size: 11px; color: var(--text-dim); }
  .meta-sep { color: var(--text-dim); font-size: 11px; opacity: 0.5; }
  .view-count {
    position: absolute; top: 7px; left: 7px; z-index: 3;
    display: inline-flex; align-items: center; gap: 3px;
    font-size: 9px; color: var(--text-dim);
    background: rgba(10,10,14,0.45);
    padding: 2px 5px; border-radius: var(--radius-pill);
    pointer-events: none;
  }
  .view-count svg { width: 10px; height: 10px; opacity: 0.8; }
  #view-count-num { font-family: var(--font); color: var(--text-mid); font-variant-numeric: tabular-nums; }

  /* Vouch/scam pills pegged to the bottom-right of the header area, so they
     sit above the rightmost stat card (deaths) and stay out of the way. */
  /* bottom: 0, not 32px.
     These are absolutely positioned, so they sit OUTSIDE the flex flow — which is
     why align-items:flex-end on .player-header had no effect on them. The 32px
     was matched to the header's old padding-bottom; that padding is gone now, so
     it was just lifting them 32px off the baseline the character sits on.
     At 0 they bottom-align with .skin-box, and both get the grid's 12px below. */
  .vouch-pills {
    display: flex; gap: 8px;
    position: absolute; right: 0; bottom: 0;
  }
  .pill { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; padding: 4px 10px; border-radius: var(--radius-pill); }
  .pill-good { background: rgba(34,197,94,0.12); color: var(--green); }
  .pill-bad  { background: rgba(239,68,68,0.12); color: var(--red); }
  .action-row { display: flex; gap: 10px; margin-top: 4px; }
  .btn-outline { background: none; font-family: var(--font); font-size: 12px; font-weight: 600; padding: 8px 14px; border-radius: 8px; cursor: pointer; }
  .btn-outline.blue { border: 1px solid var(--blue-border); color: var(--blue); }
  .btn-outline.red  { border: 1px solid rgba(239,68,68,0.28); color: var(--red); }

  /* Icon + label centered as a group, constrained to the card so long labels
     like "BLOCKS PLACED" never leak outside. */
  .stat-head {
    display: flex; align-items: center; justify-content: center;
    gap: 6px; margin-bottom: 8px;
    max-width: 100%;
  }
  .stat-label-wrap {
    display: inline-flex; align-items: center; gap: 6px;
    min-width: 0;
  }
  .stat-ico {
    width: 18px; height: 18px; flex-shrink: 0;
    image-rendering: pixelated;
    object-fit: contain;
  }
  .card { text-align: center; }
  .stat-value.blue { color: var(--blue); }
  .stat-value.green { color: var(--green); }
  .stat-value.red { color: var(--red); }
  .stat-value.gold { color: var(--gold); }
  .stat-value.purple { color: var(--purple); }

  /* ── Skeleton shimmer (shown while stats load) ── */
  .stat-value { position: relative; min-height: 1em; }
  body.is-loading .stat-value {
    color: transparent !important;
    border-radius: 6px;
    background: linear-gradient(90deg, var(--surface-3) 25%, var(--surface-4) 37%, var(--surface-3) 63%);
    background-size: 400% 100%;
    animation: shimmer 1.4s ease infinite;
  }
  body.is-loading .stat-value.blue,
  body.is-loading .stat-value.green,
  body.is-loading .stat-value.red,
  body.is-loading .stat-value.gold,
  body.is-loading .stat-value.purple { color: transparent !important; }
  @keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }

  .player-error {
    display: none;
    max-width: 1000px; margin: 32px auto; padding: 20px;
    background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.2);
    border-radius: var(--radius-card); color: #fca5a5; font-size: 14px; text-align: center;
  }

/* ── Shared card (used by homepage carousel) ──────────────────────────────── */
main { width: 100%; }

.lb-track { display: flex; gap: 24px; width: max-content; will-change: transform; }

.lcard {
  width: 380px; flex-shrink: 0;
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: 16px; padding: 20px; display: flex; flex-direction: column;
  transform-origin: center center; cursor: pointer;
  transition: transform .26s cubic-bezier(0.34,1.3,0.5,1), box-shadow .26s, border-color .26s;
  /* Resting float shadow — soft, sits below the card */
  box-shadow: 0 10px 30px rgba(0,0,0,0.45), 0 3px 10px rgba(0,0,0,0.3);
}
.lcard:hover {
  /* Grow from the center in every direction (transform-origin: center).
     No margin push — the track gap is wide enough that the scaled card has
     room to grow with padding to spare, so neighbors stay put. */
  transform: scale(1.08);
  box-shadow: 0 30px 70px rgba(0,0,0,0.65), 0 12px 34px rgba(0,0,0,0.45);
  /* More dominant outline on hover — brighter blue with a soft glow ring. */
  border-color: var(--blue);
  box-shadow: 0 30px 70px rgba(0,0,0,0.65), 0 12px 34px rgba(0,0,0,0.45), 0 0 0 1px var(--blue-border);
  z-index: 3;
}
.lcard-title {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  font-size: 15px; font-weight: 700; text-transform: uppercase; letter-spacing: .02em; margin-bottom: 18px;
}
.lcard-title .lcard-title-text { display: inline-flex; align-items: center; gap: 9px; }
.lcard-title img { width: 22px; height: 22px; image-rendering: pixelated; }
.lcard-rows { display: flex; flex-direction: column; gap: 10px; margin-bottom: 18px; }
.crow { display: flex; align-items: center; gap: 11px; min-width: 0; }
.crank { font-size: 13px; font-weight: 800; width: 28px; flex-shrink: 0; font-variant-numeric: tabular-nums; color: var(--text-dim); }
.chead { width: 24px; height: 24px; border-radius: 5px; image-rendering: pixelated; background: var(--surface-3); flex-shrink: 0; }
.cname { max-width: 60%; min-width: 0; margin-right: auto; font-size: 13.5px; font-weight: 600; color: rgba(255,255,255,0.55); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color .12s; }
.clink { cursor: pointer; }
.cname.clink:hover { color: #fff; }
.cval { font-size: 13.5px; font-weight: 700; font-variant-numeric: tabular-nums; flex-shrink: 0; }
.lb-empty { font-size: 13px; color: var(--text-dim); text-align: center; padding: 30px 0; }
/* Empty compact card: fill the same vertical space a populated top-5 list
   occupies, so the "View more" button lands at the same height as other cards
   instead of floating up. Centered vertically within that space. */
.lcard-compact .lb-empty {
  min-height: 150px;
  display: flex; align-items: center; justify-content: center;
  padding: 0;
}

.lcard-more {
  margin-top: auto; align-self: stretch;
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  background: linear-gradient(180deg, rgba(91,140,247,0.16), rgba(91,140,247,0.08));
  border: 1px solid var(--blue-border); color: var(--blue-bright);
  font-family: var(--font); font-size: 12.5px; font-weight: 700; cursor: pointer;
  padding: 10px 16px; border-radius: 10px; transition: background .16s, border-color .16s;
}
.lcard-more svg { width: 14px; height: 14px; transition: transform .18s ease; }
.lcard-more:hover { background: linear-gradient(180deg, rgba(91,140,247,0.28), rgba(91,140,247,0.16)); border-color: rgba(91,140,247,0.5); }
.lcard-more:hover svg { transform: translateX(4px); }

/* Compact variant — smaller everything, for the homepage teaser */
.lcard-compact { width: 260px; padding: 14px; border-radius: 13px; }
/* Icon + title text centered together as one group (icon sits next to the
   text, and the whole pair is centered in the card). */
.lcard-compact .lcard-title {
  position: relative;
  display: flex; align-items: center; justify-content: center; gap: 7px;
  font-size: 12.5px;
  /* Top divider spans the FULL card width (extend past the 14px padding) and
     casts a soft shadow beneath it for separation. */
  padding-bottom: 12px;
  margin: 0 -14px 10px;
  padding-left: 14px; padding-right: 14px;
  border-bottom: 1px solid var(--border);
  box-shadow: 0 4px 8px -4px rgba(0,0,0,0.5);
}
.lcard-compact .lcard-title-text {
  display: inline; align-items: initial; gap: 0;   /* plain text, not a flex box */
}
.lcard-compact .lcard-title img {
  position: static; transform: none; margin: 0;
  width: 17px; height: 17px; flex-shrink: 0;
}
.lcard-compact .crank { font-size: 11px; width: 22px; }
.lcard-compact .chead { width: 20px; height: 20px; }
.lcard-compact .cname { font-size: 11.5px; }
.lcard-compact .cval { font-size: 11.5px; }
.lcard-compact .lcard-more { font-size: 11px; padding: 7px 12px; border-radius: 8px; gap: 6px; margin-top: 10px; }
.lcard-compact .lcard-more svg { width: 12px; height: 12px; }

/* Rows list — no bottom divider (removed); the View more button follows
   directly with its own top margin. */
.lcard-compact .lcard-rows {
  gap: 0;
  margin-bottom: 0;
  /* Reserve the full 5 rows (5 x 30px, measured). Boards with fewer entries —
     Vouches with one, a new board with none — used to collapse and drag the
     "View more" button up with them, so the cards in a row all sat at different
     heights. The slot is the same whether it's filled or not. */
  min-height: 150px;
}
/* Alternating row tint so the top-5 is easy to scan, matching the leaderboard
   page. Each row gets padding so the rounded tint has room. */
.lcard-compact .crow { padding: 5px 7px; border-radius: 7px; }
.lcard-compact .crow:nth-child(even) { background: rgba(120,160,235,0.04); }

/* Skeleton shimmer */
.crow-skeleton .chead-ph, .crow-skeleton .cname-ph, .crow-skeleton .cval-ph,
.chead-ph, .cname-ph, .cval-ph, .bbar-ph {
  background: linear-gradient(90deg, var(--surface-3) 25%, var(--surface-4) 37%, var(--surface-3) 63%);
  background-size: 400% 100%; animation: shimmer 1.4s ease infinite; border-radius: 4px;
}
.crow-skeleton .chead-ph { width: 24px; height: 24px; }
.crow-skeleton .cname-ph { flex: 1; height: 12px; }
.crow-skeleton .cval-ph { width: 48px; height: 12px; }

/* ── Homepage carousel wrapper ────────────────────────────────────────────── */
/* Horizontal clip (so the track scrolls invisibly) but generous vertical
   padding so a hovered/scaled card has room to grow without being cut off at
   the top or bottom. overflow stays hidden on X via the clip; the padding
   creates the vertical breathing space. */
.home-carousel { position: relative; overflow-x: clip; overflow-y: visible; padding: 34px 0 40px; }

/* ── Leaderboard page: one contained panel ────────────────────────────────── */
.lbp-panel {
  max-width: 640px; margin: 40px auto 0;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 1px 0 rgba(255,255,255,0.05) inset, 0 4px 24px rgba(0,0,0,0.5), 0 20px 60px rgba(0,0,0,0.35);
}

/* centered header */
.lbp-head { padding: 32px 26px 8px; text-align: center; }
/* Clean switch: a quick, simple opacity settle (no slide/vertical morph) so
   changing boards reads as a crisp swap rather than a flash. */
@keyframes lbpCleanSwap {
  from { opacity: 0.35; }
  to   { opacity: 1; }
}
.lbp-head-swap { animation: lbpCleanSwap .2s ease both; }
.lbp-list-swap { animation: lbpCleanSwap .22s ease both; }
@media (prefers-reduced-motion: reduce) {
  .lbp-head-swap, .lbp-list-swap { animation: none; }
}
.lbp-panel { animation: shellRise .5s cubic-bezier(0.16,1,0.3,1) both; }
@media (prefers-reduced-motion: reduce) { .lbp-panel { animation: none; } }
.lbp-badge {
  width: 60px; height: 60px; margin: 0 auto 14px; border-radius: 15px;
  background: linear-gradient(160deg, var(--surface-4), var(--surface-3));
  border: 1px solid var(--border-bright);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 1px 0 rgba(255,255,255,0.06) inset, 0 6px 18px rgba(0,0,0,0.4);
}
.lbp-badge img { width: 34px; height: 34px; image-rendering: pixelated; }
.lbp-title { font-size: 29px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.1; margin: 0; }
.lbp-sub { font-size: 13px; color: var(--text-mid); margin: 5px 0 0; }

/* chip tabs, centered */
/* Rail on the left, card on the right. The picker used to live INSIDE the card,
   above the header — 12 chips wrapped 4-4-4, competing with the icon/title/desc
   for the same space and making the top of the page a wall of buttons. Out here
   each board is one line, the hit target is the full width, and the card's
   header gets to be the only thing in it. */
.lbp-layout {
  display: grid;
  grid-template-columns: 208px 1fr;
  gap: 22px;
  /* stretch, NOT start. The rail is pegged again, and a sticky element can only
     travel inside its containing block — with `start` the wrapper shrinks to the
     rail's own height, leaving nowhere to travel and producing the erratic
     placement this page had. Stretching gives the wrapper the full row height. */
  align-items: stretch;
  max-width: 1120px;
  margin: 0 auto;
}
.lbp-main { min-width: 0; }   /* or a long row would blow the grid track out */

/* The grid item. Full height of the row, which is what gives the sticky rail
   inside it somewhere to travel. No appearance of its own. */
.lbp-rail-wrap { min-width: 0; }

.lbp-rail {
  display: flex; flex-direction: column; gap: 3px;
  /* Pegged to the screen so the picker is reachable anywhere down a 100-row
     board. This works now for two reasons that did not hold before:
       1. it sticks inside .lbp-rail-wrap, a full-height grid item, so it has
          room to travel — previously the rail WAS the grid item and had none;
       2. the router puts every view at scroll 0, so it is never measured
          mid-scroll on mount, which is what made it land in odd places.
     max-height + overflow so a long list can still scroll inside the pegged
     column rather than running off the bottom of short viewports. */
  position: sticky; top: 88px;
  max-height: calc(100vh - 108px);
  overflow-y: auto;
  scrollbar-width: none;
}
.lbp-rail::-webkit-scrollbar { width: 0; }
.lbp-tab {
  display: flex; align-items: center; gap: 10px; width: 100%;
  background: none; border: 1px solid transparent;
  color: var(--text-mid); font-family: var(--font); font-size: 13px; font-weight: 600;
  padding: 9px 12px; border-radius: 9px; cursor: pointer; text-align: left;
  /* Only transition color + border (NOT background) — transitioning the
     background caused a dark flash when switching boards. */
  transition: color .14s, border-color .14s;
}
.lbp-tab img { width: 17px; height: 17px; image-rendering: pixelated; opacity: .55; transition: opacity .14s; flex-shrink: 0; }
.lbp-tab-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lbp-tab:hover { color: var(--text); background: var(--surface-3); }
.lbp-tab:hover img { opacity: .95; }
.lbp-tab.active {
  color: #fff;
  background: var(--blue-dim);
  border-color: var(--blue-border);
}
.lbp-tab.active img { opacity: 1; }

/* No room for a rail — it becomes a scrolling strip above the card. */
@media (max-width: 1000px) {
  .lbp-layout { grid-template-columns: 1fr; gap: 14px; }
  .lbp-rail {
    position: static;
    flex-direction: row; overflow-x: auto; gap: 6px;
    padding-bottom: 4px;
    scrollbar-width: none;
    /* Undo the pegged column's constraints — here the rail is a horizontal
       strip, and a vertical max-height would clip it. */
    max-height: none; overflow-y: visible;
  }
  .lbp-rail::-webkit-scrollbar { display: none; }
  .lbp-tab { width: auto; flex-shrink: 0; }
}

/* list */
.lbp-list {
  padding: 10px 12px 8px;
  margin-top: 6px;
}
.lbp-row {
  display: flex; align-items: center; gap: 15px; padding: 11px 14px;
  border-radius: 11px; transition: background .12s;
}
/* Staggered pop-in for the list rows on first load — each row grows its height
   AND fades up, so the panel visibly expands with every player that appears
   (no empty background reserved ahead of time). */
@keyframes lbpRowIn {
  from {
    opacity: 0;
    transform: translateY(6px);
    max-height: 0;
    margin-top: 0; margin-bottom: 0;
    padding-top: 0; padding-bottom: 0;
  }
  to {
    opacity: 1;
    transform: translateY(0);
    max-height: 70px;
    padding-top: 11px; padding-bottom: 11px;
  }
}
.lbp-row-enter {
  animation: lbpRowIn .34s cubic-bezier(0.22,1,0.36,1) both;
  overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
  .lbp-row-enter { animation: none; }
}
/* Alternating row tint so long lists are easier to scan. Odd rows stay clear,
   even rows get a faint fill; hover still lifts above both. */
.lbp-row:nth-child(even) { background: rgba(120,160,235,0.035); }
.lbp-row:hover { background: var(--card-bg-hover); }
.lbp-rank { width: 26px; text-align: center; font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; flex-shrink: 0; }
.lbp-phead { width: 36px; height: 36px; border-radius: 9px; image-rendering: pixelated; background: var(--surface-3); flex-shrink: 0; box-shadow: 0 1px 3px rgba(0,0,0,0.4); }
.lbp-name {
  max-width: 60%; min-width: 0; font-size: 15px; font-weight: 600; color: rgba(255,255,255,0.55); letter-spacing: -0.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lbp-name.clink { cursor: pointer; transition: color .12s; }
.lbp-name.clink:hover { color: #fff; }
.lbp-val { margin-left: auto; font-size: 15.5px; font-weight: 700; color: var(--blue-bright); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; flex-shrink: 0; }
.lb-empty { font-size: 13px; color: var(--text-dim); text-align: center; padding: 40px 0; }

/* Pagination button — floats just below the panel card as a separate control. */
/* Same spacing as .ah-pager (16px above, nothing below) so "View more" sits
   exactly where Prev/Next sits on every other page. The old 14/40 left a visibly
   bigger gap under this one. */
/* Equal air above and below, whichever control is in here — "View more" and the
   next/previous pager share this wrapper, and only having a top margin meant one
   state had a gap under it and the other sat flush against the footer. */
.lbp-more { max-width: 640px; margin: 16px auto 28px; padding: 0 20px; display: flex; justify-content: center; }
.lbp-more-btn {
  display: inline-flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 700;
  color: var(--blue-bright); background: var(--blue-fill); border: 1px solid var(--blue-border);
  font-family: var(--font); padding: 11px 26px; border-radius: 100px; cursor: pointer; transition: all .15s;
}
.lbp-more-btn:hover { background: rgba(91,140,247,0.18); border-color: rgba(91,140,247,0.5); }
.lbp-more-btn svg { transition: transform .18s; }
.lbp-more-btn:hover svg { transform: translateX(3px); }

/* skeleton reuse */
.lbp-row .chead-ph { width: 36px; height: 36px; border-radius: 9px; }
.lbp-row .cname-ph { flex: 1; height: 13px; }
.lbp-row .cval-ph { width: 54px; height: 13px; margin-left: auto; }

@media (max-width: 560px) {
  .lbp-title { font-size: 24px; }
  .lbp-name { font-size: 14px; }
  .lbp-val { font-size: 14px; }
}

/* ============================================================
   Spawner Calculator (/spawner-calc) — spawner-view.js
   Matches the leaderboard panel language: one contained card,
   Sora type, blue accents, pixel mob icons.
   ============================================================ */
.spc-wrap { padding: 0 16px; }

.spc-panel {
  max-width: 760px; margin: 40px auto 0;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 18px;
  padding: 30px 30px 30px;
  box-shadow: 0 1px 0 rgba(255,255,255,0.05) inset, 0 4px 24px rgba(0,0,0,0.5), 0 20px 60px rgba(0,0,0,0.35);
  animation: shellRise .5s cubic-bezier(0.16,1,0.3,1) both;
}
@media (prefers-reduced-motion: reduce) { .spc-panel { animation: none; } }

.spc-head { text-align: center; margin-bottom: 22px; }
.spc-title {
  font-family: var(--font-display); font-weight: 800; font-size: 28px;
  color: var(--text); margin: 0; letter-spacing: -0.02em;
}
.spc-sub { color: var(--text-mid); font-size: 14px; margin: 6px 0 0; }

/* ── Mob picker: 2 rows, wraps naturally, centered ── */
.spc-mobs {
  padding-bottom: 24px; margin: 0 -30px 24px;
  padding-left: 30px; padding-right: 30px;
  border-bottom: 1px solid var(--border);
  box-shadow: 0 4px 8px -4px rgba(0,0,0,0.5);
}
/* Capped to exactly 5 tiles wide (5*88px + 4*12px gap = 488px) so the row
   always breaks after Skeleton and the remaining 4 centre underneath — this
   holds no matter how wide the panel grows. */
.spc-mobs-grid {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 12px;
  max-width: 488px; margin: 0 auto;
}
.spc-mob {
  position: relative;
  width: 88px; height: 88px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  background: var(--card-bg); border: 1px solid var(--border-soft); border-radius: 14px;
  cursor: pointer; font-family: var(--font); color: var(--text-mid);
  transform-origin: center center;
  transition: transform .26s cubic-bezier(0.34,1.3,0.5,1), background .16s, border-color .16s, color .16s, box-shadow .26s;
}
/* Homepage-style hover: springy scale-up + blue glow ring. */
.spc-mob:hover {
  background: var(--card-bg-hover); color: var(--text-mid);
  transform: scale(1.07);
  border-color: var(--blue);
  box-shadow: 0 16px 40px rgba(0,0,0,0.5), 0 0 0 1px var(--blue-border);
  z-index: 3;
}
.spc-mob:hover .spc-mob-icon { opacity: 1; }
.spc-mob:active { transform: scale(1.02); }
.spc-mob-icon {
  width: 42px; height: 42px; image-rendering: pixelated;
  opacity: .72; transition: opacity .16s; pointer-events: none;
}
.spc-mob-label { font-size: 11px; font-weight: 600; transition: color .16s; }
/* Soon tiles get a bit of top room so the corner badge never sits on the icon. */
.spc-mob.soon { opacity: .95; cursor: default; }
.spc-mob.soon:hover { opacity: 1; }
.spc-mob.active {
  background: var(--card-bg-hover); border-color: var(--blue-border);
  color: var(--text); box-shadow: 0 0 0 1px var(--blue-border), 0 6px 18px rgba(0,0,0,0.35);
}
.spc-mob.active:hover { transform: scale(1.07); border-color: var(--blue); }
.spc-mob.active .spc-mob-icon { opacity: 1; }
.spc-mob-soon {
  position: absolute; top: -7px; right: -6px;
  font-size: 8px; font-weight: 700; letter-spacing: .04em;
  color: #fff; text-transform: uppercase;
  background: var(--blue-dim);
  border: 1px solid var(--blue);
  padding: 2px 6px; border-radius: 6px; line-height: 1;
  box-shadow: 0 3px 8px rgba(0,0,0,0.45);
  opacity: .72;
  transition: background .16s, opacity .16s;
  z-index: 4;
}
.spc-mob:hover .spc-mob-soon { background: var(--blue); opacity: 1; }
.spc-mob.active .spc-mob-soon { display: none; }

/* ── Controls: spawners + time, side by side ── */
.spc-controls {
  display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 22px;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 16px;
  padding: 22px 22px 22px; margin-bottom: 26px;
}
.spc-field { display: flex; flex-direction: column; min-width: 0; }
.spc-label {
  font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-dim); margin-bottom: 10px; text-align: center;
}
.spc-hint { font-size: 11px; color: var(--text-dim); margin: 8px 0 0; }

.spc-stepper, .spc-time { display: flex; align-items: stretch; height: 56px; }

.spc-stepper {
  background: var(--page-bg); border: 1px solid var(--border-bright); border-radius: 12px;
  overflow: hidden; transition: border-color .12s, box-shadow .12s;
}
/* Highlight the WHOLE stepper box when the inner field is focused, matching
   the time control — not just the middle input. */
.spc-stepper:focus-within { border-color: var(--blue-border); box-shadow: 0 0 0 2px var(--blue-glow); }
.spc-step {
  width: 46px; flex: 0 0 46px; border: 0; margin: 6px 0 6px 6px;
  background: var(--surface-3); color: var(--text-mid); border-radius: 9px;
  font-family: var(--font); font-size: 24px; font-weight: 700; line-height: 1;
  cursor: pointer; transition: background .12s, color .12s;
}
.spc-step:last-child { margin: 6px 6px 6px 0; color: var(--blue-bright); }
.spc-step:hover { background: var(--surface-4); }
.spc-num {
  flex: 1; min-width: 0; border: 0; background: transparent; text-align: center;
  font-family: var(--font-display); font-weight: 800; font-size: 26px; color: var(--text);
  outline: none; -moz-appearance: textfield;
}
.spc-num::-webkit-outer-spin-button, .spc-num::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* Time = number field + dropdown fused into ONE long bubble (single border,
   divider between the two halves, shared rounded ends). */
/* Time = the same stepper as Amount, with the unit dropdown fused directly
   underneath it (stepper loses its bottom corners, dropdown loses its top). */
/* ── the time control IS the dropdown ────────────────────────────────────────
   One bordered, rounded, clipped shell: stepper on top, unit menu under it, and
   the whole thing grows. Same construction as the AH search bar and the nav
   wallet. The stepper and menu are transparent children, so there are no two
   borders to meet. .spc-time-slot reserves the collapsed height so opening it
   OVERLAYS the Bones total below instead of shoving it down the page. */
.spc-time-slot { position: relative; min-height: 96px; }    /* 56 stepper + 38 head + borders */
.spc-time {
  position: absolute; top: 0; left: 0; right: 0;
  flex-direction: column; height: auto; gap: 0;
  background: var(--page-bg);
  border: 1px solid var(--border-bright);
  border-radius: 12px;
  /* overflow:hidden rounds the menu into the shell's corners properly.
     (clip-path was used here when the tab lived inside the shell; the tab is a
     sibling in the slot now, so nothing gets swallowed and this is cleaner —
     clip-path was leaving the bottom corners looking square.) */
  overflow: hidden;
  box-shadow: none; z-index: 5;
  transition: border-color .12s, box-shadow .12s;
}
.spc-time:focus-within { border-color: var(--blue-border); box-shadow: 0 0 0 2px var(--blue-glow); }
.spc-time.drop-open { border-color: var(--blue-border); }
/* Stepper: transparent child now — the shell owns border/background/radius. */
.spc-time-stepper {
  border: 0; border-radius: 0; background: none; height: 56px;
}
.spc-time-stepper:focus-within { box-shadow: none; }

/* Custom dropdown — fused under the time stepper; expands downward. */
.spc-drop { position: relative; flex: 1; min-width: 0; }
.spc-time .spc-drop {
  /* height:auto is REQUIRED. It was a fixed 52px, which clamped the menu inside
     it — the body grew but the box couldn't, so the shell never expanded and the
     dropdown looked dead. The HEAD carries the collapsed height instead. */
  flex: none; height: auto;
  border: 0; border-top: 1px solid var(--border-bright);
  border-radius: 0; background: var(--surface-3);
  position: static;
}
/* Label centred with the chevron tucked to the right — stacking them made the
   head tall and heavy. The chevron is small and low-contrast until you hover,
   the way donutsmp.bet's wallet does it. */
.spc-time .spc-drop-head {
  border-radius: 0; height: 38px;
  flex-direction: row; justify-content: center; align-items: center; gap: 0;
  text-align: center; position: relative; padding: 0 14px;
}
.spc-time .spc-drop-value {
  flex: 0 0 auto; text-align: center;
  font-size: 16px; font-weight: 700;
  line-height: 1.1;
}
/* Chevron as a little tab hanging off the bottom edge — ported from
   donutsmp.bet's balance card (.hp2-balance-chevron): 36x14 pill,
   border-radius 0 0 20px 20px, no top border, centred. It sits OUTSIDE the
   label instead of crowding it, and it doubles as the affordance that says
   "this opens". */
/* The tab is a SIBLING of the shell (the shell is clipped, so a child would be
   swallowed). Positioned against .spc-time-slot. */
.spc-drop-tab {
  position: absolute; left: 50%; transform: translateX(-50%);
  top: 96px; margin-top: -1px;          /* hangs off the shell's bottom edge */
  /* Smaller again — at 28x11 it was still wide enough to crowd the Bones total
     underneath. */
  width: 22px; height: 9px; padding: 0;
  border-radius: 0 0 20px 20px;
  background: var(--surface-3);
  border: 1px solid var(--border); border-top: none;
  transition: background .18s, border-color .18s, box-shadow .18s, top .42s cubic-bezier(0.4,0,0.2,1);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; z-index: 6;
  transition: background .18s, border-color .18s, box-shadow .18s, top .42s cubic-bezier(0.4,0,0.2,1);
}
.spc-drop-tab .spc-drop-chevron {
  width: 7px; height: 7px; color: rgba(255,255,255,0.38);
  transition: transform .22s cubic-bezier(0.34,1.2,0.64,1), color .15s;
}
.spc-time-slot:hover .spc-drop-tab { background: var(--surface-3); }
.spc-time-slot:hover .spc-drop-tab .spc-drop-chevron { color: rgba(255,255,255,0.7); }
/* Open: ride down with the expanded shell and flip. No glow — it was the loud part. */
/* The tab hangs off the shell's edge, so when the shell lights up (focus or
   open) the tab has to light up with it — otherwise the highlight stops dead at
   the border and the tab reads as a separate sticker. */
.spc-time:focus-within ~ .spc-drop-tab,
.spc-time.drop-open ~ .spc-drop-tab {
  border-color: var(--blue-border);
  background: var(--surface-4);
}
.spc-time-slot.tab-open .spc-drop-tab {
  top: 232px;   /* 96 collapsed + the menu's exact 136px content height */
}
.spc-time-slot.tab-open .spc-drop-tab .spc-drop-chevron { color: rgba(255,255,255,0.7); }
.spc-time-slot.tab-open .spc-drop-tab .spc-drop-chevron { transform: rotate(180deg); }
.spc-time .spc-drop-item { text-align: center; font-size: 15px; }
.spc-time .spc-drop.open { border-color: var(--blue-border); }
/* No radius on open: the head is a flat child inside the shell, and rounding it
   is exactly what curved the highlighted row's corners against the panel. */
.spc-time .spc-drop.open .spc-drop-head { border-radius: 0; }
.spc-drop-head {
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  /* radius:0 — the shell (overflow:hidden) does ALL corner shaping now. The old
     `0 11px 11px 0` was left from when the head was the right half of a fused
     bubble, and it rounded the highlighted row's corners against the shell. */
  background: var(--surface-3); border: 0; border-radius: 0;
  color: var(--text); font-family: var(--font); font-weight: 600; font-size: 16px;
  padding: 0 14px 0 16px; cursor: pointer; text-align: left;
  position: relative; z-index: 2;
  transition: background .14s;
}
.spc-drop:not(.open) .spc-drop-head:hover { background: var(--surface-4); }
.spc-drop.open .spc-drop-head { background: var(--surface-4); border-radius: 0 11px 0 0; }
.spc-drop-value { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.spc-drop-chevron {
  width: 16px; height: 16px; color: var(--blue-bright); flex-shrink: 0;
  transition: transform .22s cubic-bezier(0.34,1.2,0.64,1);
}
.spc-drop.open .spc-drop-chevron { transform: rotate(180deg); }
.spc-drop-body {
  /* In flow inside the shell — growing it grows the whole control. The shell
     owns border/radius/overflow, so there's nothing here to align or clip. */
  position: static; width: 100%;
  /* #0e121b, same as the AH search dropdown: a step darker than the field above
     it. --page-bg made it identical to the stepper, so the menu read as a hole
     rather than a surface. */
  background: #0e121b; border: 0; border-top: 1px solid var(--border-bright);
  border-radius: 0; box-shadow: none;
  display: flex; flex-direction: column; gap: 0; padding: 0;
  max-height: 0; opacity: 0; overflow: hidden; pointer-events: none;
  /* .42s to match the profile dropdown — one curve, one duration. */
  transition: max-height .42s cubic-bezier(0.4,0,0.2,1),
              opacity .32s cubic-bezier(0.4,0,0.2,1);
}
/* max-height must equal the REAL content height (4 units x 41px = 164), not a
   generous guess. At 240 the body stopped growing ~68% through the animation
   while the tab kept travelling for the full .42s — that's the drift. Matching
   them means the shell's edge and the tab move as one. */
.spc-drop.open .spc-drop-body { max-height: 136px; opacity: 1; pointer-events: auto; }

/* While the unit menu is open, dim everything above it. Same idea as the AH
   search blur: the menu is the only thing you're reading. Clicks still land, so
   clicking out closes it. */
/* Everything except the time control dims: header, mob picker, the convert
   column beside it and the Worth block below. */
.spc-head, .spc-mobs, .spc-stage-convert, .spc-worth-block, .spc-flow-sep,
.spc-stage-produce .spc-stage-out,
.spc-produce-controls .spc-field:first-child {
  transition: filter .22s ease, opacity .22s ease;
}
.spc-drop-open .spc-head, .spc-drop-open .spc-mobs,
.spc-drop-open .spc-stage-convert, .spc-drop-open .spc-worth-block,
.spc-drop-open .spc-flow-sep, .spc-drop-open .spc-stage-produce .spc-stage-out,
.spc-drop-open .spc-produce-controls .spc-field:first-child {
  filter: blur(4px); opacity: .5; user-select: none;
}
.spc-drop-item {
  /* Edge-to-edge + square: the shell clips the last item into the corner. */
  display: flex; align-items: center; justify-content: center; gap: 8px;
  position: relative; width: 100%; background: none; border: none;
  color: var(--text-mid); font-family: var(--font); font-weight: 600; font-size: 14px;
  padding: 8px 12px; border-radius: 0; cursor: pointer; text-align: left;
  transition: background .12s, color .12s;
}
/* Full-bleed highlight — the row fills edge to edge and the shell clips the
   last one into the bottom corners, matching the AH dropdown. */
.spc-drop-item:hover { background: var(--surface-3); color: var(--text); }
.spc-drop-item.selected { color: var(--blue-bright); background: var(--blue-fill); }
.spc-drop-check {
  position: absolute; right: 12px; width: 13px; height: 13px;
  opacity: 0; transition: opacity .14s;
}
.spc-drop-item.selected .spc-drop-check { opacity: 1; }

.spc-drop-head:focus-visible, .spc-drop-item:focus-visible, .spc-step:focus-visible, .spc-mob:focus-visible {
  box-shadow: 0 0 0 2px var(--blue-glow); border-radius: 10px;
}
.spc-num:focus { outline: none; box-shadow: none; }

/* ── Output ── */
.spc-output {
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 16px;
  padding: 22px; min-height: 150px;
  box-shadow: 0 6px 20px rgba(0,0,0,0.28);
}
/* Head: icon + context on the left, /sell|/ah method toggle on the right. */
.spc-out-head {
  display: flex; align-items: center; gap: 10px;
  padding-bottom: 16px; margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
}
.spc-out-icon { width: 22px; height: 22px; image-rendering: pixelated; flex-shrink: 0; }
.spc-out-ctx { color: var(--blue-bright); font-size: 13.5px; font-weight: 600; flex: 1; min-width: 0; }

/* Method toggle (/sell | /ah) */
.spc-method {
  display: inline-flex; gap: 4px; flex-shrink: 0;
  background: var(--page-bg); border: 1px solid var(--border); border-radius: 9px; padding: 3px;
}
.spc-method-btn {
  background: none; border: 0; border-radius: 7px; cursor: pointer;
  font-family: var(--font); font-weight: 600; font-size: 13px; color: var(--text-mid);
  padding: 5px 14px; transition: background .12s, color .12s;
}
.spc-method-btn:hover { color: var(--text); }
.spc-method-btn.active { background: var(--blue-dim); color: #fff; }

/* Main row: bones → worth */
.spc-out-main { display: flex; align-items: center; padding: 22px 0 8px; }
.spc-out-cell { flex: 1; text-align: center; min-width: 0; }
.spc-out-arrow { flex: 0 0 auto; color: var(--text-dim); padding: 0 6px; margin-bottom: 22px; }
.spc-out-arrow svg { width: 26px; height: 26px; display: block; }
.spc-out-val {
  font-family: var(--font-display); font-weight: 800; font-size: 44px;
  color: var(--text); line-height: 1; letter-spacing: -0.02em;
  white-space: nowrap;
}
.spc-out-worth { color: var(--green); }
.spc-out-drop { color: var(--text-mid); font-size: 15px; font-weight: 600; margin-bottom: 10px; }

/* Bottom control strip: price + multiplier (or market note) */
.spc-strip {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding-top: 16px; margin-top: 6px; border-top: 1px solid var(--border);
}
.spc-price { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.spc-price-lbl { color: var(--text-mid); font-size: 12.5px; }
.spc-price-val { color: var(--text); font-size: 13px; font-weight: 700; }
.spc-strip-note { color: var(--text-dim); font-size: 12px; text-align: right; }
.spc-mult { display: flex; align-items: center; gap: 12px; }
.spc-mult-lbl { color: var(--text-mid); font-size: 12.5px; white-space: nowrap; }
.spc-mult-field {
  display: flex; align-items: center; height: 34px;
  background: var(--page-bg); border: 1px solid var(--border-bright); border-radius: 9px;
}
.spc-mult-btn {
  width: 30px; height: 26px; margin: 0 4px; border: 0; border-radius: 6px;
  background: var(--surface-3); color: var(--text-mid);
  font-family: var(--font); font-size: 17px; font-weight: 700; line-height: 1; cursor: pointer;
  transition: background .12s, color .12s;
}
.spc-mult-btn:last-child { color: var(--blue-bright); }
.spc-mult-btn:hover { background: var(--surface-4); }
.spc-mult-val {
  width: 52px; min-width: 0; text-align: center; border: 0; background: transparent;
  font-family: var(--font-display); font-weight: 800; font-size: 15px; color: var(--text);
  outline: none; -moz-appearance: textfield;
}
.spc-mult-val::-webkit-outer-spin-button, .spc-mult-val::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

.spc-out-foot { text-align: center; color: var(--gold); font-size: 11px; margin: 12px 0 0; }
.spc-out-note {
  text-align: center; color: var(--text-dim); font-size: 11px;
  margin: 14px 0 0; padding-top: 14px; border-top: 1px solid var(--border-soft);
}

/* "Coming soon" state for mobs without rates */
.spc-soon-note { display: flex; align-items: center; gap: 16px; padding: 8px 4px; }
.spc-soon-icon { width: 46px; height: 46px; image-rendering: pixelated; opacity: .5; flex: 0 0 46px; }
.spc-soon-title { color: var(--text); font-size: 15px; font-weight: 700; margin: 0; }
.spc-soon-body { color: var(--text-mid); font-size: 13px; margin: 4px 0 0; line-height: 1.4; }

.spc-wrap { padding-bottom: 8px; }
.spc-disclaimer {
  max-width: 760px; margin: 14px auto 40px; text-align: center;
  color: var(--text-dim); font-size: 11px; padding: 0 16px;
}
/* Centered variant of the bottom strip (used in /ah mode). */
.spc-strip-center { justify-content: center; }
.spc-strip-center .spc-price { gap: 8px; }

@media (max-width: 560px) {
  .spc-panel { padding: 24px 18px; }
  .spc-mobs { margin: 0 -18px 24px; padding-left: 18px; padding-right: 18px; }
  .spc-mobs-grid { max-width: 428px; }   /* 5*76px tiles + 4*12px gaps */
  .spc-title { font-size: 24px; }
  .spc-controls { grid-template-columns: 1fr; gap: 18px; }
  .spc-mob { width: 76px; height: 76px; }
  .spc-mob-icon { width: 38px; height: 38px; }
  .spc-out-val { font-size: 30px; }
  /* Stack the head (context over the toggle) and the strip on narrow screens. */
  .spc-out-head { flex-wrap: wrap; }
  .spc-out-ctx { flex: 1 1 100%; order: 1; }
  .spc-method { order: 2; margin-top: 10px; }
  .spc-out-arrow svg { width: 20px; height: 20px; }
  .spc-strip { flex-direction: column; align-items: stretch; gap: 12px; }
  .spc-mult { justify-content: space-between; }
  .spc-strip-note { text-align: left; }
}

/* ── Staged flow: PRODUCE → CONVERT → SELL (spawner-view.js v2) ──────────────
   One card holding three stages laid out left-to-right on wide screens, each
   with its own live output pinned to the bottom, joined by flow arrows. Stacks
   vertically (arrows rotate down) on narrow screens. */
.spc-flow {
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 16px;
  padding: 24px; box-shadow: 0 6px 20px rgba(0,0,0,0.28);
}
.spc-stages {
  display: grid;
  grid-template-columns: minmax(0,1fr) auto minmax(0,1fr);
  align-items: stretch;
  /* Full-bleed divider + soft downward shadow under the two columns — the same
     separator language as the mob picker and the leaderboard header. */
  margin: 0 -24px; padding: 0 24px 24px;
  border-bottom: 1px solid var(--border);
  box-shadow: 0 4px 8px -4px rgba(0,0,0,0.5);
}
/* The `hidden` attribute is a UA display:none, which an author display:grid/flex
   silently beats — so hiding these needs an explicit rule. */
.spc-stages[hidden], .spc-soon-note[hidden], .spc-worth-block[hidden] { display: none; }
.spc-stage { display: flex; flex-direction: column; min-width: 0; padding: 2px 18px; }
.spc-stage-produce { padding-left: 2px; }
.spc-stage-convert { padding-right: 2px; }

.spc-stage-label {
  font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--text-dim); margin-bottom: 16px;
}
.spc-stage-opt { font-weight: 600; text-transform: none; letter-spacing: 0; color: var(--text-dim); opacity: .6; }

/* Produce holds the (unchanged) stepper + time controls, stacked. */
.spc-produce-controls { display: flex; flex-direction: column; gap: 14px; }
.spc-produce-controls .spc-label { text-align: left; margin-bottom: 8px; }

/* Clean vertical rule between the two columns (replaced the floating arrow). */
.spc-flow-sep {
  width: 1px; align-self: stretch; margin: 4px 10px 0;
  background: linear-gradient(to bottom,
    transparent, var(--border) 12%, var(--border) 88%, transparent);
  box-shadow: 1px 0 6px -2px rgba(0,0,0,0.55);
}

/* Each stage's live output, pinned to the bottom so the three values align. */
.spc-stage-out { margin-top: auto; padding-top: 18px; text-align: center; }
/* The icon already says what the number is — the word was redundant. */
.spc-stage-out-label { display: none; }
.spc-stage-out-val {
  font-family: var(--font-display); font-weight: 800; font-size: 30px; line-height: 1;
  letter-spacing: -0.02em; color: var(--text); white-space: nowrap;
}
/* Worth — the answer, centred under the two stages and set apart by a divider. */
/* Sits darker than the card so the answer separates instead of blending, and
   runs full-bleed into the card's bottom corners. */
.spc-worth-block {
  /* No top margin: the panel must begin AT the divider, else a strip of
     card-bg shows through and reads as a mis-aligned cutoff. */
  margin: 0 -24px -24px; padding: 22px 24px 18px;
  /* Between --card-bg and --page-bg on purpose: --page-bg made it identical
     to both the chips and the page behind the card. */
  background: rgba(0,0,0,0.28);
  border-radius: 0 0 15px 15px;
  text-align: center;
}
.spc-worth-title {
  color: var(--text-mid); font-size: 12px; font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase; margin-bottom: 10px;
}
/* price bottom-left, data freshness bottom-right */
.spc-worth-foot {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  margin-top: 14px;
}
.spc-worth-price { color: var(--text-dim); font-size: 12px; text-align: left; }
.spc-worth-price.is-est { color: var(--gold); }
.spc-worth-age {
  color: var(--text-dim); font-size: 11px; opacity: .75;
  font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right;
}
.spc-worth-val {
  /* inline-block keeps the [data-tip] hover box tight to the digits; as a
     block it spanned the panel and popped the tooltip from far left. */
  display: inline-block;
  font-family: var(--font-display); font-weight: 800; font-size: 42px; line-height: 1;
  letter-spacing: -0.02em; color: var(--green); white-space: nowrap;
}
.spc-stage-out-note { color: var(--text-dim); font-size: 11.5px; margin-top: 8px; }
.spc-stage-out-note.is-est { color: var(--gold); }

/* Convert chips: 2×2 grid of pill toggles. */
.spc-chips { display: grid; grid-template-columns: 1fr; gap: 8px; }
.spc-chip {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 10px 12px; border-radius: 11px; text-align: center;
  background: var(--page-bg); border: 1px solid var(--border-bright); color: var(--text-mid);
  font-family: var(--font); font-weight: 600; font-size: 12.5px; cursor: pointer;
  transition: background .14s, border-color .14s, color .14s, box-shadow .14s, transform .12s;
}
.spc-chip:hover { background: var(--surface-3); color: var(--text); border-color: var(--blue-border-dim); }
.spc-chip:active { transform: scale(0.98); }
.spc-chip.active {
  background: var(--blue-fill); border-color: var(--blue-border); color: var(--blue-bright);
  box-shadow: 0 0 0 1px var(--blue-border);
}
.spc-chip-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.spc-chip-ico { display: inline-flex; flex-shrink: 0; }
.spc-chip-img { width: 18px; height: 18px; image-rendering: pixelated; display: block; }

/* Item icon leading the converted amount (real MC texture, pixelated). */
.spc-stage-out-row { display: flex; align-items: center; justify-content: center; gap: 9px; }
.spc-item-ico { display: inline-flex; flex-shrink: 0; }
.spc-item-img {
  width: 26px; height: 26px; image-rendering: pixelated; display: block;
}

/* Sell stage header: label + /sell|/ah toggle (reuses .spc-method styles). */
.spc-stage-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 16px; }
.spc-stage-head .spc-stage-label { margin-bottom: 0; }

/* Stack the flow on narrow screens: arrows point down, dividers between rows. */
@media (max-width: 700px) {
  .spc-stages { grid-template-columns: 1fr; }
  .spc-stage { padding: 18px 2px; }
  .spc-stage-produce { padding-top: 2px; }

  .spc-flow-sep {
    width: auto; height: 1px; align-self: auto; margin: 0;
    background: linear-gradient(to right, transparent, var(--border) 12%, var(--border) 88%, transparent);
    box-shadow: 0 1px 6px -2px rgba(0,0,0,0.55);
  }
  .spc-stage-out { margin-top: 16px; }
  .spc-worth-val { font-size: 36px; }
  /* Give the produce controls room to sit side by side again if space allows. */
  .spc-produce-controls { flex-direction: row; gap: 16px; }
  .spc-produce-controls .spc-field { flex: 1; }
}
@media (max-width: 460px) {
  .spc-produce-controls { flex-direction: column; }
}

/* ============================================================
   Auction House (/auction-house) — auction-view.js
   Clean card layout in the site's dark theme.
   ============================================================ */
.ah-wrap { max-width: 980px; margin: 0 auto; padding: 0 24px 60px; }

/* homepage-style search shell reused on the auction + vouches pages.
   .search-shell already provides the homepage's 64px top padding and the
   shellRise staggered entrance — we intentionally don't override it so the
   spacing matches the homepage exactly. */

/* search box locked (guest) + icon */
.ah-search-box.locked { cursor: default; }
.ah-search-ico { width: 18px; height: 18px; color: var(--text-mid); flex-shrink: 0; display: inline-flex; }
.ah-search-ico svg { width: 100%; height: 100%; }
.ah-search-locked-text { flex: 1; color: var(--text-dim); font-size: 14px; font-weight: 600; text-align: left; }

/* search row — reuses the homepage .search-box pill */
/* search row — reuses the homepage .search-box pill */
.ah-search-box { width: 100%; }
.ah-search-box.locked { cursor: default; }
.ah-search-ico { width: 18px; height: 18px; color: var(--text-mid); flex-shrink: 0; display: inline-flex; }
.ah-search-ico svg { width: 100%; height: 100%; }
.ah-search-locked-text { flex: 1; color: var(--text-dim); font-size: 14px; font-weight: 600; }

/* sections */
.ah-section { margin: 30px 0 0; }
.ah-sec-title { font-size: 16px; font-weight: 800; color: var(--text); }
.ah-sec-title-row { display: flex; align-items: baseline; gap: 12px; }
.ah-sec-note { font-size: 11.5px; color: var(--text-dim); }

/* base items — single row, no box: just icon over name */
.ah-base-section { margin-top: 24px; }
.ah-base-row { margin-top: 0; }
.ah-tiles { display: grid; grid-template-columns: repeat(14, 1fr); gap: 8px; }
.ah-tile {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 8px 2px; min-width: 0;
  cursor: pointer; transition: transform .14s;
}
.ah-tile:hover { transform: translateY(-3px); }
.ah-tile:hover .ah-tile-name { color: var(--blue-bright); }
.ah-tile-ico { width: 34px; height: 34px; display: flex; align-items: center; justify-content: center; }
.ah-tile-name {
  font-size: 9.5px; font-weight: 600; color: var(--text-mid); text-align: center;
  line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
  transition: color .14s;
}

/* item images (mc.nerothe.com) render crisp, pixel-art style */
.ah-img {
  width: 32px; height: 32px; object-fit: contain;
  image-rendering: pixelated; image-rendering: crisp-edges;
}
.ah-tile-ico .ah-img { width: 32px; height: 32px; }
.ah-card-ico .ah-img { width: 30px; height: 30px; }
.ah-hero-ico { display: inline-flex; flex-shrink: 0; }
.ah-hero-ico .ah-img { width: 48px; height: 48px; image-rendering: pixelated; }

/* glyph placeholder tile */
.ah-glyph {
  width: 30px; height: 30px; border-radius: 7px;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-3); border: 1px solid var(--border);
  font-size: 10px; font-weight: 800; color: var(--blue-bright); letter-spacing: .5px;
}
.ah-hero-ico .ah-glyph { width: 60px; height: 60px; border-radius: 10px; font-size: 18px; }
.ah-card-ico .ah-glyph, .ah-tile-ico .ah-glyph { width: 30px; height: 30px; }

/* top searched — 2-col cards */
.ah-top-grid { margin-top: 14px; }
.ah-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.ah-card {
  display: flex; align-items: center; gap: 12px; padding: 12px 14px;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 12px;
  cursor: pointer; transition: transform .14s, border-color .14s, background .14s;
}
.ah-card:hover { transform: translateY(-2px); border-color: var(--blue-border); background: var(--card-bg-hover); }
.ah-card-rank { font-size: 13px; font-weight: 800; color: var(--text-dim); width: 26px; flex-shrink: 0; }
.ah-card-ico { flex-shrink: 0; }
.ah-card-main { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.ah-card-compact .ah-card-main { justify-content: center; }
.ah-card-name { font-size: 13.5px; font-weight: 700; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ah-card-sub { font-size: 10px; color: var(--text-dim); }
.ah-card-right { display: flex; flex-direction: column; align-items: flex-end; flex-shrink: 0; }
.ah-card-price { font-size: 15px; font-weight: 800; color: var(--green); }
.ah-chg { font-size: 10.5px; font-weight: 600; margin-top: 2px; }
.ah-chg.up { color: var(--green); }
.ah-chg.down { color: var(--red); }
.ah-chg.flat { color: var(--text-mid); }

/* ── item detail ── */
/* The item page is short (a card + a chart) and was sitting top-aligned with a
   pile of dead space underneath. Centring it in the viewport — minus the header
   and footer — puts the chart where the eye already is.
   min-height, not height: a taller chart or a long name must still push the page
   down rather than overflow a fixed box. */
.ah-item {
  padding-top: 4px;
  min-height: calc(100vh - 210px);
  display: flex; flex-direction: column; justify-content: center;
}
@media (max-height: 700px) { .ah-item { min-height: 0; justify-content: flex-start; } }
.ah-back {
  display: inline-flex; align-items: center; margin: 0;
  color: var(--text-dim); font-size: 12.5px; font-weight: 600; cursor: pointer;
  transition: background .15s;
}
.ah-back:hover { color: var(--text); }
/* The hero is now a ROW, not a card: only .ah-hero-card is boxed, and it sizes
   to its contents rather than stretching the full width. Everything else (the
   track button, the freshness) sits outside it on the page. */
.ah-hero {
  position: relative;
  display: flex; justify-content: flex-start; align-items: center; gap: 16px;
  margin: 40px 0 0; padding: 0;
  background: none; border: 0; border-radius: 0; box-shadow: none;
}
/* The hero card's shadow pointed down and shoved the stats card visually away;
   tightening it lets the two sit close without looking glued. */
.ah-hero-card { box-shadow: 0 6px 18px rgba(0,0,0,0.25) !important; }
/* Back is positioned against the HERO ROW, not the card — that's what keeps it
   out of the card's :hover. The padding is still a real hit area (the text alone
   was a ~50x16 target); it just isn't painted. */
.ah-hero > .ah-back {
  position: absolute; left: -8px; top: -26px; margin: 0;
  padding: 6px 10px; border-radius: 8px;
  background: none;
  z-index: 3;   /* above the card, so its hit area always wins */
}
/* Only Back pops — and it's the inner span that moves, so the padding (the hit
   area) stays put while the label nudges. */
.ah-back-inner {
  display: inline-block;
  transition: transform .16s cubic-bezier(0.34,1.3,0.5,1), color .15s;
}
/* The label nudges and brightens — no background wash. A translucent white block
   behind text on a dark page reads as a rendering artefact, not a hover. */
.ah-hero > .ah-back:hover .ah-back-inner { transform: translateX(-3px); color: var(--text); }
/* padding back to 16: the reserved space was only needed while .ah-hero-kind was
   absolutely positioned. It's in the flow now, so the card sizes to it. */
.ah-hero-card {
  position: relative; z-index: 2;
  display: inline-flex; align-items: center; gap: 18px;
  padding: 16px 26px 16px 20px;
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.28);
  /* No transition either — nothing changes on hover any more, and leaving one
     here would just be a rule waiting to animate the next thing someone adds. */
}
/* No hover state. The card ISN'T interactive — it's a readout — and a lift on
   hover promises a click that does nothing. */
@media (prefers-reduced-motion: reduce) { .ah-hero-card { transition: none; } .ah-hero-card:hover { transform: none; } }
.ah-hero-info { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
/* Name sits ABOVE the price as a quiet caption — the price is the headline, so
   the hierarchy is size, not two competing bold lines. */
.ah-hero-name {
  font-size: 15px; font-weight: 700; color: var(--text-mid);
  letter-spacing: .02em; white-space: nowrap; text-align: center;
}
.ah-hero-info { align-items: center; }
.ah-hero-side { display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
.ah-pill { font-size: 11px; font-weight: 600; padding: 3px 9px; border-radius: 6px; }
.ah-pill.ok { color: var(--green); background: rgba(34,197,94,0.12); border: 1px solid rgba(34,197,94,0.3); }
.ah-pill.warn { color: var(--gold); background: rgba(245,158,11,0.10); border: 1px solid rgba(245,158,11,0.3); }
/* Above the freshness line, outside the card. The rate is the more useful of the
   two, so it takes the higher slot. */
/* What the headline number MEANS. Same shape as the profile view counter: a
   small pill pinned inside the card, qualifying the big number without competing
   with it.
     Current price — enough recent sales to trust it
     24hr average  — thin lately; a day's worth says roughly this
     Asking price  — nobody's buying; a hope, not a price
   The card is position:relative already (the icon glow uses it), so this anchors
   to the card, not the page. */
/* Centred under the price, in the flow. Bare coloured text — no pill: it's a
   caption on the number, and a chip would compete with what it describes. */
.ah-hero-kind {
  display: block; text-align: center; margin-top: 1px;
  font-size: 9.5px; font-weight: 700; letter-spacing: .02em;
  white-space: nowrap;
}
.ah-hero-kind.sold { color: var(--green); }
.ah-hero-kind.avg  { color: var(--blue-bright); }
/* Amber: an asking price is materially weaker evidence and should look like it. */
.ah-hero-kind.ask  { color: #eab308; }

/* Three stacked qualifiers to the right of the card, bottom-up:
     bottom  2px  freshness    "updated 12m ago"
     bottom 20px  price kind   "Current price"
     bottom 40px  sold rate    "1.24K/hr sold"
   They're absolutely positioned so the card can't grow to fit them — its size is
   driven by the price alone. That means the offsets are hand-spaced and have to
   move together; adding a fourth without re-spacing would overlap silently. */
.ah-hero-rate {
  position: absolute; left: calc(100% + 14px); bottom: 22px;
  font-size: 10px; font-weight: 700; color: var(--blue-bright);
  white-space: nowrap;
}
.ah-hero-rate[hidden] { display: none; }

/* Outside the card, at its bottom-right. Absolute so it can't affect its size. */
.ah-hero-fresh {
  position: absolute; left: calc(100% + 14px); bottom: 3px;
  font-size: 12px; color: var(--text-dim); opacity: .85;
  white-space: nowrap; pointer-events: none;
}
.ah-hero-price {
  font-family: var(--font-display);
  font-size: 34px; font-weight: 800; color: var(--green);
  line-height: 1; letter-spacing: -0.02em; white-space: nowrap;
}
.ah-hero-btn {
  height: 40px; padding: 0 18px; border-radius: 9px; border: 0; cursor: pointer;
  font-family: var(--font); font-size: 13px; font-weight: 700; transition: background .15s, opacity .15s;
}
.ah-hero-btn.track { background: var(--blue-dim); color: #fff; }
.ah-hero-btn.track:hover { background: var(--blue); }
.ah-hero-btn.untrack { background: var(--surface-3); color: var(--text-mid); border: 1px solid var(--border); }
.ah-hero-btn.untrack:hover { color: var(--text); border-color: var(--border-bright); }
.ah-hero-btn:disabled { opacity: .6; cursor: default; }

/* chart card — oxlicensing-style, green */
/* Just enough for the switch floating above; the hero card sits right on top.
   Card padding is uniform so the plot's own padL/padR/padB land symmetrically. */
/* 10px: the hero card sits just on top. The switch floats above the card on the
   RIGHT while the hero card is on the LEFT, so they share the band without
   colliding — no need to reserve a full row of height for it. */
/* The switch lives INSIDE the card now, top-right — so padding-top makes room
   for it rather than a negative margin hanging it outside. */
/* 10px: the hero card sits right on top of the chart. The switch floats at -42px
   on the RIGHT while the hero card is on the LEFT, so they share the band without
   colliding — no need to reserve a whole row for it. */
.ah-chart-card {
  margin-top: 10px; padding: 24px;
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: 16px; position: relative;
}
/* Player stats history: the outline of the whole unit (active tab + card) takes
   the SELECTED stat's colour (money green, shards purple, …) via --panel-c set
   in drawSeries, so the panel reads as one colour-coded block. */
.pl-chart-wrap .ah-chart-card { border-color: var(--panel-c, rgba(120,160,235,0.30)); }

/* ── item detail: live sales log UNDER the chart ──────────────────────────── */
/* A full-width ledger of individual sales below the price chart: seller, qty,
   price, time — newest first, prepended live as new sales land. */
.ah-sales {
  margin-top: 14px;
  /* Exact sliding-card look: card-bg, soft border, rounded, float shadow. */
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: 16px; padding: 16px 18px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.45), 0 3px 10px rgba(0,0,0,0.3);
}
.ah-sales-head {
  position: relative;
  display: flex; align-items: center; justify-content: space-between;
  /* Full-width divider under the header (the card's "separate header" look). */
  padding: 0 24px 12px; margin: 0 -18px 12px;
  border-bottom: 1px solid var(--border);
  box-shadow: 0 4px 8px -4px rgba(0,0,0,0.5);
}
.ah-sales-title {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12.5px; font-weight: 700; letter-spacing: 0.02em;
  color: var(--text); text-transform: uppercase;
}
.ah-sales-clock { width: 16px; height: 16px; image-rendering: pixelated; flex-shrink: 0; }
.ah-sales-live {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 10px; font-weight: 600; letter-spacing: 0.03em;
  color: var(--text-dim); text-transform: uppercase;
}
/* "live" pill with a softly pulsing green dot — the AH feed's live language. */
.ah-sales-live {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; font-weight: 600; letter-spacing: 0.03em;
  color: var(--text-dim); text-transform: uppercase;
}
.ah-sales-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--green); box-shadow: 0 0 0 0 rgba(34,197,94,0.5);
  animation: ahSalesPulse 2s ease-out infinite;
}
@keyframes ahSalesPulse {
  0%   { box-shadow: 0 0 0 0 rgba(34,197,94,0.45); }
  70%  { box-shadow: 0 0 0 6px rgba(34,197,94,0); }
  100% { box-shadow: 0 0 0 0 rgba(34,197,94,0); }
}
@media (prefers-reduced-motion: reduce) { .ah-sales-dot { animation: none; } }

.ah-sales-list {
  display: flex; flex-direction: column;
  max-height: 430px; overflow-y: auto;   /* ~13 rows visible, scroll for the rest */
  scrollbar-width: thin; scrollbar-color: var(--border-bright) transparent;
}
.ah-sales-list::-webkit-scrollbar { width: 6px; }
.ah-sales-list::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 3px; }
.ah-sales-list::-webkit-scrollbar-track { background: transparent; }

.ah-sales-row {
  display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 14px;
  position: relative;   /* for the is-new flash overlay */
  padding: 7px 6px; border-radius: 7px;
}
/* Stable stripe — assigned per row on insert (see makeRow/addLive), not by
   position, so a new row arrives already the right colour and nothing re-tints.
   Same tint as the sliding cards. */
.ah-sales-row.is-stripe { background: rgba(120,160,235,0.04); }
/* Seller face — small, rounded, crisp Minecraft pixels. */
.ah-sales-face {
  width: 20px; height: 20px; border-radius: 5px; flex-shrink: 0;
  image-rendering: pixelated; background: var(--card-bg);
}
.ah-sales-who { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.ah-sales-seller {
  font-size: 13px; font-weight: 600; color: rgba(255,255,255,0.55);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ah-sales-seller.ah-feed-link { cursor: pointer; transition: color .12s; }
.ah-sales-seller.ah-feed-link:hover { color: #fff; }
.ah-sales-anon { color: var(--text-dim); font-weight: 500; }
.ah-sales-qty {
  font-size: 11.5px; font-weight: 700; color: var(--blue-bright); flex-shrink: 0;
  font-family: var(--font); font-variant-numeric: tabular-nums;   /* see .ah-sales-price */
}
.ah-sales-money { display: inline-flex; align-items: baseline; gap: 7px; justify-self: end; white-space: nowrap; }
.ah-sales-price {
  font-size: 15px; font-weight: 700; color: var(--green);
  /* Was var(--mono) — JetBrains Mono draws a DOTTED ZERO, which is what made
     "609,000,000" look like it had specks through it. tabular-nums keeps the
     digits monospaced so the column still aligns. */
  font-family: var(--font); font-variant-numeric: tabular-nums;
}
/* Stacked-sale price alternates total <-> per-item. Both spans share one grid
   cell so the box sizes to the WIDER of the two (no jump), right-aligned, and we
   crossfade opacity when the list toggles .show-unit. */
.ah-sales-price-alt { display: inline-grid; justify-items: end; }
.ah-sales-price-alt > span { grid-area: 1 / 1; white-space: nowrap; transition: opacity 0.3s ease; }
.ah-sales-price-alt .ah-sp-unit { opacity: 0; }
.ah-sales-list.show-unit .ah-sales-price-alt .ah-sp-total { opacity: 0; }
.ah-sales-list.show-unit .ah-sales-price-alt .ah-sp-unit { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .ah-sales-price-alt > span { transition: none; }
}
.ah-sales-unit {
  font-size: 10.5px; color: var(--text-dim);
  font-family: var(--font); font-variant-numeric: tabular-nums;   /* see .ah-sales-price */
}
.ah-sales-time { font-size: 10px; color: var(--text-dim); justify-self: end; white-space: nowrap; min-width: 62px; text-align: right; }
.ah-sales-empty { font-size: 13px; color: var(--text-dim); text-align: center; padding: 26px 8px; }

/* A newly-arrived sale flashes a faint green, then settles. Deliberately does
   NOT touch opacity or transform — the row is fully visible at every frame, so
   the animation can never leave it hidden. (The JS also drops the class when the
   flash ends.) */
.ah-sales-row.is-new { animation: ahSalesSlide 1.5s cubic-bezier(0.22,1,0.36,1) both; }
/* The green now READS as the sale arriving: a crisp accent bar on the left edge
   plus a soft green glow that fades left-to-right, so it sits on top of the row's
   stripe instead of washing the whole thing out. */
.ah-sales-row.is-new::before {
  content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: linear-gradient(90deg, rgba(34,197,94,0.22) 0%, rgba(34,197,94,0.04) 35%, rgba(34,197,94,0) 65%);
  box-shadow: inset 2px 0 0 0 var(--green);
  animation: ahSalesFlash 1.5s cubic-bezier(0.32,0,0.24,1) both;
}
@keyframes ahSalesSlide {
  0% { opacity: 0; transform: translateY(-8px); }
  30%, 100% { opacity: 1; transform: translateY(0); }
}
@keyframes ahSalesFlash { 0% { opacity: 0; } 14% { opacity: 1; } 100% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .ah-sales-row.is-new { animation: none; }
  .ah-sales-row.is-new::before { animation: none; opacity: 0; }
}

@media (max-width: 560px) {
  .ah-sales-row { gap: 8px; }
  .ah-sales-time { min-width: 48px; }
  .ah-sales-unit { display: none; }
}

/* The switch sits ABOVE the card, tucked into its top-right — a control FOR the
   card, not content inside it. Absolute, so it can't shift the plot. */
/* The range switch, above the card on the right — where it started. The legend
   moved INSIDE the card (see .ah-legend), so the head holds one thing again. */
.ah-chart-head {
  position: absolute; top: -42px; right: 0;
  margin: 0; padding: 0;
}
/* Player chart: mirror the title. The switch is centered in the right area
   (right of the centered tab strip), pegged to the reserved tab width so it
   never shifts with tab count / tracked state. The AH item chart is unaffected. */
.pl-chart-wrap .ah-chart-head {
  width: calc(50% - var(--tabs-reserve, 467px) / 2);
  justify-content: center;
}
.pl-chart-wrap .ah-range-btn { font-size: 10px; padding: 5px 10px; letter-spacing: .03em; }

/* Chart legend — the same pattern as oxLicensing's dashboard: dot + label,
   opacity .25 when off, a .15s fade. A legend, not buttons: it says "here's what
   the lines mean, and you can turn them off", which is exactly what it does. */
/* Inside the chart card, top-right — over the plot, where a legend belongs: next
   to the lines it names rather than out in the page furniture.
   z-index above the SVG; pointer-events auto so it stays clickable through the
   chart's own hit layer. */
.ah-legend {
  position: absolute; top: 14px; right: 18px; z-index: 4;
  display: inline-flex; align-items: center; gap: 14px;
  pointer-events: auto;
}
.ah-legend-item {
  display: flex; align-items: center; gap: .4rem;
  font-size: .72rem; color: var(--text-dim);
  cursor: pointer; user-select: none;
  opacity: .25;                       /* off by default; .is-on lights it */
  transition: opacity .15s;
}
.ah-legend-item.is-on { opacity: 1; }
.ah-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.ah-legend-dot.sold { background: var(--green); }
.ah-legend-dot.ask  { background: var(--blue-bright); }
.ah-legend-dot.vol  { background: var(--green); }

/* Locked: dimmer than merely "off", and not clickable. Deliberately still
   visible — the point is that the data EXISTS and signing in reveals it. */
.ah-legend-item.is-locked {
  opacity: .18; cursor: not-allowed;
}

/* ── Binder-tab stat selector (player chart) ────────────────────────────────
   Icon-only tabs that stick UP out of the chart card's top edge, like the
   index tabs on a day planner — centered in the band between the title (left)
   and the range switch (right). The active tab is pulled fully out, capped in
   its stat's colour, and merges into the card body. Hovering shows the stat
   name via the shared [data-tip] tooltip. */
#pl-legend {
  position: absolute;
  left: 50%; right: auto; top: auto;
  bottom: 100%;                 /* sit the strip on the card's top edge */
  transform: translateX(-50%);
  margin-bottom: 0;             /* sit ON the card's top edge, NOT over it — so the
                                   card's coloured outline stays continuous beneath
                                   the inactive tabs (only the active tab merges in) */
  display: flex; align-items: flex-end; justify-content: flex-start; gap: 3px;
  /* Reserve the full 10-tab width and left-align within it, so the donut (first
     tab) lands at the same x whether the player is tracked (all tabs) or not
     (donut + add). Untracked then looks like a subset of the tracked layout. */
  min-width: var(--tabs-reserve, 467px);
  z-index: 6; pointer-events: auto;
}
.pl-stat-tab {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 29px; padding: 0;
  /* Inactive: a recessed chip — DARKER than the card so it reads as "behind",
     but lighter than the page so it doesn't disappear into it. Subtle border;
     the bright stat-coloured outline is reserved for the active tab. */
  background: #0e1220;
  border: 1px solid var(--border); border-bottom: none;
  border-top-width: 3px;        /* 3px top always, so colouring it in never shifts */
  border-radius: 11px 11px 0 0;
  cursor: pointer;
  transition: height .18s cubic-bezier(0.22,1,0.36,1), background .18s, border-color .18s, box-shadow .18s;
}
.pl-stat-ic { width: 21px; height: 21px; image-rendering: pixelated; opacity: .5; transition: opacity .18s; }
/* Hover: pop up a little, brighter edge, stat colour previewed on the cap. */
.pl-stat-tab:hover {
  height: 34px;
  background: var(--card-bg);
  border-color: var(--border-bright);
  border-top-color: var(--tab-c, var(--blue-bright));
  box-shadow: 0 -4px 16px rgba(0,0,0,0.28);
}
.pl-stat-tab:hover .pl-stat-ic { opacity: .95; }
.pl-stat-tab.is-on {
  height: 38px;
  margin-bottom: -1px;          /* only the active tab overlaps in, to merge */
  background: var(--card-bg);    /* same fill as the card — flows straight in */
  border-color: var(--panel-c, var(--tab-c, var(--blue-bright)));       /* stat-coloured sides */
  border-top-color: var(--panel-c, var(--tab-c, var(--blue-bright)));   /* and cap — one outline with the card */
}
/* Inactive tabs (and the + tab) float just ABOVE the card's top edge so the
   card's coloured outline runs unbroken beneath them — they never sit on it. */
.pl-stat-tab:not(.is-on) { margin-bottom: 3px; }
.pl-stat-tab.is-on .pl-stat-ic { opacity: 1; }
/* Card-coloured sliver over the card's top border under the active tab, so it
   merges seamlessly into the chart. */
.pl-stat-tab.is-on::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px;
  background: var(--card-bg);
}
/* "Add tracking" (+) tab — untracked players. Display-only for now. It stands a
   little taller than the inactive tabs (but shorter than the active one) so it
   reads as the actionable one. */
.pl-stat-add { height: 33px; }
.pl-stat-add .pl-stat-plus {
  font-size: 20px; font-weight: 300; line-height: 1; color: var(--text-dim);
  transition: color .18s;
}
.pl-stat-add:hover .pl-stat-plus { color: var(--blue-bright); }
/* Pitch text to the right of the + tab (untracked players). Two single-line
   messages that crossfade every 5s. */
.pl-stat-addinfo {
  display: inline-flex; align-items: center; align-self: center;
  margin-left: 11px;
  white-space: nowrap; pointer-events: none;
}
.pl-addinfo-single {
  font-size: 11.5px; font-weight: 500; color: var(--text-dim);
  white-space: nowrap;
}
.pl-addinfo-single b { color: var(--green); font-weight: 800; }

/* Top metric cards acting as selectors (tracked players only). */
/* Top metric cards acting as selectors (tracked players only). Hover matches the
   homepage sliding cards: grow from center, float shadow, bright blue ring. */
.stat-card-link { cursor: pointer; position: relative; transition: transform .2s cubic-bezier(0.22,1,0.36,1), box-shadow .2s ease, background .2s ease; }
.stat-card-link:hover {
  transform: scale(1.06);
  box-shadow: 0 26px 60px rgba(0,0,0,0.6), 0 10px 28px rgba(0,0,0,0.42), 0 0 0 1px var(--blue-border);
  z-index: 3;
}
/* Selected: an outline in the stat's OWN colour, no background fill. It's an
   ::before ring so picking a new stat animates the outline in with a little pop
   rather than snapping. */
.stat-card-link::before {
  content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  box-shadow: inset 0 0 0 1.5px var(--card-c, var(--blue));
  opacity: 0; transform: scale(1.04);
  transition: opacity .2s ease, transform .32s cubic-bezier(0.34,1.5,0.5,1);
}
.stat-card-link.is-selected::before { opacity: 1; transform: scale(1); }

@media (max-width: 640px) {
  .pl-stat-tab { width: 36px; height: 27px; }
  .pl-stat-tab.is-on { height: 34px; }
  /* Not enough width to reserve the full tab row or center the title beside it —
     fall back to a natural centered strip and a left title. */
  #pl-legend { min-width: 0; }
  .pl-chart-title { width: auto; min-width: 0; justify-content: flex-start; }
  .pl-chart-wrap .ah-chart-head { width: auto; justify-content: flex-end; }
  .pl-stat-ic { width: 18px; height: 18px; }
}
/* flex-end: the range switch is the only thing left in the head, so it sits
   top-RIGHT rather than stranded on the left where the metric picker used to be. */
.ah-chart-head { display: flex; align-items: center; justify-content: flex-end; gap: 12px; flex-wrap: wrap; }
.ah-chart-head-left { display: flex; flex-direction: column; gap: 3px; }
.ah-chart-note { font-size: 11px; color: var(--text-dim); }

/* metric dropdown */
.ah-metric { position: relative; }
.ah-metric-btn {
  display: inline-flex; align-items: center; gap: 6px; background: transparent; border: 0;
  color: var(--text); font-family: var(--font); font-size: 15px; font-weight: 700; cursor: pointer; padding: 0;
}
.ah-metric-btn svg { width: 15px; height: 15px; color: var(--text-mid); }
.ah-metric-menu {
  position: absolute; top: 26px; left: 0; z-index: 20; min-width: 150px;
  background: var(--surface-3); border: 1px solid var(--border-bright); border-radius: 10px;
  padding: 5px; box-shadow: 0 12px 34px rgba(0,0,0,0.5);
}
.ah-metric-menu button {
  display: block; width: 100%; text-align: left; background: transparent; border: 0;
  color: var(--text-mid); font-family: var(--font); font-size: 12.5px; font-weight: 600;
  padding: 8px 10px; border-radius: 7px; cursor: pointer;
}
.ah-metric-menu button:hover { background: var(--surface-4); color: var(--text); }
.ah-metric-menu button.on { color: var(--green); }

/* range presets (1D / 7D / 30D / All) */
/* Range switcher — sliding pill. Restyled onto the site's own tokens: the
   hardcoded rgba(255,255,255,…) track and the loud #3b82f6 gradient were lifted
   straight from donutsmpbet and never matched this page. Now it's the same
   surface/border/blue language as the nav, the chips and the pager. */
.ah-chart-range {
  position: relative; display: inline-flex; gap: 0; height: fit-content;
  background: var(--page-bg);
  border: 1px solid var(--border);
  border-radius: 10px; padding: 3px;
  /* Recessed track — the pill sits in it rather than on a flat panel. */
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.4);
}
.ah-range-slider {
  position: absolute; top: 3px; left: 3px; width: 34px;
  height: calc(100% - 6px);
  /* Depth, the way donutsmpbet's signup toggle does it: a lit gradient rather
     than a flat fill, an inset highlight along the top edge to catch the light,
     and a drop shadow so the pill reads as sitting ON the track. Flat colour is
     what made it look like a coloured rectangle instead of a control. */
  background: linear-gradient(160deg, var(--blue), var(--blue-dim) 60%, #1e3a8a);
  border: 1px solid var(--blue-border);
  border-radius: 7px; z-index: 0; pointer-events: none;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.18),
    0 2px 6px rgba(0,0,0,0.35),
    0 0 10px rgba(91,140,247,0.18);
  transition: left .22s cubic-bezier(0.4,0,0.2,1), width .22s cubic-bezier(0.4,0,0.2,1);
}
.ah-range-btn {
  position: relative; z-index: 1;
  border: 0; background: transparent; color: var(--text-dim);
  font-family: var(--font); font-size: 11px; font-weight: 700; letter-spacing: .04em;
  padding: 6px 12px; border-radius: 6px; cursor: pointer;
  white-space: nowrap; user-select: none; transition: color .18s ease;
}
.ah-range-btn:hover:not(.is-active) { color: var(--text); }
.ah-range-btn.is-active { color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.35); }

/* Search button while rate-limited: a muted dark blue, so it stays in the site's
   palette and reads as "resting" rather than erroring. */
.ah-search-box .btn-search.cooling {
  background: linear-gradient(135deg, #1c2a4a, #16213c);
  border: 1px solid var(--blue-border-dim, rgba(120,160,235,0.18));
  color: rgba(150,175,225,0.9);
  box-shadow: none; filter: none; opacity: 1;
  cursor: default;
  font-variant-numeric: tabular-nums;   /* digits don't jitter as it ticks */
  min-width: 72px;
}
.ah-search-box .btn-search.cooling:hover {
  background: linear-gradient(135deg, #1c2a4a, #16213c); transform: none; filter: none;
}

/* Tooltip price in the same green as every other money figure on the site. */
.ah-tip-val { color: var(--green); }

/* Skeleton rows: the list's shape while data lands. "Loading…" text is a
   different height from a list, so the layout jumped twice — into it and out of
   it. These are the same rows, empty. */
.vch-skel-row {
  display: block; height: 46px; border-radius: 10px;
  background: var(--card-bg); border: 1px solid var(--border);
  opacity: .4;
  animation: vchSkel 1.4s ease-in-out infinite;
}
.vch-skel-row + .vch-skel-row { margin-top: 6px; }
@keyframes vchSkel { 0%,100% { opacity: .25; } 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .vch-skel-row { animation: none; } }

/* ── vouches: arrows, not emoji ── */
.vch-ico { width: 11px; height: 11px; flex-shrink: 0; }
.vch-tally {
  display: inline-flex; align-items: center; gap: 3px;
  font-variant-numeric: tabular-nums; font-weight: 700;
}
.vch-tally + .vch-tally { margin-left: 10px; }
.vch-tally.pos { color: var(--green); }
.vch-tally.neg { color: var(--red); }
.vch-cast-btn .vch-ico, .vch-item-badge .vch-ico { margin-right: 5px; }

/* The two columns were never actually laid out — .vch-cols had no rule at all, so
   the sections just stacked and nothing lined up.
   grid with equal tracks + align-items:stretch makes both start AND end together,
   which is the whole point of putting them side by side: they're meant to be read
   against each other. */
.vch-cols {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 22px;
  align-items: stretch;
  margin-top: 28px;
}
.vch-col { display: flex; flex-direction: column; min-width: 0; }
/* One baseline for both heads; the lists below take the slack, so the columns
   also finish level however many rows each has. */
.vch-col > .ah-sec-title-row {
  min-height: 32px;
  display: flex; align-items: center;
  margin: 0 0 10px;    /* tight — the switch should sit just above the card */
}
.vch-col > .vch-lb,
.vch-col > #vch-recent { flex: 1; }
@media (max-width: 860px) {
  .vch-cols { grid-template-columns: 1fr; gap: 30px; }
}

/* Recent vouches: avatar | name over voucher | verdict + time */
.vch-feed-row { display: flex; align-items: center; gap: 11px; }
.vch-feed-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.vch-feed-name {
  font-size: 13px; font-weight: 700; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vch-feed-by {
  font-size: 10.5px; color: var(--text-dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vch-feed-right { display: flex; align-items: center; gap: 9px; flex-shrink: 0; }
/* Solid, not washed out. At 14% alpha on a dark card the fill was almost
   invisible and the arrow read as grey — the one thing in the row you need to
   catch at a glance. Full-strength icon, a fill you can actually see, and a ring
   to give it an edge. */
.vch-feed-chip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: 7px;
  flex-shrink: 0;
}
.vch-feed-chip .vch-ico { width: 13px; height: 13px; }
.vch-feed-chip.pos {
  background: rgba(34,197,94,0.16); color: var(--green);
  box-shadow: inset 0 0 0 1px rgba(34,197,94,0.34);
}
.vch-feed-chip.neg {
  background: rgba(239,68,68,0.16); color: var(--red);
  box-shadow: inset 0 0 0 1px rgba(239,68,68,0.34);
}

/* Discord Bot pill. Lives in nav-right — NOT in nav-links — because the navbar
   is a `1fr auto 1fr` grid and the middle track sizes to its contents, so a pill
   in there drags the whole nav off-centre.
   `margin-right: auto` parks it at the START of the right track, i.e. tucked up
   against the centre links rather than crowding the account. */
/* margin: 0 auto — equal auto margins centre it in whatever space is left
   between the nav links and the account, rather than hugging either. */
.nav-pill {
  display: inline-flex; align-items: center; gap: 7px;
  margin: 0 auto;
  padding: 5px 10px 5px 9px; border-radius: 9px;
  background: linear-gradient(180deg, rgba(255,255,255,0.03), transparent);
  background-color: var(--surface-3);
  border: 1px solid var(--border);
  color: var(--text-mid); font-size: 12.5px; font-weight: 600;
  white-space: nowrap;
}
.nav-pill svg,
.nav-pill img { width: 18px; height: 18px; flex-shrink: 0; fill: currentColor; }
.nav-pill-tag {
  font-size: 8.5px; font-weight: 800; letter-spacing: .06em;
  padding: 2px 5px; border-radius: 5px;
  background: var(--surface-4); color: var(--text-dim);
  line-height: 1;
}

/* Not built yet: dimmed, but not drained. grayscale(1) killed the logo's colour
   entirely, which read as broken rather than pending — opacity alone says
   "inactive" while leaving the icon recognisable. No hover state: a hover
   response on something you can't click is a lie about what it does. */
.nav-pill.is-soon {
  cursor: default; opacity: .62;
  pointer-events: none;
}
.nav-pill.is-soon .nav-pill-text { color: var(--text-dim); }

/* The live version of the pill. Same shape as its inert sibling, but it now
   behaves like something you can press: it lifts on hover and takes the brand
   blue, so nobody has to guess whether it does anything. */
.nav-pill-link {
  text-decoration: none; cursor: pointer;
  /* Sized to sit level with the account bubble beside it rather than being a
     little shorter: same 34px height and 10px radius the nav's other controls
     use, with even padding either side of the icon. */
  height: 34px; padding: 0 13px 0 11px; gap: 8px; border-radius: 10px;
  font-size: 12.5px; font-weight: 600; letter-spacing: .01em;
  /* Keep the base rule's `margin: 0 auto`. It is what holds the pill in the gap
     between the nav links and the account bubble — overriding it let the pill
     collapse against the account menu and read as part of it. */
  transition: border-color .16s ease, background-color .16s ease,
              color .16s ease, transform .12s ease;
}
.nav-pill-link img { width: 17px; height: 17px; }
.nav-pill-link:hover {
  background-color: var(--card-bg-hover);
  border-color: var(--blue-border);
  color: var(--text);
}
.nav-pill-link:hover .nav-pill-text { color: var(--blue-bright); }
.nav-pill-link:active { transform: translateY(1px); }
.nav-pill-link:focus-visible { outline: 2px solid var(--blue-bright); outline-offset: 2px; }
/* Below the leaderboard breakpoint the nav is tight — keep the icon, drop the
   words, so it stops competing with the links for room. */
@media (max-width: 900px) {
  .nav-pill-link { padding: 0 10px; }
  .nav-pill-link .nav-pill-text { display: none; }
}

/* Identical row to its neighbours (it matches the rule above now) — only dimmed
   and inert. */
.nav-dropdown-menu .nav-soon {
  opacity: .38; cursor: default; pointer-events: none;
}

/* Vouch pills are links now — they carry data-href to /vouches/<name>. */
.pill-link { cursor: pointer; transition: filter .14s, transform .14s; }
.pill-link:hover { filter: brightness(1.25); transform: translateY(-1px); }
/* A zero count shouldn't shout as loudly as a real one. */
.pill.pill-zero { opacity: .5; }

/* Balance history on the profile. Reuses .ah-chart-card wholesale — same skin,
   same range pill, same everything — and just needs a title, spacing, and room
   for the floating switch above it. */
.pl-chart-wrap {
  --tabs-reserve: 467px;
  position: relative;
  margin: 44px 0 64px;      /* bottom gap keeps it off the footer */
}
.pl-chart-wrap[hidden] { display: none; }
/* Straddles the card's top edge — the page background shows through the gap in
   the border, the way donutsmp.bet mounts its headings on a panel. */
/* Straddling the card's border needed an opaque background to punch through it —
   which on a dark page is a black rectangle sitting in the middle of the card.
   The effect only works when the page behind is a flat colour; yours isn't.
   So: sit the title ABOVE the card instead. No mask, no block, nothing to
   mismatch. */
.pl-chart-title {
  position: static;
  transform: none;
  margin: 0 0 12px;
  font-family: var(--font-display);
  font-size: 16px; font-weight: 800; letter-spacing: -0.01em;
  color: var(--text);
  background: none;
  padding: 0;
  text-align: left;
  white-space: nowrap;
}
/* The title sits above the card on the LEFT; the switch floats at -42px on the
   RIGHT. They share that band — which only works because they're on opposite
   sides. The padding-top on the wrap is what reserves it. */
.pl-chart-card { margin-top: 0; }
.pl-chart-wrap { padding-top: 6px; }
.pl-chart-title { min-height: 32px; display: flex; align-items: center; justify-content: center; width: calc(50% - var(--tabs-reserve, 467px) / 2); min-width: 150px; }

/* ── live sales feed ────────────────────────────────────────────────────────
   Only transform + opacity animate. Animating height/top would make the browser
   re-layout the page on every frame; these two run on the GPU and touch nothing
   else — which is the difference between smooth and janky on a mid-range phone. */
.ah-feed-section { margin-top: 34px; }
.ah-feed-live {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; color: var(--text-dim); text-transform: uppercase;
  letter-spacing: .08em; font-weight: 700;
}
.ah-feed-dot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--green);
  animation: feedPulse 2s ease-in-out infinite;
}
@keyframes feedPulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }

/* A rail down the LEFT, outside the page's flow.
   position:absolute takes it out of the document entirely, so the centred column
   can't feel it — no margin, no grid track, nothing to knock the layout off
   centre. It hangs off .ah-shell's left edge and spans its full height, so it
   runs from the title down to the pager exactly as the content does. */
/* The rail is absolute against this. Without a positioned ancestor it would hang
   off the viewport instead and drift as the page scrolls. The AH overview's wrap
   is plain `.ah-wrap` — `.ah-shell` doesn't exist on it, so that selector would
   have matched nothing and the rail would have been positioned against the page. */
.ah-wrap:has(> .ah-feed-section) { position: relative; }
.ah-feed-section {
  position: absolute;
  /* top:0 puts the rail's "Live sales" heading on the same line as the page's
     own title. The HEIGHT is set in JS from the last card's bottom edge — the
     pager comes and goes with the page count, so a fixed inset would be wrong
     half the time. */
  top: 0;
  /* Pin to the VIEWPORT's left edge, not to the content's. `50% - 50vw` walks
     out to the window edge whatever the wrap's width is, so the rail tucks into
     the margin instead of crowding the column. */
  left: calc(50% - 50vw + 18px);
  width: 118px;
  margin: 0; padding: 0;
}
.ah-feed-section .ah-sec-title-row { padding: 0 0 0 2px; margin-bottom: 8px; }
.ah-feed-section .ah-sec-title { font-size: 11px; letter-spacing: .04em; opacity: .75; }
/* Below this the margin is too thin to hold the rail without crowding. */
@media (max-width: 1240px) { .ah-feed-section { display: none; } }

/* The column is what moves: a tile is inserted at the top, the whole column
   shifts up by its height, then animates back to 0 — so every tile glides down
   together as ONE transform. The clip hides the incoming tile behind the edge.
   sticky: the rail follows as you scroll, so the feed stays beside whatever
   you're actually reading rather than scrolling away at the top. */
/* NOT sticky. Sticky pinned it to the window so it hung there while the page
   moved underneath — which is what made it feel bolted to the screen rather than
   part of the page. Static means it scrolls away with everything else, and the
   rail spans the content's full height so there are tiles the whole way down. */
.ah-feed-clip {
  position: relative;
  overflow: hidden;
  height: 100%;
  /* Horizontal padding so a hovered tile's 1.08 scale isn't clipped. */
  padding: 4px 6px 8px;
  margin-left: -6px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 24px, #000 calc(100% - 60px), transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0, #000 24px, #000 calc(100% - 60px), transparent 100%);
}
.ah-feed {
  display: flex; flex-direction: column; gap: 8px;   /* == FEED_GAP_PX in the JS */
  will-change: transform;
  transform: translateZ(0);   /* one compositor layer for the whole column */
}
/* Square-ish tiles. flex-shrink:0 matters twice over here: the strip is wider
   than the viewport, so without it flexbox would squeeze every tile to fit and
   the measured width the ticker slides by would be a lie. */
/* The wrapper is what the strip lays out and what the ticker measures — the card
   plus the timestamp under it. flex-shrink:0 matters here: the strip is wider
   than the viewport, so without it flexbox would squeeze each item and the width
   the ticker slides by would be wrong. */
.ah-feed-item {
  flex: 0 0 auto; flex-shrink: 0;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  /* Hover scales the WRAPPER, so the timestamp rides along with the card instead
     of sitting still underneath it. transform doesn't affect layout, so this
     can't disturb the strip's measured widths or fight the ticker's transform. */
  transform-origin: center;
  transition: transform .22s cubic-bezier(0.34,1.3,0.5,1);
}
.ah-feed-item:hover { transform: scale(1.08); z-index: 2; }
.ah-feed-item:hover .ah-feed-time { color: var(--text-mid); opacity: 1; }
.ah-feed-card {
  position: relative;
  width: 100%; height: auto;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  /* height:auto + real padding instead of a fixed box. The card was 84px tall
     holding ~55px of content, so a third of it was dead space. Let it size to
     what's in it. */
  gap: 3px; padding: 9px 7px 8px;
  /* Lit from above without going plastic: a soft top sheen, a hairline highlight
     on the top edge only (that single line is what sells "raised"), and a
     two-stage shadow — a tight contact shadow plus a wider ambient one, the way
     a real object sits on a surface. */
  background: linear-gradient(168deg, rgba(255,255,255,0.055) 0%, rgba(255,255,255,0.012) 42%, transparent 70%), var(--card-bg);
  border: 1px solid var(--border-bright);
  border-radius: 13px;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.06),
    0 1px 2px rgba(0,0,0,0.4),
    0 6px 18px rgba(0,0,0,0.42);
  cursor: pointer;
  /* Same grow-from-centre as the homepage cards. The strip's 8px gap means a
     scaled tile has room without pushing its neighbours — and scale is a
     compositor property, so it can't fight the ticker's transform. */
  transition: border-color .18s, box-shadow .22s;
}
/* The wrapper does the scaling now — the card only lights up. */
.ah-feed-item:hover .ah-feed-card {
  border-color: var(--blue-border);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.10),
    0 2px 4px rgba(0,0,0,0.45),
    0 12px 30px rgba(0,0,0,0.5);
}
/* Only the name is clickable-through to the player; the rest of the tile is the
   item link. */
/* Skeleton tiles: the strip's shape while the first batch is in flight, so it's
   never an empty hole. Deliberately inert — no border, no shimmer chasing the
   eye; it should read as "loading", not as content. */
.ah-feed-skel .ah-feed-card {
  background: var(--card-bg);
  border-color: var(--border);
  box-shadow: none;
  opacity: .45;
  animation: feedSkel 1.4s ease-in-out infinite;
}
.ah-feed-skel .ah-feed-card:first-child { animation: feedSkel 1.4s ease-in-out infinite; }
@keyframes feedSkel { 0%,100% { opacity: .30; } 50% { opacity: .5; } }
@media (prefers-reduced-motion: reduce) { .ah-feed-skel .ah-feed-card { animation: none; } }

/* The enchantment glint, as the game draws it: a purple sheen that sweeps
   diagonally across the item, over and over. Minecraft's is a scrolling texture
   masked to the item's shape; this is a moving gradient with mix-blend-mode so it
   lights the icon rather than sitting on top of it as a purple film.
   Sweeping, not static — the movement is the whole signature. */
/* The glint must be exactly the icon's size. .ah-feed-ico is an inline-flex box
   that was bigger than the 22px texture inside it, so `inset: 0` painted a purple
   rectangle around the item rather than over it. Sizing the box to the icon makes
   the overlay land on the pixels. */
.ah-feed-card.has-detail .ah-feed-ico {
  position: relative; overflow: hidden;
  display: block; width: 22px; height: 22px;
  border-radius: 3px;
}
.ah-feed-card.has-detail .ah-feed-ico::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(
    115deg,
    transparent 20%,
    rgba(200,120,255,0.55) 42%,
    rgba(232,190,255,0.85) 50%,
    rgba(200,120,255,0.55) 58%,
    transparent 80%
  );
  background-size: 260% 100%;
  mix-blend-mode: screen;        /* lights the pixels, doesn't wash them out */
  animation: mcGlint 2.6s linear infinite;
  pointer-events: none;
}
@keyframes mcGlint {
  from { background-position: 160% 0; }
  to   { background-position: -60% 0; }
}
/* No purple border — the glint on the icon is the signal, and tinting the whole
   card made every enchanted sale shout. */
@media (prefers-reduced-motion: reduce) {
  .ah-feed-card.has-detail .ah-feed-ico::after { animation: none; opacity: .5; }
}

/* No underline — the tile is already a link, and an underline inside it reads as
   a second, competing one. Weight is enough of a signal.
   font-weight is pre-set to 600 so the 700 hover doesn't change the text's width
   and nudge the layout. */
.ah-feed-link { cursor: pointer; font-weight: 600; transition: color .14s, font-weight .14s; }
.ah-feed-link:hover { color: var(--blue-bright); font-weight: 700; text-decoration: none; }
/* position:relative so the count anchors to the icon. The count is absolute, so
   it is OUT OF FLOW and cannot change this box's size — the icon renders at 28px
   whether the stack is 1 or 64. Fixed width/height on the img makes that a
   guarantee rather than an accident. */
.ah-feed-top { position: relative; display: inline-flex; margin-bottom: 2px; }
.ah-feed-ico .ah-img { width: 28px; height: 28px; image-rendering: pixelated; display: block; }
.ah-feed-ico .ah-glyph { width: 28px; height: 28px; font-size: 10px; border-radius: 6px; }
/* Quantity rides the icon's corner rather than taking a line of its own — a
   square has no room to spare. */
/* Minecraft's own stack count: white text hard-shadowed down-right, sitting on
   the item's bottom-right corner. No bubble — the bubble was a UI convention
   from a different game. This is what a stack of 64 actually looks like. */
.ah-feed-qty {
  position: absolute; right: -5px; bottom: -4px;
  font-size: 12px; font-weight: 700; line-height: 1;
  color: #fff;
  /* The 1px hard offset (not a blur) is what makes it read as Minecraft. */
  text-shadow: 1.5px 1.5px 0 rgba(0,0,0,0.85);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
/* .in is kept so the markup stays the same; it's a no-op now. */
.ah-feed-card.in { opacity: 1; }
/* The newest card flashes so your eye catches the arrival. Animates border-color
   and box-shadow ONLY — never `background`: with fill-mode:both the final
   keyframe sticks forever, and a `background` in there permanently overrode the
   card's own gradient. An animation's end state outlives the animation.
   The glow makes it findable at a glance without being a strobe. */
.ah-feed-item:first-child .ah-feed-card { animation: feedNew 1.4s cubic-bezier(0.16,1,0.3,1) both; }
@keyframes feedNew {
  0% {
    border-color: var(--blue-bright);
    box-shadow: 0 0 0 3px rgba(91,140,247,0.28), 0 6px 20px rgba(91,140,247,0.30);
  }
  55% {
    border-color: var(--blue-border);
    box-shadow: 0 0 0 1px rgba(91,140,247,0.10), 0 4px 14px rgba(0,0,0,0.35);
  }
  100% {
    border-color: var(--border);
    box-shadow: 0 4px 14px rgba(0,0,0,0.35);
  }
}
.ah-feed-ico { display: inline-flex; flex-shrink: 0; }
/* The headline: it's what the tile is FOR. */
.ah-feed-price {
  font-family: var(--font-display);
  font-size: 17px; font-weight: 800; color: var(--green);
  font-variant-numeric: tabular-nums; line-height: 1.1;
  letter-spacing: -0.02em;
}
/* The item name lives in the tooltip — at 118px it would truncate to noise, and
   the icon already says what it is. */
/* Pinned to the card's bottom edge — absolute, so it can't pull the icon and
   price off-centre above it. */
.ah-feed-seller {
  position: absolute; left: 8px; right: 8px; bottom: 8px;
  text-align: center;
  font-size: 10px; color: var(--text-mid);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Beneath the card, not inside it — it's a caption, not content. */
.ah-feed-time {
  font-size: 9.5px; color: var(--text-dim); opacity: .6;
  font-variant-numeric: tabular-nums; letter-spacing: .02em;
  line-height: 1;
}
@media (prefers-reduced-motion: reduce) {
  .ah-feed { transition: none !important; transform: none !important; }
  .ah-feed-card:first-child { animation: none; }
  .ah-feed-dot { animation: none; }
}
/* Icons match the rest of the page (tiles are 32px, top-viewed cards 30px) —
   they were 22px here, which made the rail look like a different site. */
.ah-feed-item { width: 100%; }
.ah-feed-ico .ah-img { width: 30px; height: 30px; }
.ah-feed-ico .ah-glyph { width: 30px; height: 30px; font-size: 9px; }
.ah-feed-card.has-detail .ah-feed-ico { width: 30px; height: 30px; }
.ah-feed-price { font-size: 14px; }
.ah-feed-qty { font-size: 11px; right: -5px; bottom: -4px; }
.ah-feed-time { font-size: 8.5px; }
.ah-feed { gap: 6px; }

/* The seller sits in the flow now, not pinned — a fixed box needed padding
   reserved for it, which is where the dead space came from. */
.ah-feed-seller {
  position: static;
  /* display:block is load-bearing. It's a <span> — inline elements ignore width,
     max-width AND overflow, so the ellipsis rule below silently did nothing and
     long names just pushed the card wider. */
  display: block; width: 100%; max-width: 100%;
  font-size: 9px; color: var(--text-mid);
  /* Names cap at 16 chars; ~13 fit here. Longer ones truncate rather than wrap,
     which would make that one card taller than its neighbours. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  text-align: center;
}

/* Pager under the top-viewed cards. Same button language as the rest of the
   site: dark surface, subtle border, blue text on hover. */
/* Pager under the top-viewed cards. Styled as .lcard-more ("View more" on the
   homepage cards): blue gradient, blue border, and the arrow slides on hover. */
.ah-pager {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  margin-top: 16px;
}
.ah-pager[hidden] { display: none; }
.ah-page-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  background: linear-gradient(180deg, rgba(91,140,247,0.16), rgba(91,140,247,0.08));
  border: 1px solid var(--blue-border); color: var(--blue-bright);
  font-family: var(--font); font-size: 12.5px; font-weight: 700; cursor: pointer;
  padding: 10px 18px; border-radius: 10px;
  min-width: 104px;                    /* both buttons stay the same size */
  transition: background .16s, border-color .16s;
}
.ah-page-btn svg { width: 14px; height: 14px; transition: transform .18s ease; }
.ah-page-prev svg { transform: scaleX(-1); }    /* same arrow, mirrored */
.ah-page-btn:hover {
  background: linear-gradient(180deg, rgba(91,140,247,0.28), rgba(91,140,247,0.16));
  border-color: rgba(91,140,247,0.5);
}
.ah-page-next:hover svg { transform: translateX(4px); }
.ah-page-prev:hover svg { transform: scaleX(-1) translateX(4px); }
/* Unavailable direction: invisible but still occupying its slot, so the other
   button never moves. */
.ah-page-btn.is-off { visibility: hidden; pointer-events: none; }

/* View count beside the price on Top viewed — mirrors .view-count on the stats page. */
/* Pegged to the card's top-right and overhanging the edge — same treatment as
   the "SOON" badge on the spawner calculator's mob tiles. */
.ah-card { position: relative; }
.ah-card-views {
  position: absolute; top: -7px; left: -6px; z-index: 3;
  display: inline-flex; align-items: center; gap: 3px;
  font-size: 9px; font-weight: 700; color: var(--text-dim);
  background: #0b0e16;                       /* darker than the card so it recedes */
  border: 1px solid var(--border);
  padding: 2px 6px; border-radius: var(--radius-pill); line-height: 1;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 3px 8px rgba(0,0,0,0.45);
}
.ah-card-views svg { width: 9px; height: 9px; opacity: .8; }
.ah-card-views[data-tip] { cursor: help; }
.ah-card-views svg { width: 10px; height: 10px; opacity: .8; }
.ah-card-right { display: inline-flex; align-items: center; }

/* ── item autocomplete popup (AH search) ── */
/* z-index 140 is REQUIRED, and it must live here rather than on the popup.
   .search-box runs the shellRise animation with fill-mode:both, whose final
   keyframe is transform: translateY(0). A computed transform other than `none`
   creates a stacking context, which trapped .ah-ac's z-index inside this box —
   so the base-items section (later in the DOM) painted OVER the popup. The
   blurred tiles sitting on top were what made the panel look see-through, and
   they swallowed clicks on the rows underneath.
   140 keeps it under the sticky header (150) but above the page sections. */
.ah-search-box { position: relative; z-index: 140; }
/* Fused to the panel while open, exactly how .nav-wallet-head meets
   .nav-wallet-body: the head squares its bottom, the body drops its top border,
   and the head's bottom border becomes the divider between them.
   The 28px is the important part. The bar is 57px tall, so --radius-pill (100px)
   is ALREADY clamped by the browser to 28.5px. Writing `100px 100px 0 0` makes
   the left side sum 100 > 57, which rescales the top corners UP to 57px — a
   semicircle. That was the "clipping" glitch. Naming 28px keeps the top pixel-
   identical to the pill and only squares the bottom. */
/* ── the search bar IS the dropdown ───────────────────────────────────────
   One bordered, rounded, clipped shell containing the input row and the
   results. It just gets taller. There is no second element, so there's nothing
   to align, no borders to join, and no corner/panel timings to reconcile —
   every one of those bugs was a symptom of faking one UI with two.

   The radius never changes: 28px on the 57px collapsed bar renders as a pill,
   and on the expanded shell as a rounded rect. Same value, both states. */
.ah-search-slot {
  position: relative;
  z-index: 140;               /* under the sticky header (150), over the page */
  min-height: 57px;           /* reserves the collapsed height so growth overlays */
  /* The slot carries the bar's width, since the shell inside it is absolute.
     Same 460px as .search-box on the homepage — I'd overridden it to none,
     which is what stretched the bar full-width. */
  max-width: 460px; margin: 0 auto;
}
.search-box.ah-search-box {
  position: absolute; top: 0; left: 0; right: 0;
  display: block; padding: 0;          /* the row owns the padding now */
  overflow: hidden;                    /* clips the results to the shell's radius */
  border-radius: 28px;
}
.ah-search-row {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 20px;
}

/* Only the two browse lists dim — header, title, subtext and footer never do.
   pointer-events:none stops a blurred tile firing a hover tooltip over the popup. */
.ah-blurable { transition: filter .18s ease, opacity .18s ease; }
.ah-blurable.is-blurred {
  /* Dimmed but still CLICKABLE. pointer-events:none swallowed the mousedown
     entirely, so a rate-limited user couldn't open a base/top item at all —
     even though item pages read stored data and never touch the API. */
  filter: blur(4px); opacity: .5; user-select: none;
}
@media (prefers-reduced-motion: reduce) { .ah-blurable { transition: none; } }

/* Results: in normal flow inside the shell, so growing them grows the shell.
   No border, no radius, no shadow of its own — the shell provides all of that. */
.ah-ac {
  background: #0e121b;                 /* darker than the bar, inside one border */
  display: flex; flex-direction: column; gap: 0;
  /* NO padding: rows run edge-to-edge so the last row's hover fills the panel's
     bottom corners completely. The shell is overflow:hidden with a 28px radius,
     so it clips that fill into the curve for free — no "last row" special case
     and nothing to keep in sync if the radius ever changes. Insetting the rows
     always leaves a sliver of panel showing around the bottom corners. */
  padding: 0; text-align: left;
  /* Never scrolls: results are capped at 8, and max-height fits all 8 exactly. */
  max-height: 0; overflow: hidden; pointer-events: none;
  /* Every property uses the SAME curve and duration. max-height was on
     cubic-bezier while padding was on `ease` — the two finished at different
     rates, which is the stutter in the first/last few frames. */
  transition: max-height .26s cubic-bezier(0.4,0,0.2,1),
              border-color .26s cubic-bezier(0.4,0,0.2,1);
  border-top: 1px solid transparent;
}
.ah-ac.open {
  max-height: 360px; pointer-events: auto;
  border-top-color: var(--border);     /* the divider between row and results */
}
@media (prefers-reduced-motion: reduce) { .ah-ac { transition: none; } }
.ah-ac-row {
  display: flex; align-items: center; gap: 10px; width: 100%;
  /* Square + full-bleed: the shell's radius does all the corner shaping. */
  background: none; border: 0; border-radius: 0; cursor: pointer;
  padding: 10px 20px; text-align: left;
  font-family: var(--font); color: var(--text-mid);
  transition: background .12s, color .12s;
}
.ah-ac-row:hover, .ah-ac-row.on { background: var(--surface-3); color: var(--text); }
.ah-ac-row .ah-img, .ah-ac-row .ah-glyph { pointer-events: none; }
.ah-ac-row .ah-img { width: 22px; height: 22px; image-rendering: pixelated; flex-shrink: 0; }
.ah-ac-row .ah-glyph { width: 22px; flex-shrink: 0; font-size: 10px; color: var(--text-dim); }
.ah-ac-name {
  font-size: 13.5px; font-weight: 600; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}

/* legend */
.ah-chart-legend { display: flex; align-items: center; gap: 6px; justify-content: flex-end; margin: 6px 2px 2px; font-size: 11px; color: var(--text-mid); }
.ah-legend-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); display: inline-block; }

/* plot */
/* The svg is pulled OUT of the card on the left and bottom so the axis labels,
   drawn at x=-12 and y=H+20, land outside the card's border — easier to read
   than numbers crowded against the plot. overflow:visible on both the svg and
   the card is what lets them escape. */
.ah-chart-body { position: relative; height: 300px; margin-top: 10px; }
.ah-svg { width: 100%; height: 300px; display: block; overflow: visible; }

/* Volume bars — oxLicensing's treatment: a muted fill that brightens on hover,
   flat bottoms, 3px rounded tops. Bars because a per-hour COUNT is a discrete
   total; a line between two of them draws moments that never happened. */
.ah-bar {
  fill: rgba(34,197,94,0.7);
  transition: fill .12s;
}
.ah-svg:hover .ah-bar { fill: rgba(34,197,94,0.9); }
.ah-chart-card { overflow: visible; }   /* the switch floats above it */
/* oxlicensing's exact grid: SOLID rgba(37,40,54,.8) at 1px, and NO vertical
   lines at all (its x grid is display:false). The dashes were the noisy part —
   a dashed lattice competes with the data; a plain rule just gives you a level
   to read against. */
.ah-grid { stroke: rgba(37,40,54,0.8); stroke-width: 1; }
/* Baseline: the value the range STARTED at. Brighter than a gridline so it reads
   as a reference, dashed so it never competes with the data line. */
.ah-baseline { stroke: rgba(136,176,255,0.34); stroke-width: 1; stroke-dasharray: 5 5; }
/* Below the baseline the fill turns red — a loss should be obvious before you
   read a number. Above it keeps the series colour (drawn twice, so brighter). */
.ah-area-down { fill: rgba(239,68,68,0.13); stroke: none; }
/* Amount <-> percentage cross-fade, same mechanism as the sold card's
   sales/items alternation: two inverse keyframe sets, one always up. */
.ah-tip-delta.has-alt { position: relative; }
/* 7s loop: the AMOUNT holds for 5s (0-71%), the percentage for 2s (71-100%).
   a1 is the amount and starts visible, so the card opens on the number. */
.ah-tip-delta.has-alt .ah-tip-alt.a1 { animation: ah-delta-a 7s infinite; }
.ah-tip-delta.has-alt .ah-tip-alt.a2 {
  position: absolute; left: 0; right: 0; top: 0;
  animation: ah-delta-b 7s infinite;
}
@keyframes ah-delta-a { 0%, 69% { opacity: 1; } 72%, 100% { opacity: 0; } }
@keyframes ah-delta-b { 0%, 69% { opacity: 0; } 72%, 100% { opacity: 1; } }
/* Gain/loss fill, split at the baseline. Kept low-alpha: it's a background
   signal, and the data line still has to be the brightest thing on the plot. */
/* oxlicensing doesn't use a gradient at all — its area is a FLAT
   rgba(34,197,94,.08). That's why it looks clean: a gradient draws the eye down
   into empty space, a faint flat tint just weights the area under the line.
   Both stops are the same value, so the fill is uniform. */
.ah-grad-top { stop-color: rgb(34,197,94); stop-opacity: 0.08; }
.ah-grad-bot { stop-color: rgb(34,197,94); stop-opacity: 0.08; }
/* The SOLD line: blue, dashed, thinner. Dashed because it's a different KIND of
   number — asking is continuous (there's always a listing), sold only exists
   where a trade happened, so the gaps are real information rather than missing
   data. */
/* An asking-primary chart is blue THROUGHOUT — line, dots and tooltip outline.
   Half-converting it would be worse than not converting it: a blue line with
   green dots reads as two series when there's one. */
.ah-line.is-ask {
  stroke: var(--blue-bright);
  stroke-dasharray: 5 4;
}
.ah-svg.is-ask .ah-pt { fill: var(--blue-bright); }
.ah-tip-group.is-ask .ah-tip:not(.ah-tip-b) {
  border-color: rgba(120,160,235,0.38) !important;
}
/* stop-color + stop-opacity, exactly like the green — NOT rgba().
   The base rule already sets stop-opacity:0.08, and an rgba() stop-color
   MULTIPLIES with it: 0.28 x 0.08 = 0.022, which is invisible. Same structure as
   the sold fill, same weight, just blue. */
.ah-grad-top.is-ask { stop-color: rgb(120,160,235); }
.ah-grad-bot.is-ask { stop-color: rgb(120,160,235); }

.ah-line2 {
  fill: none; stroke: var(--blue-bright); stroke-width: 1.75;
  stroke-dasharray: 5 4;
  vector-effect: non-scaling-stroke; stroke-linejoin: round; stroke-linecap: round;
  opacity: .9;
}
/* Sample size: deliberately quiet. It's context for the number above, not a
   number in its own right. */
.ah-tip-n {
  font-size: 9.5px; color: var(--text-dim); opacity: .7;
  margin-top: 3px; font-variant-numeric: tabular-nums;
}
.ah-tip-row2 { color: var(--text-dim); }
.ah-tip-val2 { color: var(--blue-bright); }

/* borderColor: rgba(34,197,94,.85), borderWidth: 2 */
.ah-line {
  fill: none; stroke: rgba(34,197,94,0.85); stroke-width: 2;
  vector-effect: non-scaling-stroke; stroke-linejoin: round; stroke-linecap: round;
}
/* Bigger and ringed than Chart.js's default 2px — at 2px on a dark card they
   were nearly invisible against the line. The dark ring separates each dot from
   the line it sits on. */
.ah-pt { fill: rgb(34,197,94); stroke: var(--card-bg); stroke-width: 1.5; r: 3.2; }
.ah-pt { fill: var(--green); opacity: 0.85; }
/* ticks: { color: '#555a72', font: { size: 11 } } */
.ah-axis-y { fill: #555a72; font-size: 11px; font-family: var(--font); text-anchor: end; }
.ah-axis-x { fill: #555a72; font-size: 11px; font-family: var(--font); }
.ah-cursor { stroke: var(--green); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0.5; }
.ah-hoverdot { fill: #fff; stroke: var(--green); stroke-width: 2.5; }
/* Ask-only view: the crosshair, hover dot and active point follow the blue line
   instead of staying green, so the whole hover reads as one colour. */
.ah-svg.is-ask .ah-cursor { stroke: var(--blue-bright); }
.ah-svg.is-ask .ah-hoverdot { stroke: var(--blue-bright); }
.ah-svg.is-ask .ah-pt.on { stroke: var(--blue-bright); }

/* tooltip */
/* Chart.js eases its tooltip over 400ms with easeOutQuart —
   cubic-bezier(0.165, 0.84, 0.44, 1) — which covers most of the distance early
   then settles. That's why it reads as gliding rather than sliding. */
.ah-cursor {
  transition: x1 .4s cubic-bezier(0.165,0.84,0.44,1), x2 .4s cubic-bezier(0.165,0.84,0.44,1);
}
/* The point you're on grows where it stands (Chart.js pointRadius 2 ->
   pointHoverRadius 4). `r` is animatable, so it swells rather than popping. */
.ah-pt { transition: r .18s ease, fill .18s ease; }
/* pointHoverRadius */
.ah-pt.on { r: 5.5; fill: #fff; stroke: rgb(34,197,94); stroke-width: 2.5; }
@media (prefers-reduced-motion: reduce) { .ah-pt, .ah-cursor { transition: none; } }

/* The asking card. Pinned directly under the price card as a pair (see showTip),
   so it reads as a second data point for the same hour rather than a competing
   box. Tinted toward its line's blue and slightly recessed — it's the secondary
   reading, and it should look it. */
/* The GROUP is the only positioned thing — header and cards are static children.
   Two absolutely-positioned cards each animating their own left/top drifted apart
   mid-move and stuck when a transition was interrupted. One mover, no desync. */
.ah-tip-group {
  position: absolute; z-index: 10; pointer-events: none;
  /* gap 4, not 2 — grouping the cards tightened the spacing they had when each
     was positioned separately. */
  display: flex; flex-direction: column; gap: 4px;
  /* Room for the caret below. */
  padding-bottom: 7px;
  transition: left .4s cubic-bezier(0.165,0.84,0.44,1), top .4s cubic-bezier(0.165,0.84,0.44,1);
}

/* The caret — Chart.js draws one by default (oxlicensing sets no caret options,
   so it gets the default 5px triangle) and it's what makes a floating box read as
   "this point" rather than "somewhere near here".
   --caret-x is set per hover: the group is clamped to the plot edges, so the
   point isn't always at its centre. */
.ah-tip-group::after {
  content: '';
  position: absolute;
  bottom: 1px;
  left: var(--caret-x, 50%);
  transform: translateX(-50%);
  width: 0; height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  /* Matches the border of whichever card sits at the bottom — the group sets
     --caret-color so a sold-only tip points green and an asking-only one blue. */
  border-top: 6px solid var(--caret-color, #252836);
}
@media (prefers-reduced-motion: reduce) { .ah-tip-group { transition: none; } }

/* The date. Centred — it labels the whole stack, so it shouldn't hang off one
   edge of it. */
/* The date: bare text above the stack, not a card.
   A box around it made it look like a third reading — but it isn't data, it's a
   label for the data below it. */
.ah-tip-head {
  padding: 0 0 3px; box-sizing: border-box;
  font-size: 10.5px; font-weight: 700; color: var(--text-mid);
  white-space: nowrap; text-align: center;
  text-shadow: 0 1px 3px rgba(0,0,0,0.9);   /* legible over the plot, with no box */
}

/* "Not enough data" — a real answer, where silence read as a broken chart. */
.ah-tip-none { font-size: 11px; color: var(--text-dim); }
/* The same answer inside a card, where the OTHER series has data. Dim, because
   it's an absence, not a value. */
.ah-tip-none-inline { color: var(--text-dim); font-weight: 600; }

/* Two facts, one line, alternating: "20 sales" and "1.28K sold".
   A stack of 64 makes those wildly different numbers and both matter, but the
   card is too small for two rows. CSS animation, not a JS timer — no interval to
   leak when the tooltip is destroyed, and it pauses with the OS's reduce-motion
   setting for free. */
/* TWO keyframe sets, not one with a delay.
   animation-delay leaves the element at its DEFAULT opacity until the delay
   elapses — so for the first 3s both labels were fully visible and printed on top
   of each other ("104.84K sales" over "20 sales"). Inverse keyframes have no
   waiting period: one is always up, the other always down. */
.ah-tip-n.has-alt { position: relative; text-align: center; }
.ah-tip-n.has-alt .ah-tip-alt.a1 { animation: ah-alt-b 4s infinite; }
.ah-tip-n.has-alt .ah-tip-alt.a2 {
  /* left+right, not left alone — the absolute copy needs the same box as the
     static one or text-align has nothing to centre within. */
  position: absolute; left: 0; right: 0; top: 0;
  animation: ah-alt-a 4s infinite;
}
/* 4s total = 2s a side. */
@keyframes ah-alt-a { 0%, 45% { opacity: 1; } 50%, 100% { opacity: 0; } }
@keyframes ah-alt-b { 0%, 45% { opacity: 0; } 50%, 100% { opacity: 1; } }

/* Motion is decoration; both numbers still exist for anyone who'd rather read
   than watch. The sales count is the honest default. */
@media (prefers-reduced-motion: reduce) {
  .ah-tip-n.has-alt .ah-tip-alt.a1 { display: none; }
  .ah-tip-n.has-alt .ah-tip-alt.a2 { animation: none; opacity: 1; position: static; }
}

/* Tooltip value colours, per stat — matching the cards above the chart.
   Money green, spending red, shards purple, everything else the playtime amber.
   .ah-tip-val sets the default; these override it. */
.ah-tip-val.v-money  { color: var(--green); }
.ah-tip-val.v-spent  { color: #ef4444; }
.ah-tip-val.v-shards { color: #a855f7; }
.ah-tip-val.v-stat   { color: #f59e0b; }

/* The change since the previous hour. Green up, red down — and deliberately
   smaller than the value it qualifies. */
.ah-tip-delta {
  font-size: 10.5px; font-weight: 700; margin-top: 2px;
}
.ah-tip-delta.up   { color: var(--green); }
.ah-tip-delta.down { color: #ef4444; }

/* Each card outlined in ITS OWN LINE'S colour — green for sold, blue for asking —
   so you can tell them apart without reading the labels. */
.ah-tip:not(.ah-tip-b) { border-color: rgba(34,197,94,0.38) !important; }
.ah-tip-b {
  border-color: rgba(120,160,235,0.38) !important;
  background: #151822 !important;
}

.ah-tip {
  /* Static: the group positions it. */
  position: relative; box-sizing: border-box;
  /* oxlicensing's exact Chart.js tooltip skin: #181b24 on #252836, 6px radius,
     10px padding. */
  background: #181b24; border: 1px solid #252836; border-radius: 6px;
  padding: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); min-width: 110px;
  /* Glide between points instead of teleporting — the tooltip is repositioned
     by JS on every move, and without this it snaps from point to point. */
  transition: left .4s cubic-bezier(0.165,0.84,0.44,1), top .4s cubic-bezier(0.165,0.84,0.44,1);
}
@media (prefers-reduced-motion: reduce) { .ah-tip { transition: none; } }
/* The date steps back to grey, the "Price:" label takes the blue the date used
   to have, and the number is green like every other money figure. Three tiers,
   each doing one job. */
.ah-tip { text-align: center; }
.ah-tip-date { font-size: 12px; font-weight: 700; color: var(--text-mid); margin-bottom: 4px; }
.ah-tip-row { font-size: 11.5px; color: var(--blue-bright); display: flex; align-items: center; gap: 5px; justify-content: center; }
.ah-tip-row b { color: var(--green); font-weight: 700; }
.ah-tip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); display: inline-block; }

@media (max-width: 620px) {
  .ah-chart-range { align-self: flex-start; }
}

/* no-history card */
.ah-nohist { margin-top: 20px; padding: 20px 24px; background: var(--card-bg); border: 1px solid var(--border); border-radius: 16px; }
.ah-nohist-title { font-size: 14px; font-weight: 700; color: var(--text); }
.ah-nohist-text { font-size: 12.5px; color: var(--text-mid); margin: 6px 0 16px; max-width: 620px; line-height: 1.5; }
.ah-nohist-line { height: 2px; background: rgba(74,96,128,0.5); border-radius: 2px; }

/* skeletons */
.ah-tile.sk { height: 60px; border-radius: 8px; background: var(--card-bg); }
.ah-card.sk { height: 58px; background: var(--card-bg); border: 1px solid var(--border-soft); }
.sk-hero { height: 180px; }
.ah-skeleton-row { display: grid; grid-template-columns: repeat(14, 1fr); gap: 8px; }
.ah-skeleton-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.ah-tile.sk, .ah-card.sk, .sk-hero { animation: ah-pulse 1.4s ease-in-out infinite; }
@keyframes ah-pulse { 0%,100% { opacity: .5; } 50% { opacity: .85; } }
@media (prefers-reduced-motion: reduce) { .ah-tile.sk, .ah-card.sk, .sk-hero { animation: none; } }

.ah-empty { color: var(--text-dim); font-size: 13px; padding: 20px 0; text-align: center; }

/* toast */
.ah-toast {
  position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%) translateY(12px);
  background: var(--surface-4); color: var(--text); border: 1px solid var(--border-bright);
  padding: 10px 16px; border-radius: 10px; font-size: 13px; font-weight: 600;
  box-shadow: 0 8px 28px rgba(0,0,0,0.4); opacity: 0; pointer-events: none;
  transition: opacity .2s, transform .2s; z-index: 2000;
}
.ah-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* responsive: collapse the 14-wide base row + 2-col cards on narrow screens */
@media (max-width: 760px) {
  .ah-tiles, .ah-skeleton-row { grid-template-columns: repeat(7, 1fr); }
  .ah-cards, .ah-skeleton-cards { grid-template-columns: 1fr; }
  .ah-hero { flex-direction: column; }
  .ah-hero-price { font-size: 34px; }
}

/* ============================================================
   Vouches (/vouches) — vouches-view.js
   Trade reputation: leaderboard, recent feed, player profiles.
   ============================================================ */
.vch-wrap { max-width: 980px; }

/* The profile's back link is the first thing on the page, so without a top
   margin it tucks under the sticky header. */
.vch-profile .vch-back { display: inline-block; margin-top: 26px; }

/* two-column landing */
.vch-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 26px; }
.vch-col { min-width: 0; }
/* The two heads must be identical in height AND margin, or the lists below start
   at different heights. The left one carries the Trusted/Reported toggle and the
   right one doesn't, so left to itself the left head is taller — min-height on
   both makes the toggle fit inside a box the plain title also occupies. */
.vch-lb-head { margin-bottom: 0; }
.vch-live { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 600; color: var(--green); }
.vch-live .ah-dot { width: 7px; height: 7px; }

/* leaderboard toggle */
/* Same control as the AH range switch: recessed track, lit gradient pill. It was
   a pair of flat chips in a grey box; this matches the language used everywhere
   else on the site. */
.vch-toggle {
  position: relative; display: inline-flex; gap: 0; margin-left: auto;
  background: var(--page-bg);
  border: 1px solid var(--border);
  border-radius: 10px; padding: 3px;
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.4);
}
/* Red when Reported is active: the pill is the only thing saying which list
   you're looking at, and blue-for-scammers is the wrong signal. */
.vch-toggle.is-reported .vch-tg-slider {
  background: linear-gradient(160deg, #ef4444, #b91c1c 60%, #7f1d1d);
  border-color: rgba(239,68,68,0.55);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.18),
    0 2px 6px rgba(0,0,0,0.35),
    0 0 10px rgba(239,68,68,0.22);
}
.vch-tg-slider {
  position: absolute; top: 3px; left: 3px;
  height: calc(100% - 6px);
  background: linear-gradient(160deg, var(--blue), var(--blue-dim) 60%, #1e3a8a);
  border: 1px solid var(--blue-border);
  border-radius: 7px; z-index: 0; pointer-events: none;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.18),
    0 2px 6px rgba(0,0,0,0.35),
    0 0 10px rgba(91,140,247,0.18);
  transition: left .22s cubic-bezier(0.4,0,0.2,1), width .22s cubic-bezier(0.4,0,0.2,1);
}
.vch-tg {
  position: relative; z-index: 1;
  border: 0; background: transparent; color: var(--text-dim);
  font-family: var(--font); font-size: 11px; font-weight: 700; letter-spacing: .04em;
  padding: 6px 12px; border-radius: 6px; cursor: pointer;
  white-space: nowrap; user-select: none;
  transition: color .18s ease;
}
.vch-tg:hover:not(.on) { color: var(--text); }
/* The pill behind it does the colour — the button only lights its text. */
.vch-tg.on { background: none; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.35); }

/* leaderboard rows */
.vch-lb { display: flex; flex-direction: column; gap: 8px; }
.vch-lb-row {
  display: flex; align-items: center; gap: 12px; padding: 10px 14px;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 10px;
  cursor: pointer; transition: transform .14s, border-color .14s, background .14s;
}
.vch-lb-row:hover { transform: translateY(-2px); border-color: var(--blue-border); background: var(--card-bg-hover); }
.vch-rank { font-size: 12px; font-weight: 800; color: var(--text-dim); width: 22px; flex-shrink: 0; }
.vch-av { width: 32px; height: 32px; border-radius: 6px; flex-shrink: 0; image-rendering: pixelated; }
.vch-lb-main { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.vch-lb-name { font-size: 13.5px; font-weight: 700; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vch-lb-sub { font-size: 10.5px; color: var(--text-mid); }
.vch-lb-pct { font-size: 18px; font-weight: 800; display: flex; flex-direction: column; align-items: flex-end; line-height: 1; flex-shrink: 0; }
.vch-lb-pct-label { font-size: 9px; font-weight: 400; color: var(--text-dim); margin-top: 2px; }
.vch-lb-pct.good { color: var(--green); }
.vch-lb-pct.mid  { color: var(--gold); }
.vch-lb-pct.bad  { color: var(--red); }
.vch-lb-pct.none { color: var(--text-dim); }

/* recent feed */
.vch-feed { display: flex; flex-direction: column; gap: 8px; }
.vch-feed-row {
  display: flex; align-items: center; gap: 12px; padding: 10px 14px;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 10px;
  cursor: pointer; transition: transform .14s, border-color .14s, background .14s;
}
.vch-feed-row:hover { transform: translateY(-2px); border-color: var(--blue-border); background: var(--card-bg-hover); }
.vch-feed-chip {
  width: 26px; height: 26px; border-radius: 6px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; font-size: 12px;
}
.vch-feed-chip.pos { background: rgba(34,197,94,0.16); border: 1px solid rgba(34,197,94,0.4); }
.vch-feed-chip.neg { background: rgba(239,68,68,0.16); border: 1px solid rgba(239,68,68,0.4); }
.vch-feed-main { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.vch-feed-line { font-size: 12px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vch-feed-line b { font-weight: 700; }
.vch-feed-line b.pos { color: var(--green); }
.vch-feed-line b.neg { color: var(--red); }
.vch-feed-comment { font-size: 10.5px; color: var(--text-mid); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vch-feed-time { font-size: 10px; color: var(--text-dim); flex-shrink: 0; }

/* ── profile ── */
.vch-head {
  display: flex; justify-content: space-between; align-items: flex-start; gap: 16px;
  padding: 26px 28px; background: var(--card-bg); border: 1px solid var(--border);
  border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.24); margin-top: 4px;
}
.vch-head-left { display: flex; gap: 18px; align-items: center; min-width: 0; }
.vch-head-av { width: 72px; height: 72px; border-radius: 12px; image-rendering: pixelated; }
.vch-head-name { font-size: 26px; font-weight: 800; color: var(--text); }
.vch-head-since { font-size: 12px; color: var(--text-mid); margin-top: 4px; }
.vch-head-trust { text-align: right; flex-shrink: 0; }
.vch-head-pct { font-size: 44px; font-weight: 800; line-height: 1; display: flex; flex-direction: column; align-items: flex-end; }
.vch-head-pct span { font-size: 13px; font-weight: 600; color: var(--text-mid); margin-top: 2px; }
.vch-head-pct.good { color: var(--green); }
.vch-head-pct.mid  { color: var(--gold); }
.vch-head-pct.bad  { color: var(--red); }
.vch-head-pct.none { color: var(--text-dim); }
/* Under the name, sized to match the leaderboard rows' .vch-lb-sub — the two
   views should be indistinguishable. It sat on the right at 11.5px before, so it
   needs its own size here rather than inheriting the trust column's. */
.vch-head-tallies {
  display: flex; align-items: center;
  font-size: 12px; margin-top: 5px;
}

/* cast bar */
.vch-cast { margin-top: 16px; padding: 18px 20px; background: var(--card-bg); border: 1px solid var(--blue-border-dim); border-radius: 13px; }
.vch-cast-self, .vch-cast-guest { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.vch-cast-label { font-size: 12.5px; font-weight: 600; color: var(--text-mid); }
.vch-cast-row { display: flex; gap: 10px; margin-top: 12px; }
.vch-cast-btn {
  border: 1px solid var(--border); background: var(--surface-3); color: var(--text);
  font-family: var(--font); font-size: 12.5px; font-weight: 700; padding: 9px 16px;
  border-radius: 8px; cursor: pointer; transition: border-color .14s, background .14s;
}
.vch-cast-btn.pos:hover, .vch-cast-btn.pos.sel { border-color: rgba(34,197,94,0.5); background: rgba(34,197,94,0.12); color: var(--green); }
.vch-cast-btn.neg:hover, .vch-cast-btn.neg.sel { border-color: rgba(239,68,68,0.5); background: rgba(239,68,68,0.12); color: var(--red); }
.vch-cast-expand { margin-top: 14px; }
.vch-cast-expand textarea {
  width: 100%; min-height: 60px; resize: vertical; box-sizing: border-box;
  background: var(--page-bg); border: 1px solid var(--border); border-radius: 9px;
  color: var(--text); font-family: var(--font); font-size: 13px; padding: 10px 12px; outline: none;
}
.vch-cast-expand textarea:focus { border-color: var(--blue-border); }
.vch-cast-actions { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; }
.vch-cast-verdict { font-size: 12px; font-weight: 600; }
.vch-cast-verdict.pos { color: var(--green); }
.vch-cast-verdict.neg { color: var(--red); }
.vch-cast-submit {
  background: var(--blue-dim); color: #fff; border: 0; border-radius: 8px;
  font-family: var(--font); font-size: 12.5px; font-weight: 700; padding: 8px 18px; cursor: pointer;
}
.vch-cast-submit:hover { background: var(--blue); }
.vch-cast-submit:disabled { opacity: .6; cursor: default; }
.vch-cast-guest .btn-search { flex-shrink: 0; }

/* detailed vouch list */
.vch-list-sec { margin-top: 24px; }
.vch-list-title { margin-bottom: 14px; }
.vch-list { display: flex; flex-direction: column; gap: 8px; }
.vch-item { display: flex; gap: 12px; padding: 14px 16px; background: var(--card-bg); border: 1px solid var(--border); border-radius: 11px; }
.vch-item-av { width: 30px; height: 30px; border-radius: 6px; flex-shrink: 0; image-rendering: pixelated; }
.vch-item-body { min-width: 0; flex: 1; }
.vch-item-head { display: flex; align-items: center; gap: 10px; }
.vch-item-name { font-size: 13px; font-weight: 700; color: var(--text); }
.vch-item-badge { font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 6px; }
.vch-item-badge.pos { color: var(--green); background: rgba(34,197,94,0.14); border: 1px solid rgba(34,197,94,0.4); }
.vch-item-badge.neg { color: var(--red); background: rgba(239,68,68,0.14); border: 1px solid rgba(239,68,68,0.4); }
.vch-item-time { font-size: 10px; color: var(--text-dim); margin-left: auto; }
.vch-item-comment { font-size: 12.5px; color: var(--text-mid); margin-top: 6px; line-height: 1.45; }

@media (max-width: 760px) {
  .vch-cols { grid-template-columns: 1fr; }
  .vch-head { flex-direction: column; }
  .vch-head-trust { text-align: left; }
  .vch-head-pct { align-items: flex-start; }
}

/* ── Player search page (/players) ─────────────────────────────────────────
   Reuses the AH landing's tile/card/pager/views classes wholesale; only the
   player face is new. Faces match the AH item-icon sizes (32px tiles, 30px
   cards). */
.ps-face-wrap { display: inline-flex; align-items: center; justify-content: center; }
.ps-face { image-rendering: pixelated; border-radius: 6px; display: block; }
/* Skulls are solid squares, so at the item-icon size they look bigger than the
   items (which have transparent padding). Nudge the base-row skull down a touch
   so it sits in the tile square with a little breathing room. */
.ps-tile .ps-face { width: 27px; height: 27px; }

/* Player top-viewed cards: the face uses the AH's .ah-img class, so the card
   sizes pixel-identical to the item cards — no min-height hack needed. */

/* ── Recently-searched live rail (/players) — uses the AH live-sales feed
   classes (.ah-feed / .ah-feed-clip / .ah-feed-card) verbatim, so it looks and
   animates EXACTLY like the item sale history: skull, balance, name, timestamp
   under, and the whole-column slide on a new arrival. */
.ah-wrap.ps-wrap { position: relative; }
.ps-feed-section {
  position: absolute; top: 0;
  left: calc(50% - 50vw + 18px);
  width: 122px; margin: 0; padding: 0;
}
.ps-feed-section .ah-sec-title-row { padding: 0 0 0 2px; margin-bottom: 8px; }
.ps-feed-section .ah-sec-title { font-size: 11px; letter-spacing: .04em; opacity: .75; }
@media (max-width: 1240px) { .ps-feed-section { display: none; } }
/* Measured in JS against the top-cards grid now (see fitRail in
   player-search-view.js), the same as the Auction House. The old fixed 80vh had
   no relationship to where the cards end, so the strip stopped early on some
   viewports and ran past the pager on others. */
.ps-feed-section .ah-feed-clip { height: 100%; min-height: 240px; }
.ps-feed-empty { font-size: 11px; color: var(--text-dim); padding: 8px 4px; }
/* Round the skulls inside the otherwise-square .ah-img slots so player heads
   match the rounded faces used everywhere else (base tiles, cards, feed). */
.ps-round { border-radius: 6px; }

/* ── Server Stats page (/server-stats) ─────────────────────────────────────── */
.ss-wrap { max-width: 900px; margin: 0 auto; }
/* Player-count chart with the live-status bubble merged onto its top edge
   (poptab-style, centered). Absolute so it can never collide with the head row
   of the card (title + range) sitting just inside. */
.ss-chart-wrap { position: relative; margin: 60px auto 60px; }
.ss-chart-wrap .ss-chart-card { border-color: rgba(120,160,235,0.35); }
.ss-live-tab {
  position: absolute; left: 50%; bottom: 100%; transform: translateX(-50%);
  margin-bottom: -1px;                 /* sit on the card's top border -> merged */
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  min-width: 210px;                    /* steady width so count<->MOTD swap is calm */
  padding: 10px 20px 12px;
  background: var(--card-bg);
  border: 1px solid rgba(120,160,235,0.35); border-bottom: none;
  border-radius: 14px 14px 0 0;
  z-index: 6; white-space: nowrap; font-size: 15px; line-height: 1;
  /* Static on purpose — no transition/animation. */
}
.ss-live-tab.is-offline { opacity: .92; }
.ss-tab-icon { width: 27px; height: 27px; border-radius: 6px; image-rendering: pixelated; flex-shrink: 0; }
.ss-po { font-size: 18px; font-weight: 800; color: var(--green); font-variant-numeric: tabular-nums; }
/* The " / max" reads at the SAME size as the online count, just muted. */
.ss-slash { font-size: 18px; font-weight: 700; color: var(--text-mid); font-variant-numeric: tabular-nums; }
.ss-pm { color: var(--text-mid); font-size: 15px; }
/* Cross-fade the bubble's contents when it swaps count <-> MOTD. */
/* The tab itself never fades — that took its background and border with it and
   made the whole thing blink off the panel. Only what is inside it changes. */
.ss-live-tab { transition: none; }

/* Fixed width, not min-width. The two phases are different lengths, so a
   min-width let the tab resize on every swap and dragged the icon with it. A
   fixed box means the text changes and nothing else moves. */
/* Fixed width so the tab never resizes as the count changes. The icon and the
   number sit together in the middle rather than at opposite ends: with the text
   stretched across the remaining space they read as two separate things that
   happen to share a box. Tabular figures keep the pair from shifting as the
   digits change, which is what the stretched layout was guarding against. */
.ss-live-tab { width: 268px; min-width: 0; justify-content: center; gap: 9px; }
.ss-live-inner {
  flex: 0 1 auto; min-width: 0;
  display: inline-flex; align-items: center; justify-content: center;
  overflow: hidden; white-space: nowrap;
  font-variant-numeric: tabular-nums;
  transition: opacity .18s ease;
}
/* Room either side of the divider — "17,725/ 35,000" ran the two numbers into
   the slash and read as one token. */
.ss-slash { margin: 0 7px 0 6px; opacity: .55; }

/* Vertically centred between the nav and the footer. The page is short, so
   main's min-height left all the slack underneath it. */
.ss-wrap {
  min-height: calc(100vh - 300px);
  display: flex; flex-direction: column; justify-content: center;
}

/* Title and range live above the card now, sized exactly like the balance
   history header on a player page — same 16px title, same small range pill.
   The server-stats versions were noticeably larger, which is the difference
   between the two screenshots. */
.ss-chart-head-outer {
  /* The player page's own arrangement, reserve and all.
     It keeps a middle track for its item tabs (--tabs-reserve) and then does two
     DIFFERENT things: the title is centred in the gutter left of that track,
     while the range sits hard right. Not symmetric — and every attempt of mine
     to make it symmetric is why this row kept looking wrong.
     Here the reserved middle is the live count tab, so the reserve is its width. */
  --tabs-reserve: 268px;
  display: flex; align-items: center; justify-content: space-between;
  margin: 0 0 12px;
}
.ss-chart-head-outer .ss-chart-title {
  display: flex; align-items: center; justify-content: center;
  width: calc(50% - var(--tabs-reserve) / 2); min-width: 150px; min-height: 32px;
}
.ss-chart-head-outer .ah-chart-range { width: auto; justify-content: flex-end; }
.ss-chart-head-outer .ss-chart-title {
  position: static; transform: none; margin: 0; padding: 0;
  background: none; text-align: center; white-space: nowrap;
  font-family: var(--font-display);
  font-size: 16px; font-weight: 800; letter-spacing: -0.01em;
  color: var(--text);
}
.ss-chart-head-outer .ah-range-btn {
  font-size: 10px; padding: 5px 10px; letter-spacing: .03em;
}
.ss-chart-head-outer .ah-chart-range { align-self: center; }
/* The anchor for the live tab. It has to be the card's own box, or the tab
   rides above the header instead of sitting on the card's top edge. */
.ss-card-anchor { position: relative; }

/* Centred on the page, and narrow enough that the chart is not a letterbox. */
/* Centred on the page. width:100% matters — a bare max-width on a block that is
   also a flex/grid child can be sized by the parent instead of by this rule,
   which is how it ended up sitting a little left of centre. */
.ss-wrap { width: 100%; max-width: 900px; margin-left: auto; margin-right: auto; }
.ss-chart-wrap { position: static; width: 100%; margin: 60px auto; }
.ss-live-tab.is-offline { opacity: .92; }
/* MOTD rendered inline in the bubble, exactly like the in-game server list:
   real § colours, its own line breaks, centered. */
.ss-motd-inline { font-size: 13px; line-height: 1.4; text-align: center; white-space: pre-line; }
/* The server chart's head is a normal in-card row (the base .ah-chart-head is
   absolutely pinned top-right for item/player charts — undo that here) so the
   title sits on the LEFT and the range switch on the RIGHT, each in its half. */
.ss-chart-card .ah-chart-head {
  position: static; top: auto; right: auto;
  width: 100%; justify-content: space-between; align-items: center;
  flex-wrap: nowrap; margin: 0 0 10px;
}
.ss-chart-card .ah-chart-head-left { margin-right: auto; }
.ss-chart-title { font-family: var(--font-display); font-size: 15px; font-weight: 700; color: var(--text); }

/* ── Player AuctionHouse history — reuses the auction live-sale cards verbatim ─ */
.pl-trades { margin: 42px 0 64px; }   /* 64px bottom gap keeps it off the footer, matching the chart */
/* Title and pager share one line, like the chart's title + range switch. */
.pl-trades-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 38px; }
.pl-trades-title { font-family: var(--font-display); font-size: 18px; font-weight: 800; color: var(--text); }
.pl-trades-pager { margin-top: 0; }   /* the shared .ah-pager adds top margin; on the title line it needs none */
.pl-trades-pager .ah-page-btn { padding: 7px 14px; min-width: 86px; font-size: 12px; }
.pl-trades-card { margin-top: 12px; }
.pl-trades-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 10px;
}
/* The cards ARE the auction feed cards; only the ticker's flex sizing needs
   overriding so they fill the grid, and the newest-card arrival flash is off —
   a trade list has no "just arrived" card. */
.pl-trades-grid .ah-feed-item { flex: none; width: 100%; }
.pl-trades-grid .ah-feed-item:first-child .ah-feed-card { animation: none; }

/* ── settings modal ───────────────────────────────────────────────────────────
   Built from parts the site already has: the .lcard shell (card bg, hairline
   blue border, deep float shadow) and the segmented pill used by the vouches
   toggle and the chart's range selector. No new control types — a yes/no
   setting is an Off|On pill, so there's one thing to learn and it already
   matches everything around it. */
html.set-open, body.set-open { overflow: hidden; }
/* Scrolling the settings list must not hand off to the page behind it once it
   hits the end — that hand-off is what makes a modal feel unfocused. */
.set-backdrop, .set-pane { overscroll-behavior: contain; }

.set-backdrop {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center; padding: 24px;
  background: rgba(6,8,14,0.72);
  backdrop-filter: blur(6px) saturate(0.9);
  -webkit-backdrop-filter: blur(6px) saturate(0.9);
  opacity: 0; transition: opacity .18s ease;
}
.set-backdrop.open { opacity: 1; }

/* Same construction as .lcard so it reads as one of the site's cards, lifted. */
.set-modal {
  position: relative;
  width: min(880px, 100%); height: min(600px, calc(100vh - 48px));
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 18px;
  box-shadow: 0 30px 70px rgba(0,0,0,0.65), 0 12px 34px rgba(0,0,0,0.45),
              0 0 0 1px var(--blue-border-dim);
  display: grid; grid-template-rows: auto 1fr auto; overflow: hidden;
  transform: translateY(14px) scale(0.97);
  transition: transform .26s cubic-bezier(0.34,1.3,0.5,1);
}
.set-backdrop.open .set-modal { transform: none; }

.set-head {
  display: flex; align-items: center; gap: 12px;
  padding: 16px 20px; border-bottom: 1px solid var(--border-soft);
}
.set-head h2 {
  margin: 0; font-size: 15px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .04em; color: var(--text);
}
.set-x {
  margin-left: auto; width: 30px; height: 30px; flex-shrink: 0;
  display: grid; place-items: center;
  background: transparent; border: 1px solid transparent; border-radius: 9px;
  color: var(--text-dim); cursor: pointer;
  transition: color .16s, background .16s, border-color .16s;
}
.set-x:hover { color: var(--text); background: var(--card-bg-hover); border-color: var(--border); }
.set-x svg { width: 15px; height: 15px; }

.set-body { display: grid; grid-template-columns: 186px 1fr; min-height: 0; }

/* Recessed rail, same idea as the range selector's sunken track. */
.set-rail {
  position: relative; padding: 14px;
  border-right: 1px solid var(--border-soft);
  background: linear-gradient(180deg, rgba(10,10,14,0.55), rgba(10,10,14,0.25));
  display: flex; flex-direction: column; gap: 2px;
}
.set-nav {
  position: relative; z-index: 1;
  border: 1px solid transparent;
  display: flex; align-items: center; gap: 10px;
  height: 38px; padding: 0 11px;
  background: transparent; border: 0; border-radius: 7px;
  font-family: var(--font); font-size: 12.5px; font-weight: 600;
  color: var(--text-dim); cursor: pointer; text-align: left;
  white-space: nowrap; user-select: none; transition: color .18s ease;
}
.set-nav:hover:not(.on) { color: var(--text); }
.set-nav.on {
  color: #fff; font-weight: 700;
  background: linear-gradient(160deg, var(--blue), var(--blue-dim) 60%, #1e3a8a);
  border: 1px solid var(--blue-border);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.18), 0 2px 6px rgba(0,0,0,0.35);
  text-shadow: 0 1px 2px rgba(0,0,0,0.35);
}
.set-nav svg { width: 15px; height: 15px; flex-shrink: 0; opacity: .9; }

.set-pane { padding: 20px 22px 24px; overflow-y: auto; min-height: 0; }
.set-h { font-size: 15px; font-weight: 700; color: var(--text); margin-bottom: 3px; }
.set-sub { margin: 0 0 16px; font-size: 12px; color: var(--text-mid); line-height: 1.5; }

.set-rows {
  background: var(--page-bg);
  border: 1px solid var(--border-soft);
  border-radius: 12px; overflow: hidden;
}
.set-row {
  display: flex; align-items: center; gap: 18px;
  padding: 13px 15px; border-bottom: 1px solid var(--border-soft);
}
.set-row:last-child { border-bottom: 0; }
.set-row-txt { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.set-row-t { font-size: 13px; font-weight: 600; color: var(--text); }
.set-row-d { font-size: 11.5px; color: var(--text-dim); line-height: 1.45; }

/* The site's one control, verbatim from .vch-toggle / .ah-range. */
.set-seg {
  position: relative; display: inline-flex; gap: 0; margin-left: auto;
  /* #0e121b — the same step-darker surface the profile dropdown and the AH
     search results use, so every control on the site reads as one family. */
  flex-shrink: 0; background: #0e121b;
  border: 1px solid var(--border); border-radius: 10px; padding: 3px;
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.4);
}
.set-seg-slider {
  position: absolute; top: 3px; left: 3px; height: calc(100% - 6px);
  background: linear-gradient(160deg, var(--blue), var(--blue-dim) 60%, #1e3a8a);
  border: 1px solid var(--blue-border);
  border-radius: 7px; z-index: 0; pointer-events: none;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.18),
    0 2px 6px rgba(0,0,0,0.35),
    0 0 10px rgba(91,140,247,0.18);
  transition: left .22s cubic-bezier(0.4,0,0.2,1), width .22s cubic-bezier(0.4,0,0.2,1);
}
.set-sg {
  position: relative; z-index: 1;
  border: 0; background: transparent; color: var(--text-dim);
  font-family: var(--font); font-size: 11px; font-weight: 700; letter-spacing: .04em;
  padding: 6px 12px; border-radius: 6px; cursor: pointer;
  white-space: nowrap; user-select: none; transition: color .18s ease;
}
.set-sg:hover:not(.on) { color: var(--text); }
.set-sg.on { background: none; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.35); }

/* Wide option sets wrap, so there is no slider to sit behind the active one —
   it takes the fill itself. Left-aligned and allowed to grow, since ten options
   cannot sit on one line beside a label. */
.set-seg.is-wrap { flex-wrap: wrap; gap: 3px; margin-left: auto; max-width: 340px; justify-content: flex-end; }
.set-seg.is-wrap .set-sg.on {
  background: linear-gradient(160deg, var(--blue), var(--blue-dim) 60%, #1e3a8a);
  border: 1px solid var(--blue-border);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.18), 0 2px 6px rgba(0,0,0,0.35);
}
.set-seg.is-wrap .set-sg { border: 1px solid transparent; padding: 6px 10px; }

/* Dropdown — the profile wallet's construction, reused. ONE card owns the
   border, radius and background; the head sits on top and the list grows out of
   its bottom edge, sharing the same left and right sides. There is only ever one
   outline, so there is nothing to keep aligned. */
.set-menu {
  --menu-radius: 12px;
  position: relative; margin-left: auto; flex-shrink: 0;
  width: 186px; height: 36px;      /* keeps its slot; the card grows over the row */
}
.set-menu-head {
  position: absolute; inset: 0 0 auto 0; z-index: 2;
  display: flex; align-items: center; gap: 9px;
  height: 36px; padding: 0 11px; box-sizing: border-box;
  font-family: var(--font); font-size: 12.5px; font-weight: 600; color: var(--text);
  /* #0e121b — the step-darker surface the profile dropdown and the AH search
     results already use, so every control reads as one family. */
  background: #0e121b;
  border: 1px solid var(--border); border-radius: var(--menu-radius);
  cursor: pointer; text-align: left;
  transition: border-color .15s, border-radius .28s cubic-bezier(0.4,0,0.2,1);
}
.set-menu-head:hover { border-color: var(--border-bright); }
.set-menu.open .set-menu-head {
  border-color: var(--blue-border);
  border-bottom-color: transparent;                          /* head and list join */
  border-radius: var(--menu-radius) var(--menu-radius) 0 0;
}
.set-menu-head img { width: 18px; height: 18px; image-rendering: pixelated; flex: none; }
.set-menu-label { margin-right: auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.set-menu-chev {
  width: 13px; height: 13px; flex: none; color: var(--text-dim);
  transition: transform .28s cubic-bezier(0.4,0,0.2,1);
}
.set-menu.open .set-menu-chev { transform: rotate(180deg); }

/* One curve, one duration — height and opacity only, the way the wallet body
   animates. Anything else stutters. */
.set-menu-body {
  position: absolute; top: 35px; left: 0; right: 0; z-index: 1;
  display: flex; flex-direction: column; box-sizing: border-box;
  background: #0e121b;
  border: 1px solid var(--blue-border); border-top: 0;
  border-radius: 0 0 var(--menu-radius) var(--menu-radius);
  box-shadow: 0 18px 40px rgba(0,0,0,0.55);
  max-height: 0; opacity: 0; overflow: hidden; pointer-events: none;
  transition: max-height .34s cubic-bezier(0.4,0,0.2,1),
              opacity .24s cubic-bezier(0.4,0,0.2,1);
}
.set-menu.open .set-menu-body {
  max-height: 254px; opacity: 1; pointer-events: auto;
  overflow-y: auto; overscroll-behavior: contain;
  /* The site's own thin scrollbar — the same one .ah-sales-list uses. A 6px
     thumb on a transparent track sits inside the menu instead of being drawn
     over it, so it never shifts the rows or breaks the rounded corner. */
  scrollbar-width: thin; scrollbar-color: var(--border-bright) transparent;
}
.set-menu-body::-webkit-scrollbar { width: 6px; }
.set-menu-body::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 3px; }
.set-menu-body::-webkit-scrollbar-track { background: transparent; }
/* Room for the thumb so it never sits on top of an icon or a label. */
.set-menu.open .set-menu-body .set-mi { padding-right: 14px; }

/* The pane scrolls, which would clip an open menu at the row edge. Let it out
   while one is open — the panes are short enough that nothing is lost. */
.set-pane.has-menu,
.set-pane.has-menu .set-rows { overflow: visible; }

/* The pane heading used to supply the gap under the modal's header bar (its own
   margin plus the blurb's 16px). With both gone the rows sat flush against the
   divider, so the spacing moves here. */
.set-page > .set-rows:first-child { margin-top: 4px; }
/* The row holding the open menu has to sit above its siblings, or the next row's
   background paints over the list. */
.set-pane.has-menu .set-row:has(.set-menu.open) { position: relative; z-index: 5; }

.set-mi {
  display: flex; align-items: center; gap: 10px; width: 100%;
  /* Edge-to-edge and square, so the shell's rounding clips the first and last
     hover into the corners and there is no "last item" special case. */
  padding: 9px 11px; border: 0; border-radius: 0; cursor: pointer;
  font-family: var(--font); font-size: 12.5px; font-weight: 600;
  color: var(--text-mid); background: none; text-align: left; white-space: nowrap;
  transition: background .12s, color .12s;
}
.set-mi img { width: 18px; height: 18px; image-rendering: pixelated; flex: none; }
.set-mi:hover { background: var(--surface-3); color: var(--text); }
.set-mi.on { color: #fff; background: rgba(91,140,247,0.14); }

/* The pane scrolls, which would clip an open menu at the row. Let it out while
   one is open; the panes are short enough that nothing is lost. */
.set-pane.has-menu { overflow: visible; }

.set-foot {
  display: flex; align-items: center; justify-content: flex-end; gap: 10px;
  padding: 12px 20px; border-top: 1px solid var(--border-soft);
  background: rgba(10,10,14,0.35);
}
.set-done {
  font-family: var(--font); font-size: 12.5px; font-weight: 700;
  padding: 9px 20px; border-radius: 10px; cursor: pointer; color: #fff;
  border: 1px solid var(--blue-border);
  background: linear-gradient(160deg, var(--blue), var(--blue-dim) 60%, #1e3a8a);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.18), 0 2px 6px rgba(0,0,0,0.35);
  transition: filter .16s;
}
.set-done:hover { filter: brightness(1.08); }
.set-reset {
  font-family: var(--font); font-size: 12.5px; font-weight: 700;
  padding: 9px 16px; border-radius: 10px; cursor: pointer;
  color: var(--text-dim); background: var(--page-bg); border: 1px solid var(--border);
  transition: color .16s, border-color .16s, background .16s;
}
.set-reset:hover { color: #fca5a5; border-color: rgba(239,68,68,0.28); background: rgba(239,68,68,0.07); }

/* Confirm layer. Sits INSIDE the modal rather than being a second overlay on
   the page: the decision belongs to the settings you are looking at, and a
   detached dialog loses that connection. */
.set-confirm {
  position: absolute; inset: 0; z-index: 2;
  display: grid; place-items: center; padding: 24px;
  background: rgba(6,8,14,0.66);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  animation: set-confirm-in .14s ease both;
}
.set-confirm[hidden] { display: none; }
@keyframes set-confirm-in { from { opacity: 0; } to { opacity: 1; } }
.set-confirm-box {
  width: min(500px, 100%);
  background: var(--card-bg); border: 1px solid rgba(239,68,68,0.22);
  border-radius: 14px; padding: 20px;
  box-shadow: 0 24px 60px rgba(0,0,0,0.7), 0 0 0 1px rgba(239,68,68,0.10);
  animation: set-confirm-rise .2s cubic-bezier(0.34,1.3,0.5,1) both;
}
@keyframes set-confirm-rise {
  from { opacity: 0; transform: translateY(10px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
.set-confirm-t { font-size: 15px; font-weight: 700; color: var(--text); margin-bottom: 7px; text-align: center; }
.set-confirm-b { margin: 0 0 18px; font-size: 12.5px; color: var(--text-mid); line-height: 1.55; text-align: center; }
.set-confirm-actions { display: flex; justify-content: center; gap: 10px; }
.set-confirm-actions button { min-width: 104px; }
.set-btn-ghost {
  font-family: var(--font); font-size: 12.5px; font-weight: 700;
  padding: 9px 16px; border-radius: 10px; cursor: pointer;
  color: var(--text-mid); background: var(--page-bg); border: 1px solid var(--border);
  transition: color .16s, border-color .16s;
}
.set-btn-ghost:hover { color: var(--text); border-color: var(--border-bright); }
.set-btn-danger {
  font-family: var(--font); font-size: 12.5px; font-weight: 700;
  padding: 9px 16px; border-radius: 10px; cursor: pointer; color: #fff;
  background: linear-gradient(160deg, #ef4444, #b91c1c 60%, #7f1d1d);
  border: 1px solid rgba(239,68,68,0.55);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.18), 0 2px 6px rgba(0,0,0,0.35);
  transition: filter .16s;
}
.set-btn-danger:hover { filter: brightness(1.08); }
.set-btn-safe {
  font-family: var(--font); font-size: 12.5px; font-weight: 700;
  padding: 9px 16px; border-radius: 10px; cursor: pointer; color: #fff;
  background: linear-gradient(160deg, #22c55e, #16a34a 60%, #14532d);
  border: 1px solid rgba(34,197,94,0.55);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.18), 0 2px 6px rgba(0,0,0,0.35);
  transition: filter .16s;
}
.set-btn-safe:hover { filter: brightness(1.08); }

.set-page { display: none; }
.set-page.on { display: block; }

.set-backdrop :focus-visible { outline: 2px solid var(--blue-bright); outline-offset: 2px; }

@media (max-width: 720px) {
  .set-backdrop { padding: 12px; }
  .set-modal { height: min(640px, calc(100vh - 24px)); }
  .set-body { grid-template-columns: 1fr; }
  .set-rail {
    flex-direction: row; overflow-x: auto; gap: 6px;
    border-right: 0; border-bottom: 1px solid var(--border-soft);
  }
  .set-nav.on { background: var(--surface-3); }
  .set-row { flex-wrap: wrap; gap: 10px; }
  .set-seg { margin-left: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .set-backdrop, .set-modal, .set-seg-slider { transition: none; }
}


/* ── oxverify link modal ──────────────────────────────────────────────────────
   The flow is oxverify's own page in an iframe. The box only has to hold it and
   get out of the way — everything inside is styled by oxverify, using this
   tenant's theme. */
.ox-modal {
  position: fixed; inset: 0; z-index: 3000;
  display: grid; place-items: center; padding: 24px;
  /* Matches .bet's onboarding overlay: a heavier scrim and a real blur, so the
     modal is the only thing the eye can settle on. 4px left the leaderboard
     behind it legible enough to compete with the card. */
  background: rgba(0, 0, 0, 0.8);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  /* Scrim AND frame fade as one thing, once the flow inside has reported it is
     ready and at its final height. Fading the scrim on open and the frame later
     meant two ramps with a gap between them, and the height settling somewhere
     in the middle — which is the grow-and-flash. Nothing is shown until there
     is a finished card to show. */
  opacity: 0;
  transition: opacity .16s ease;
}
.ox-modal.ox-shown { opacity: 1; }

/* Mid-verification the backdrop stops dismissing. A click gets a small shake
   instead of nothing at all — silence reads as a frozen page. */
.ox-modal-busy { cursor: default; }
.ox-modal-busy.ox-nudge .ox-modal-box { animation: ox-nudge .3s cubic-bezier(.36,.07,.19,.97); }
@keyframes ox-nudge {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-4px); }
  40%, 60% { transform: translateX(4px); }
}
/* 380px matches .ob-modal's own max-width exactly, so the frame and the card
   share an edge instead of the card floating inside a wider hole. The height is
   a starting guess only — the flow measures itself and posts its real height,
   and auth.js sets it from there. */
/* The card inside fills this frame, so THIS is the modal's real width. Height
   is a starting guess only — the flow measures itself and posts its height, and
   auth.js applies it.

   No transition on height: the flow reports a new one every step, and animating
   it made the card visibly grow into place. .bet has no such animation — its
   card just appears at the new size, centred — so the height snaps here too. */
.ox-modal-box {
  position: relative;
  /* No entrance animation on the box: the frame inside it fades once, and two
     animations on nested elements is what made opening feel unsettled. */
  /* 380px, matching .ob-modal's own max-width in .bet. Earlier screenshots were
     captured at 125% browser zoom, which made .bet's card measure ~453px and
     sent me chasing 420 and then 500 — all of it too wide. Measured against a
     known-width capture of our own modal, .bet's is 380 CSS px exactly. */
  width: min(380px, 100%);
  height: 520px;
  max-height: 92vh;
}

/* Transparent hole, not a card. oxverify draws its own .ob-modal inside this
   frame — giving the frame a background, radius and shadow of its own meant
   two nested cards, one visibly inside the other. */
.ox-modal-frame {
  width: 100%; height: 100%; border: 0; border-radius: 0;
  background: transparent;
  box-shadow: none;
  /* No fade of its own — the wrapper above handles the single transition. */
}
/* Sits outside the frame, because we cannot draw inside someone else's page. */
.ox-modal-x {
  position: absolute; top: -34px; right: -4px;
  background: none; border: 0; color: rgba(255, 255, 255, 0.5);
  font-size: 26px; line-height: 1; cursor: pointer; padding: 4px 8px;
  font-family: var(--font);
}
.ox-modal-x:hover { color: #fff; }
/* Superseded by the close button inside the flow itself, which can sit where it
   belongs instead of floating above the corner. */
.ox-modal-x { display: none; }
/* Lock BOTH — which element scrolls varies by browser. */
html.ox-modal-open, body.ox-modal-open { overflow: hidden; }




/* Asking-line markers. Same treatment as the sold dots — a ring in the card
   colour so overlapping points stay separable — in the asking line's blue, and
   a shade smaller so the primary reads as the primary where they coincide. */
.ah-pt2 {
  fill: var(--blue-bright);
  stroke: var(--card-bg);
  stroke-width: 1.5;
  r: 2.6;
  opacity: .85;
  pointer-events: none;
}


/* The router adds .view-entering for one frame after swapping, then removes it.
   Nothing ever styled it, so the fade it was written for never existed and the
   swap was a hard cut. */
.view-entering { opacity: 0; }
#view, main > .view, [data-view] { transition: opacity .16s ease; }

/* The footer sits after <main>, so it follows the page height. While a view is
   still measuring itself — skeletons resolving, a chart sizing — that height can
   change more than once, and the footer tracked every step of it. A minimum
   height on the content area means it settles once. */
main { min-height: 60vh; }


/* ── Spawner market ─────────────────────────────────────────────────────────
   The picker reuses .spc-mob wholesale from the calculator — same tiles, same
   488px cap, same active treatment — so the two pages feel like one product.
   Only the badge differs, and it means a different thing (see market-view.js). */
/* 40px above, matching .spc-panel and .lbp-panel — the calculator and the
   leaderboard both start there, and this page sits beside them in the nav. */
.mk-wrap { max-width: 760px; margin: 40px auto 0; padding-bottom: 40px; }
.mk-head { text-align: center; padding: 0 0 22px; }
/* Matched to .spc-title / .spc-sub — the calculator is the page this one sits
   beside, and they were a notch apart in size and spacing. */
.mk-h1 { font-family: var(--font-display); font-size: 28px; font-weight: 800; letter-spacing: -0.02em; margin: 0; color: var(--text); }
.mk-h1-sub { font-size: 14px; color: var(--text-mid); margin: 6px 0 0; }

/* A count, not "Soon". The calculator's badge is about measured rates; this one
   is about stock, and a mob can have one without the other. */
.mk-count {
  position: absolute; top: -7px; right: -6px;
  font-size: 9px; font-weight: 700; color: #fff;
  background: var(--blue); border-radius: 100px; padding: 2px 6px;
  font-variant-numeric: tabular-nums;
}
.mk-mob.mk-empty { opacity: .55; }
.mk-mob.mk-empty:hover { opacity: 1; }

.mk-body { padding: 0 4px; }


.mk-rate { background: var(--card-bg); border: 1px solid var(--border); border-radius: 11px; padding: 13px 15px; margin-bottom: 12px; }
.mk-rate-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.mk-rate-l { font-size: 10px; color: var(--text-dim); letter-spacing: .06em; text-transform: uppercase; }
.mk-rate-v { font-size: 16px; font-weight: 800; color: var(--text); margin-top: 3px; font-variant-numeric: tabular-nums; }
.mk-rate-u { font-size: 11px; font-weight: 600; color: var(--text-mid); }
/* Says where the number comes from, because it is derived rather than observed
   and anyone pricing against it deserves to know that. */
.mk-rate-note { font-size: 11px; color: var(--text-dim); margin-top: 10px; padding-top: 9px; border-top: 1px solid var(--border-soft); }
.mk-rate-none { background: var(--card-bg); border: 1px solid var(--border); border-radius: 11px; padding: 13px 15px; margin-bottom: 12px; font-size: 12px; color: var(--text-mid); line-height: 1.5; }

.mk-listhead { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.mk-listcount { font-size: 11.5px; color: var(--text-dim); }
.mk-list-btn { padding: 9px 15px; border: none; border-radius: 9px; background: var(--blue); color: #fff; font-family: var(--font); font-size: 12.5px; font-weight: 700; cursor: pointer; }
.mk-list-btn:hover { filter: brightness(1.08); }

.mk-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 11px; padding: 13px 14px; margin-bottom: 8px; }
.mk-card-warn { border-color: rgba(250,199,117,0.34); }
.mk-row { display: flex; align-items: center; gap: 12px; }
.mk-av { width: 36px; height: 36px; border-radius: 8px; image-rendering: pixelated; background: var(--surface-3); flex-shrink: 0; }
.mk-main { flex: 1; min-width: 0; }
.mk-title { font-size: 13.5px; font-weight: 700; color: var(--text); }
.mk-sub { font-size: 11px; color: var(--text-mid); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mk-seller { color: var(--text); cursor: pointer; }
.mk-seller:hover { color: var(--blue-bright); }
.mk-right { text-align: right; flex-shrink: 0; }
.mk-price { font-size: 16px; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; }
.mk-pb { font-size: 10.5px; margin-top: 2px; font-variant-numeric: tabular-nums; }
.mk-pb-good { color: #4ade80; }
.mk-pb-ok { color: #efa227; }
.mk-pb-poor { color: var(--text-mid); }
.mk-pb-none { color: var(--text-dim); }

.mk-foot { display: flex; align-items: center; gap: 8px; margin-top: 10px; padding-top: 9px; border-top: 1px solid var(--border-soft); }
.mk-btn { padding: 6px 11px; border: 1px solid rgba(255,255,255,0.12); border-radius: 8px; background: rgba(255,255,255,0.04); color: var(--text-mid); font-family: var(--font); font-size: 11px; font-weight: 600; cursor: pointer; }
.mk-btn:hover { color: var(--text); border-color: rgba(255,255,255,0.2); }
.mk-vouch { font-size: 11px; color: var(--blue-bright); cursor: pointer; }
.mk-spacer { flex: 1; }
.mk-exp { font-size: 10.5px; color: var(--text-dim); }

/* Sits under the listing it describes, not over the whole page. A banner above
   everything is ignored by the third visit; this one only appears where it has
   something to say. */
.mk-flag { margin-top: 9px; padding-top: 9px; border-top: 1px solid rgba(250,199,117,0.18); font-size: 11px; color: #f0c56a; line-height: 1.45; }

.mk-empty { background: var(--card-bg); border: 1px solid var(--border); border-radius: 11px; padding: 22px 18px; text-align: center; }
.mk-empty-t { font-size: 13.5px; font-weight: 700; color: var(--text); }
.mk-empty-s { font-size: 12px; color: var(--text-mid); margin-top: 5px; line-height: 1.5; }

@media (max-width: 560px) {
  .mk-rate-grid { grid-template-columns: 1fr 1fr; }
  .mk-foot { flex-wrap: wrap; }
}


/* ── list-a-spawner modal ───────────────────────────────────────────────── */
.mk-backdrop {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(0,0,0,0.8);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  display: flex; align-items: center; justify-content: center; padding: 24px;
  animation: mk-fade .16s ease both;
}
@keyframes mk-fade { from { opacity: 0; } to { opacity: 1; } }
.mk-modal {
  width: min(440px, 100%);
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 16px;
  box-shadow: 0 30px 70px rgba(0,0,0,0.65);
  display: flex; flex-direction: column; overflow: hidden;
}
.mk-modal-head { display: flex; align-items: center; gap: 12px; padding: 15px 18px; border-bottom: 1px solid var(--border-soft); }
.mk-modal-head h2 { margin: 0; font-size: 14px; font-weight: 800; letter-spacing: .03em; text-transform: uppercase; color: var(--text); }
.mk-x { margin-left: auto; width: 26px; height: 26px; padding: 5px; border: none; background: none; color: var(--text-dim); cursor: pointer; border-radius: 7px; }
.mk-x:hover { color: var(--text); background: rgba(255,255,255,0.06); }
.mk-modal-body { padding: 16px 18px; }
.mk-modal-foot { display: flex; align-items: center; gap: 12px; padding: 13px 18px; border-top: 1px solid var(--border-soft); }
/* The one place the escrow warning belongs: next to the button that commits
   you. On the board it was wallpaper by the third visit. */
.mk-modal-note { flex: 1; font-size: 10.5px; color: var(--text-dim); line-height: 1.4; }

.mk-form { display: flex; flex-direction: column; gap: 12px; }
.mk-f { display: flex; flex-direction: column; gap: 5px; }
.mk-f-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.mk-f-l { font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim); }
.mk-f-opt { font-weight: 600; letter-spacing: 0; text-transform: none; opacity: .7; }
.mk-f input, .mk-f select {
  width: 100%; box-sizing: border-box; padding: 9px 11px;
  background: var(--page-bg); border: 1px solid var(--border); border-radius: 9px;
  color: var(--text); font-family: var(--font); font-size: 13px; outline: none;
}
.mk-f input:focus, .mk-f select:focus { border-color: var(--blue-border); }
.mk-f input::placeholder { color: var(--text-dim); }

/* Priced as you type, in the same arithmetic the board uses — so a seller sees
   what a buyer will see before anyone commits to a number. */
.mk-hint { font-size: 11.5px; color: var(--text-mid); line-height: 1.45; min-height: 17px; }
.mk-hint-good { color: #4ade80; }
.mk-hint-ok { color: #efa227; }
.mk-hint-poor { color: #f0846a; }
.mk-f-err { font-size: 11.5px; color: #f87171; }
.mk-f-err[hidden] { display: none; }



@media (max-width: 460px) { .mk-f-row { grid-template-columns: 1fr; } }

/* The max shares the muted treatment the slash has — the live number is the one
   being read; the cap is context. */
.ss-pmax { color: var(--text-mid); font-weight: 700; }


/* Locked, not hidden. The AH legend does this for signed-out visitors: the
   control stays visible and dimmed so the feature is still discoverable, and
   the tooltip says what would unlock it. Hiding the button would hide that
   selling exists here at all. */
.mk-list-btn.is-locked {
  background: var(--card-bg-hover);
  color: var(--text-dim);
  border: 1px solid var(--border);
  cursor: not-allowed;
}
.mk-list-btn.is-locked:hover { filter: none; }


/* ── oxMessaging chat sidebar ───────────────────────────────────────────────
   Docked right, matching .bet: a fixed column the page is padded around rather
   than an overlay. An overlay would cover content people are reading, and the
   whole point of a chat beside a stats table is reading both at once.

   Width tracks .bet's clamp so the two sites feel like one product. */
:root { --chat-w: clamp(219px, 20vw, 284px); }

.chat-sidebar {
  position: fixed; top: 0; right: 0; bottom: 0;
  width: var(--chat-w);
  background: #0a0a0e;
  border-left: 1px solid rgba(255,255,255,0.06);
  z-index: 60;
  display: flex; flex-direction: column;
  transform: translateX(100%);
  transition: transform .28s cubic-bezier(.4,0,.2,1);
}
body.chat-open .chat-sidebar { transform: none; }

#chat-frame { flex: 1; width: 100%; border: 0; display: block; }

/* Content shifts rather than being covered. Transitioned together with the
   panel so nothing tears during the slide. */
body { transition: padding-right .28s cubic-bezier(.4,0,.2,1); }
body.chat-open { padding-right: var(--chat-w); }

.chat-collapse, .chat-reopen {
  position: absolute; top: 8px;
  width: 26px; height: 26px; padding: 5px;
  border: 1px solid rgba(255,255,255,0.1); border-radius: 7px;
  background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.5);
  cursor: pointer; z-index: 2;
}
.chat-collapse { right: 8px; }
.chat-collapse:hover, .chat-reopen:hover { color: #fff; background: rgba(255,255,255,0.09); }

/* The way back in once it is hidden. Fixed to the edge so it is findable
   without being in the way. */
.chat-reopen {
  position: fixed; top: 50%; right: 0; transform: translateY(-50%);
  border-radius: 8px 0 0 8px; border-right: none;
  height: 44px; width: 22px; padding: 4px;
  background: var(--card-bg); z-index: 59;
  opacity: 0; pointer-events: none;
  transition: opacity .2s ease;
}
body:not(.chat-open) .chat-reopen { opacity: 1; pointer-events: auto; }

/* Below the sidebar's breakpoint the page is too narrow to give a fifth of it
   away, so the chat is off entirely rather than squeezed. */
@media (max-width: 1100px) {
  .chat-sidebar, .chat-reopen { display: none; }
  body.chat-open { padding-right: 0; }
}
