/* 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;
  /* `right` animates with the chat panel's own slide so the bar travels with
     it rather than jumping when it lands. Same curve and duration oxmessaging
     uses. */
  transition: opacity .2s, right .28s cubic-bezier(.4,0,.2,1);
  background: transparent;
}
/* With the chat open the scrollbar moves INBOARD of the panel.
   
   This is the request I twice said could not be done — and I was wrong about
   why. The earlier attempts tried to narrow the ROOT so the native scrollbar
   would land inside, which breaks every fixed element on the page. But the
   native bar is hidden (above) and what you actually see is this div, which is
   ours to place. Moving it is one line, and nothing about the page layout
   changes at all. */
html.oxchat-open .ov-scroll-track {
  right: calc(var(--oxchat-w, 284px) + 4px);
}
.ov-scroll-thumb {
  position: absolute; left: 1px; right: 1px; top: 0; width: 8px;
  border-radius: 100px;
  background: rgba(3,111,255,0.28);
  transition: background .15s;
  cursor: grab;
}
.ov-scroll-track:hover .ov-scroll-thumb { background: rgba(3,111,255,0.42); }
.ov-scroll-track.dragging .ov-scroll-thumb { background: rgba(3,111,255,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,
.mob-topbar-logo .white,
.mob-drawer-head .white { color: rgba(255,255,255,0.95); }
.brand .blue,
.mob-topbar-logo .blue,
.mob-drawer-head .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. */
/* Reserves the collapsed wallet's box so the menu OVERLAYS the header instead
   of resizing it.
   
   The height was 42px while every control in the row is 34px, which sat the
   wallet lower than its neighbours. And because the wallet inside is absolute,
   it contributed no WIDTH either — the slot measured zero and the wallet,
   pinned to right:0, extended leftward straight over the promo pill. That is
   the overlap. auth.js measures the collapsed wallet and sets --nav-wallet-w
   after it renders; the fallback keeps the row sane until it does. */
#nav-auth-slot {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: var(--nav-btn-h);
  width: var(--nav-wallet-w, auto);
  min-width: 120px;
  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 {
  flex-shrink: 0;
  margin-top: auto;
  width: 100%;
  box-sizing: border-box;
  background: #0a0a0e;
  border-top: 1px solid rgba(255,255,255,0.05);
}
/* .bet renders 35px with 8px padding — its stylesheet says 34/12 but the inline
   style on the element wins, so 35/8 is what is actually on screen. Ours was
   46/24, which is why the two footers never met in the corner. */
.footer-strip {
  display: flex;
  align-items: center;
  padding: 0 8px;
  height: 35px;
  width: 100%;
  margin: 0;
  gap: 0;
  box-sizing: border-box;
}
.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; }
/* REVERTED: no fixed row height.
   Pinning .crow to 24/20px squashed the rows — the avatar, rank and name each
   need their own natural line box, and forcing the row shorter than its content
   changed how every card looked. The skeleton placeholder mismatch below was the
   real cause of the resize; this was me over-solving it and breaking the layout
   to fix a 4px gap. */
.crow { display: flex; align-items: center; gap: 11px; min-width: 0; }
.cval { white-space: nowrap; }
.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(3,111,255,0.16), rgba(3,111,255,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(3,111,255,0.28), rgba(3,111,255,0.16)); border-color: rgba(3,111,255,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;
}
/* The skeleton placeholder MUST match the real avatar, per context.
   It was hardcoded to 24px while .lcard-compact .chead is 20px — so skeleton
   rows stood 4px taller than the rows that replaced them. Across five rows the
   card shrank 20px the moment data arrived, the carousel band resized, the page
   reflowed, and that reflow is the flash on the homepage. A placeholder whose
   only job is to hold the right space has to hold the RIGHT space. */
.crow-skeleton .chead-ph { width: 24px; height: 24px; }
.lcard-compact .crow-skeleton .chead-ph { width: 20px; height: 20px; }
.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. */
/* Back to the original pair, deliberately.
   `overflow-x: clip; overflow-y: visible` is not strictly legal — a `visible`
   paired with a `clip` computes to `auto` — and I replaced it with `overflow:
   clip` plus a 24px clip margin. That was wrong twice: the cards need far more
   than 24px of vertical room for their hover lift and shadows, so it cropped
   them, and the real cause of the load flash was a skeleton placeholder 4px
   taller than the avatar that replaced it (see .chead-ph below). Fixing the
   right thing made this change unnecessary. */
.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(3,111,255,0.18); border-color: rgba(3,111,255,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; }
/* As many whole tiles as fit, never a sliced one.
   auto-fill works out how many columns of at least 68px the row can hold;
   anything past that wraps onto a second row, which is 0px tall and clipped.
   So the strip always ends on a complete tile — icon and name — and the count
   falls from 14 to 13 to 9 to 7 by itself as the window narrows.
   The min is max(68px, one-fourteenth) so a wide screen still lands on
   exactly 14 rather than making room for a fifteenth column that has nothing
   to put in it. */
.ah-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill,
    minmax(max(68px, calc((100% - 104px) / 14)), 1fr));
  grid-template-rows: auto;
  grid-auto-rows: 0;          /* every row after the first has no height */
  gap: 8px;
  overflow: hidden;           /* ...and is clipped rather than showing a sliver */
}
.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(3,111,255,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(3,111,255,0.28), 0 6px 20px rgba(3,111,255,0.30);
  }
  55% {
    border-color: var(--blue-border);
    box-shadow: 0 0 0 1px rgba(3,111,255,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(3,111,255,0.16), rgba(3,111,255,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(3,111,255,0.28), rgba(3,111,255,0.16));
  border-color: rgba(3,111,255,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;
}
/* ONE bar answers, not all of them.
   
   `.ah-svg:hover .ah-bar` lit the entire row the moment the cursor entered the
   chart, which says "you are over the chart" — something you already know —
   and destroys the two-tone reading of the day precisely when you are trying
   to read a single hour.
   
   Driven by a class, not by :hover. The plot is covered by an invisible hit
   rectangle so the tooltip can track anywhere on the card, and that rect eats
   the pointer before it reaches a bar — so :hover here would never fire at
   all. chart.js applies .is-hot from the same index it uses for the tooltip,
   which also guarantees the lit bar and the reported number agree. */
.ah-bar.is-hot { fill: rgb(34,197,94); }
.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(auto-fill,
    minmax(max(68px, calc((100% - 104px) / 14)), 1fr));
  grid-template-rows: auto;
  grid-auto-rows: 0;
  gap: 8px;
  overflow: hidden;
}
.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.
   Both the auction house and the player page use these classes. */
@media (max-width: 760px) {
  /* No fixed count here any more — .ah-tiles fits whatever the width allows
     and clips the rest, at every size. */
  .ah-hero { flex-direction: column; }
  .ah-hero-price { font-size: 34px; }
}

/* The top-viewed cards hold two columns comfortably well below the width the
   tile strip needs, and one column of ten cards is a long scroll. So they drop
   to a single column later, on their own breakpoint. */
@media (max-width: 620px) {
  .ah-cards, .ah-skeleton-cards { grid-template-columns: 1fr; }
}

/* ============================================================
   Vouches (/vouches) — vouches-view.js
   Trade reputation: leaderboard, recent feed, player profiles.
   ============================================================ */
/* position:relative is what the rail is absolute against. Without a positioned
   ancestor it would hang off the viewport and drift as the page scrolls — the
   same trap the auction house's wrap documents. */
.vch-wrap { max-width: 980px; position: relative; }

/* 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 */
/* Trusted on the left, reported on the right — the whole reason the switch
   could go. */
.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(3,111,255,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; }
/* The name is now the ONLY thing in the middle, so it centres against the
   avatar instead of sitting high above a second line. */
.vch-lb-main { display: flex; align-items: center; 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; }

/* The percentage and its breakdown, as one block on the right.
   
   The tallies are the two numbers the percentage is CALCULATED from, so they
   belong beside it rather than under the name — where they read as a caption
   for the player and forced the name into a two-line box. Stacked, they come
   out the same height as the figure they explain, which is what lets the whole
   row line up on one baseline. */
.vch-lb-right {
  display: flex; align-items: center; gap: 10px;
  flex-shrink: 0;
}
.vch-lb-sub {
  display: flex; flex-direction: column; gap: 2px;
  font-size: 10.5px; color: var(--text-mid);
  line-height: 1;
}
.vch-lb-sub .vch-tally { display: inline-flex; align-items: center; gap: 2px; }
.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;
}

/* ── the profile hero ────────────────────────────────────────────────────
   One card: who they are on the left, what they score on the right. The old
   header put those at opposite ends of the widest element on the page with a
   void between them — this keeps them a single object, and the ring gives the
   percentage a shape so it is read before it is read. */
.vch-hero {
  display: flex; align-items: center; justify-content: space-between;
  gap: 28px; flex-wrap: wrap;
  padding: 26px 30px; margin-top: 4px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.24);
}
.vch-hero-id { display: flex; align-items: center; gap: 18px; min-width: 0; }
.vch-hero-av {
  width: 84px; height: 84px; border-radius: 14px;
  image-rendering: pixelated;
  border: 1px solid var(--border);
}
.vch-hero-who { min-width: 0; }
.vch-hero-name {
  font-family: var(--font-display);
  font-size: 27px; font-weight: 800; letter-spacing: -.02em; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vch-hero-sub { font-size: 12.5px; color: var(--text-mid); margin-top: 5px; }

.vch-hero-score { display: flex; align-items: center; gap: 22px; }

/* A proportion, drawn as one.
   
   conic-gradient rather than an SVG arc: no extra markup to keep in sync, and
   the sweep comes straight from a CSS variable the renderer sets. The inner
   disc is the card's own colour, so the ring reads as a ring rather than as a
   pie with a hole punched in it. */
.vch-ring {
  --pct: 0;
  width: 96px; height: 96px; border-radius: 50%;
  display: grid; place-items: center;
  background:
    conic-gradient(currentColor calc(var(--pct) * 1%), rgba(255,255,255,.07) 0);
  color: var(--text-dim);
  flex-shrink: 0;
}
.vch-ring.good { color: var(--green); }
.vch-ring.mid  { color: var(--gold); }
.vch-ring.bad  { color: var(--red); }
.vch-ring-in {
  width: 76px; height: 76px; border-radius: 50%;
  background: var(--card-bg);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1px;
}
.vch-ring-pct {
  font-family: var(--font-display);
  font-size: 25px; font-weight: 800; line-height: 1; color: currentColor;
}
.vch-ring-pct i { font-style: normal; font-size: 14px; margin-left: 1px; }
.vch-ring-pct.none { color: var(--text-dim); }
.vch-ring-lbl {
  font-size: 9.5px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-dim);
}

/* The two figures the ring is MADE of, stacked beside it — the same pairing
   the leaderboard rows use, so a profile reads as one of those rows opened up
   rather than as a different way of saying the same thing. */
.vch-hero-tallies { display: flex; flex-direction: column; gap: 10px; }
.vch-hero-tally {
  display: flex; align-items: center; gap: 7px;
  font-size: 13px; color: var(--text-mid);
}
.vch-hero-tally b {
  font-family: var(--font-display);
  font-size: 17px; font-weight: 800; color: var(--text);
  min-width: 22px;
}
.vch-hero-tally span { font-size: 11px; color: var(--text-dim); }
.vch-hero-tally.pos { color: var(--green); }
.vch-hero-tally.neg { color: var(--red); }

.vch-list-count { font-size: 11.5px; color: var(--text-dim); }

@media (max-width: 620px) {
  .vch-hero { justify-content: center; text-align: center; }
  .vch-hero-id { flex-direction: column; }
  .vch-hero-name { white-space: normal; }
}

/* cast bar */
/* The action, not a container for it.
   
   It was a full card whose entire contents were a label and two buttons — a
   box drawn around a decision rather than the decision itself. Now it is a
   quieter strip that belongs to the hero above it: no competing surface, a
   hairline top rule instead of a border on all four sides, and the buttons
   given the room they were asking for. */
.vch-cast {
  margin-top: 14px; padding: 16px 30px 18px;
  background: linear-gradient(180deg, rgba(120,160,235,0.035), transparent);
  border: 1px solid var(--border-soft);
  border-radius: 14px;
}
.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; }
/* The same control shape as the payment modal's method rows and oxverify's
   link buttons — surface, hairline border, 10px radius, inset highlight — so a
   decision on this page looks like a decision anywhere else on the estate. */
.vch-cast-btn {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--border); background: var(--surface-3); color: var(--text);
  font-family: var(--font); font-size: 13px; font-weight: 700; padding: 11px 18px;
  border-radius: 10px; cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.05);
  transition: border-color .14s, background .14s, color .14s, transform .1s;
}
.vch-cast-btn:active { transform: translateY(1px); }
.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; }

/* ── the vouches rail ────────────────────────────────────────────────────
   Identical geometry to the players page's, because it IS that rail: pinned to
   the viewport's left edge, out of the flow so the centred columns cannot feel
   it, hidden when there is no room beside the content. The only additions are
   the two verdict colours, which is the one thing a vouch has that a sale or a
   search does not. */
.vch-feed-section {
  position: absolute; top: 0;
  left: calc(50% - 50vw + 18px);
  width: 122px; margin: 0; padding: 0;
}
.vch-feed-section .ah-sec-title-row { padding: 0 0 0 2px; margin-bottom: 8px; }
.vch-feed-section .ah-sec-title { font-size: 11px; letter-spacing: .04em; opacity: .75; }
.vch-feed-section .ah-feed-clip { height: 100%; min-height: 240px; }
@media (max-width: 1240px) { .vch-feed-section { display: none; } }

/* The verdict sits BESIDE the name, not above it.
   
   A sale's headline is a price and deserves its own line; a verdict is an
   attribute of the person, and above the name it read as a value they had
   rather than a judgement about them. On one line the pair is a name and its
   badge, which is what it is.
   
   The chip is the site's own control shape — the same surface, border, radius
   and inset highlight as the buttons in the payment modal and the Minecraft
   link flow — so it reads as part of the family rather than a bare glyph. */
.vch-card .ah-feed-price { display: none; }

/* The verdict sits beside the FACE, and out of the flow.
   
   `.ah-feed-top` is what centres the avatar on the card, so anything added to
   that line pushes the face off-centre — and a card with a chip would then be
   aligned differently from one without. Absolute keeps the badge where the eye
   expects it while the avatar stays exactly where it is on every other card in
   every other feed.
   
   SOLID, not tinted. A translucent badge picks up whatever it is sitting on,
   so the same verdict read as a different colour depending on the card
   underneath it — the one thing a verdict must never do. */
.ah-feed-top { position: relative; }
.ah-feed-chip {
  position: absolute;
  top: 50%; left: calc(50% + 15px);
  transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 17px; height: 17px;
  border-radius: 6px;
  color: #08130c;
  background: var(--green);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.22), 0 2px 5px rgba(0,0,0,.4);
  pointer-events: none;
}
.ah-feed-chip .vch-ico { width: 11px; height: 11px; }
.vch-card-pos .ah-feed-chip { background: var(--green); color: #08130c; }
.vch-card-neg .ah-feed-chip { background: var(--red);   color: #1a0808; }
.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;
  /* Narrower, not shorter. The old 210px was sized for "19,923 / 35,000" — two
     five-digit numbers and a slash — and with just the live count it left a wide
     empty band either side. The HEIGHT was right, so the padding is unchanged. */
  min-width: 150px;
  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-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. */
/* Width lives with the other tokens at the end of this file — it is tied to
   the row's reserved middle track, and two sources for it is how the tab and
   the range ended up overlapping. */
.ss-live-tab { justify-content: center; }
.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. */
/* Centred in the space that ACTUALLY remains, not in a guess at it.
   `body` is already a flex column with `min-height: 100vh` and the footer on
   `margin-top: auto`, so `main` occupies exactly the gap between nav and footer.
   Filling that and centring inside it gives equal space above and below with no
   magic number — `100vh - 300px` was a guess at the chrome height, and being
   wrong by any amount pushes the content off-centre by that much. */
/* flex:1 is the part that was missing.
   `display:flex` alone made main a flex CONTAINER, but main was still only
   min-height:60vh — so the content centred inside 60vh, which begins directly
   under the nav and looks top-aligned. body is a flex column with the footer on
   margin-top:auto, so main has to GROW to claim the gap before centring in it
   means anything. */
main:has(> .ss-wrap) { display: flex; flex: 1 1 auto; }

.ss-wrap {
  flex: 1 1 auto;
  display: flex; flex-direction: column; justify-content: center;
  width: 100%;
}

/* Fallback for browsers without :has() — the old behaviour, so the page is
   never worse than it was. */
@supports not selector(:has(*)) {
  .ss-wrap { min-height: calc(100vh - 300px); }
}

/* The header row's older layout lived here — a flex row with a 268px reserve
   sized for the tab's old fixed width. Superseded by the token-driven grid at
   the end of this file; two competing definitions of the same row is how it
   drifted out of alignment in the first place. */

/* 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(3,111,255,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(3,111,255,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; }

/* Hidden, not removed.
   
   The overlay is built once, during an idle moment, with the frame already
   loading inside it — so opening is a class change rather than a page load
   from another origin. Removing it (or moving the frame) would RELOAD the
   iframe and throw all of that away, which is the mistake oxbilling's modal
   made and had to be restructured to undo.
   
   visibility and pointer-events go with it: an overlay at opacity 0 still
   swallows every click on the page underneath. */
.ox-modal[hidden] {
  display: none !important;
}
.ox-modal:not(.ox-shown) {
  pointer-events: none;
}
.ox-modal.ox-shown { pointer-events: auto; }

/* 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; }




/* ═══ PRO (/pro) — pro-view.js ═══════════════════════════════════════════
   The badge is the only gold on the site. Everything else here stays in the
   normal blue so the page reads as part of the site rather than an ad for it —
   the tier is the exception, so the colour is too. */

.pro-wrap { max-width: 760px; margin: 0 auto; padding: 8px 0 60px; }

.pro-head { text-align: center; padding: 26px 0 34px; }

/* ── the PRO label ──────────────────────────────────────────────────────
   
   ONE mark for PRO, everywhere it appears: this page, chat, the modal, any
   plan name. It is the header pill's face lifted out of the header — the
   gradient people already associate with the button they pressed to get here.
   
   Purple -> magenta -> amber -> yellow, drifting across a background twice the
   element's width. The values are duplicated in oxbilling's payment frame and
   in oxmessaging's emblem, because those are separate origins and cannot read
   this file; if the ramp ever changes it has to change in all three. */
.pro-tag {
  display: inline-block;
  font-family: var(--font-display, var(--font));
  font-size: 12px; font-weight: 900; letter-spacing: -.01em; line-height: 1;
  color: #fff;
  padding: 4px 8px;
  border-radius: 7px;
  text-shadow: 0 1px 2px rgba(0,0,0,.3);
  background-image: linear-gradient(100deg, #7c3aed, #d946ef 36%, #fbbf24 70%, #fde047);
  background-size: 220% 100%;
  /* Every PRO badge on every screen shows the same colour at the same second.
     
     A CSS animation starts when the ELEMENT does, so two badges on one page
     drifted apart if one was rendered later, and no two visitors ever matched.
     A negative delay starts the animation part-way through: set it from the
     wall clock and the phase is a function of the time, not of when the badge
     happened to appear. --pro-delay is written once per page (see auth.js) and
     inherited, so a badge created later is already in step. */
  animation: proTagShift 6s ease-in-out infinite;
  animation-delay: var(--pro-delay, 0s);
}
@keyframes proTagShift {
  0%, 100% { background-position: 0% 50%; }
  50%      { background-position: 100% 50%; }
}
/* Inline beside body text — a badge the height of a capital rather than a
   button sitting in the middle of a sentence. */
.pro-tag.sm { font-size: 10.5px; padding: 3px 6px; border-radius: 6px; }
@media (prefers-reduced-motion: reduce) { .pro-tag { animation: none; } }

/* The page header's copy, at a size that can carry the page. Same face, same
   gradient — only bigger, so it reads as the mark enlarged rather than as a
   second badge that happens to say PRO. */
.pro-badge-lg {
  display: inline-block;
  font-family: var(--font-display, var(--font));
  font-size: 20px; font-weight: 900; letter-spacing: -.01em; line-height: 1;
  color: #fff;
  padding: 7px 14px;
  border-radius: 10px;
  margin-bottom: 18px;
  text-shadow: 0 1px 2px rgba(0,0,0,.3);
  background-image: linear-gradient(100deg, #7c3aed, #d946ef 36%, #fbbf24 70%, #fde047);
  background-size: 220% 100%;
  animation: proTagShift 6s ease-in-out infinite;
  animation-delay: var(--pro-delay, 0s);
  box-shadow: 0 6px 20px rgba(217,70,239,.22);
}
@media (prefers-reduced-motion: reduce) { .pro-badge-lg { animation: none; } }
.pro-title {
  font-family: var(--font-display);
  font-size: clamp(26px, 4.4vw, 36px); font-weight: 700;
  letter-spacing: -.025em; margin: 0 0 10px; color: var(--text);
}
.pro-sub {
  color: var(--text-mid); font-size: 14.5px; margin: 0 auto;
  max-width: 52ch; line-height: 1.6;
}

.pro-perks { list-style: none; padding: 0; margin: 0 0 34px; display: grid; gap: 8px; }
.pro-perk {
  display: flex; gap: 14px; align-items: baseline;
  background: var(--card-bg); border: 1px solid var(--border-soft);
  border-radius: var(--radius-card); padding: 14px 18px;
}
.pro-perk-name { font-weight: 600; font-size: 14px; color: var(--text); flex: 0 0 168px; }
.pro-perk-desc { color: var(--text-mid); font-size: 13.5px; }

.pro-plans { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.pro-plan {
  position: relative;
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: var(--radius-card); padding: 22px 18px 18px;
  display: flex; flex-direction: column; text-align: center;
}
.pro-plan.is-best { border-color: var(--blue-border); background: var(--card-bg-hover); }
.pro-plan-flag {
  position: absolute; top: -9px; left: 50%; transform: translateX(-50%);
  font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--blue-bright);
  background: var(--surface-3); border: 1px solid var(--blue-border);
  border-radius: var(--radius-pill); padding: 2px 10px; white-space: nowrap;
}
.pro-plan-name { font-size: 13px; color: var(--text-mid); font-weight: 600; }
.pro-plan-price {
  font-family: var(--font-display); font-weight: 700;
  font-size: 34px; letter-spacing: -.03em; color: var(--text); margin: 6px 0 2px;
}
.pro-plan-per { font-size: 13px; font-weight: 400; color: var(--text-dim); margin-left: 3px; }
.pro-plan-note { font-size: 12.5px; color: var(--text-dim); margin-bottom: 18px; }

.pro-buy {
  margin-top: auto; width: 100%;
  background: var(--blue); color: #08101f;
  border: 0; border-radius: 9px; padding: 10px;
  font-family: var(--font); font-size: 13.5px; font-weight: 700; cursor: pointer;
}
.pro-buy:hover { filter: brightness(1.08); }
.pro-buy:disabled { opacity: .55; cursor: not-allowed; filter: none; }
.pro-plan.is-best .pro-buy { background: var(--blue-bright); }

.pro-owned {
  background: var(--card-bg); border: 1px solid var(--border-soft);
  border-radius: var(--radius-card); padding: 18px;
}
.pro-owned p { margin: 0; color: var(--text-mid); font-size: 13.5px; }

.pro-msg { min-height: 20px; margin-top: 16px; text-align: center;
  font-size: 13.5px; color: var(--text-mid); }
.pro-msg.is-bad { color: var(--red); }

.pro-restore {
  display: block; margin: 4px auto 0;
  background: none; border: 0; color: var(--text-dim);
  font-family: var(--font); font-size: 12.5px; cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px;
}
.pro-restore:hover { color: var(--text-mid); }

/* The inline badge, wherever a PRO member's name appears. */
.pro-tag {
  display: inline-block; vertical-align: middle;
  font-family: var(--mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .09em; line-height: 1;
  color: var(--gold);
  border: 1px solid rgba(245,158,11,.38);
  background: rgba(245,158,11,.10);
  border-radius: 4px; padding: 3px 5px; margin-left: 6px;
}

@media (max-width: 620px) {
  .pro-plans { grid-template-columns: 1fr; }
  .pro-perk { flex-direction: column; gap: 4px; }
  .pro-perk-name { flex: none; }
}



/* ═══ Live rails + the chat panel ════════════════════════════════════════
   All of this is OURS. oxmessaging's embed script deliberately doesn't touch
   the host page's layout, so making room for the chat is this stylesheet's job.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── the toggle's lane ──────────────────────────────────────────────────
   The chat's open/close button is fixed to the top-right corner, which is
   where Sign in sits. 34px wide with an 8px inset, so 50px of clearance puts
   them side by side. Only above the chat's own breakpoint — below it the
   button isn't rendered and this would be dead space. */
@media (min-width: 1101px) {
  .navbar { padding-right: 82px; }   /* 32 base + 50 for the button */
}

/* ── rails run the height of the content ────────────────────────────────
   They were measured to the bottom of the top-cards grid, so on a tall screen
   they stopped a third of the way down and read as cut off. Equal insets top
   and bottom instead, so the column is centred in the space it has.

   NO min-height here. Pinning it to the viewport made the rail taller than the
   wrap it is positioned inside, which is what pushed it through the footer on
   a tall screen — the wrap is the thing it must stay within. */
@media (min-width: 1241px) {
  .ah-feed-section,
  .ps-feed-section {
    top: 26px;
    bottom: 26px;
    height: auto !important;   /* overrides the inline height the views set */
  }
  .ah-feed-section .ah-feed-clip,
  .ps-feed-section .ah-feed-clip { height: 100%; }
}

/* ── the headings ───────────────────────────────────────────────────────
   Centred over the column rather than left-aligned to a tile edge, and set in
   small caps: at this size, letterspaced caps stay legible where 11px
   sentence case goes muddy. */
.ah-feed-section .ah-sec-title-row,
.ps-feed-section .ah-sec-title-row {
  display: block;
  text-align: center;
  padding: 0;
  margin-bottom: 10px;
}
.ah-feed-section .ah-sec-title,
.ps-feed-section .ah-sec-title {
  display: block;
  font-size: 9.5px;
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-dim);
  opacity: 1;
}

/* ── both at once, when there is room ───────────────────────────────────
   With the chat open the page is narrower by the panel, and the rail is
   positioned from the VIEWPORT's centre (50% - 50vw). Once the body is padded,
   the wrap's 50% no longer matches the viewport's, so the rail was computing a
   position half a panel too far left — off the screen entirely. That, not the
   breakpoint, is why a wide desktop still showed no rail.

   Half the panel back puts it where it belongs. */
/* The rail's LEFT changes when the panel opens, because the body's padding
   moves the wrap's centre. Without a transition that is a jump, landing in the
   middle of the panel's own 280ms slide — which is what stopped the two
   feeling like one movement.

   Only `left` is transitioned. A blanket transition on the section also lands
   on the tile column's slide, which is already right and must not be touched.
   Duration and curve are oxmessaging's own, so they finish together. */
/* Leaving mirrors arriving.
   Only `left`, `opacity` and `visibility` are transitioned — never a blanket
   `all`, which would also catch the tile column's own slide inside the rail
   and that animation is already right. */
.ah-feed-section,
.ps-feed-section {
  transition: left .28s cubic-bezier(.4,0,.2,1),
              opacity .28s cubic-bezier(.4,0,.2,1),
              transform .28s cubic-bezier(.4,0,.2,1),
              visibility 0s linear 0s;
  opacity: 1;
  visibility: visible;
}
html.oxchat-open .ah-feed-section,
html.oxchat-open .ps-feed-section {
  left: calc(50% - 50vw + 18px + (var(--oxchat-w, 284px) / 2));
}

/* A rail wants ~1240px. With the chat open it also wants the panel's width on
   top of that, or it crowds the centre column. Above this, both show; below
   it the rail steps aside, since the chat is what you actively opened.

   It SLIDES OUT the way it slid in — the same duration and curve, just
   reversed — rather than vanishing on display:none, which cannot animate at
   all. visibility is delayed by the length of the slide so the rail is still
   painted while it moves, then stops taking pointer events once it is gone;
   opacity alone would leave an invisible element sitting over the page. */
@media (max-width: 1540px) {
  html.oxchat-open .ah-feed-section,
  html.oxchat-open .ps-feed-section {
    opacity: 0;
    transform: translateX(-24px);
    visibility: hidden;
    pointer-events: none;
    transition: left .28s cubic-bezier(.4,0,.2,1),
                opacity .28s cubic-bezier(.4,0,.2,1),
                transform .28s cubic-bezier(.4,0,.2,1),
                visibility 0s linear .28s;
  }
}




/* ════════════════════════════════════════════════════════════════════════
   TABLET / MOBILE HEADER — ported from oxlicensing
   See public/js/mobile-nav.js. The rules here are structural only: the
   drawer's TRANSFORM is deliberately absent, because the browser applying
   one as the viewport crosses the breakpoint is what makes it jump during a
   resize. JS owns that property outright.
   ═══════════════════════════════════════════════════════════════════════ */

.mob-topbar {
  display: none;
  position: fixed; top: 0; left: 0; right: 0; height: 66px; z-index: 200;
  background: var(--page-bg);
  border-bottom: 1px solid var(--border-soft);
  backdrop-filter: blur(8px);
  align-items: center; justify-content: space-between;
  padding: 0 1rem; gap: .75rem;
  transform: translateY(-100%); opacity: 0; pointer-events: none;
}

/* Absolutely centred, so it is centred on the VIEWPORT rather than on
   whatever space the hamburger leaves — which is what keeps it steady when
   the drawer side flips. */
.mob-topbar-logo {
  position: absolute; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 11px;
  cursor: pointer; pointer-events: auto;
}
/* The desktop brand, unchanged: same 48px logo, same 21px wordmark, same
   optical nudge. Scaling it down made the header look like a different site's
   at a smaller size — and the pair always fits, even at 320px. */
.mob-topbar-logo img {
  width: 48px; height: 48px; border-radius: 11px;
  flex-shrink: 0; display: block;
}
.mob-topbar-logo .brand-text {
  font-size: 21px; font-weight: 800; letter-spacing: -0.01em;
  display: inline-block; line-height: 1;
  transform: translateY(0.04em); transform-origin: left center;
}

.mob-hamburger {
  position: absolute; top: 50%; margin-top: -20px;
  width: 40px; height: 40px;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 5px;
  background: transparent; border: none; cursor: pointer; padding: 4px;
  flex-shrink: 0;
  -webkit-tap-highlight-color: transparent;
}
/* Thumb side on touch, pointer side on a merely-narrow desktop window. */
body.is-touch .mob-hamburger { right: 7px; }
body:not(.is-touch) .mob-hamburger { left: 7px; }

.mob-hamburger span {
  display: block; width: 22px; height: 2px;
  background: var(--text-mid); border-radius: 2px;
  transition: transform .28s cubic-bezier(.4,0,.2,1),
              opacity   .2s  cubic-bezier(.4,0,.2,1),
              width     .28s cubic-bezier(.4,0,.2,1);
}
/* Top bar rotates to \ and bottom to / forming an X; the middle collapses to
   nothing rather than fading in place, so the two strokes look like they
   travelled there. */
.mob-hamburger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.mob-hamburger.open span:nth-child(2) { opacity: 0; width: 0; transform: scaleX(0); }
.mob-hamburger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.mob-drawer {
  position: fixed; top: 0; bottom: 0; width: 272px; z-index: 199;
  background: var(--surface-2, var(--page-bg));
  display: none; flex-direction: column;
  overflow-y: auto;
  will-change: transform;
  /* transform intentionally absent — JS owns it */
}
body.is-touch .mob-drawer {
  right: 0; left: auto; border-left: 1px solid var(--border);
  box-shadow: -8px 0 40px rgba(0,0,0,.7);
}
body:not(.is-touch) .mob-drawer {
  left: 0; right: auto; border-right: 1px solid var(--border);
  box-shadow: 8px 0 40px rgba(0,0,0,.7);
}

.mob-drawer-head {
  display: flex; align-items: center; gap: 11px;
  height: 66px; padding: 0 18px; flex-shrink: 0;
  border-bottom: 1px solid var(--border-soft);
}
.mob-drawer-head img { width: 38px; height: 38px; border-radius: 9px; }
.mob-drawer-head .brand-text {
  font-size: 17px; font-weight: 800; letter-spacing: -0.01em;
  display: inline-block; line-height: 1; transform: translateY(0.04em);
}

.mob-drawer-nav { display: flex; flex-direction: column; padding: 10px 8px; gap: 2px; }
.mob-drawer-row {
  padding: 12px 14px; border-radius: 9px;
  font-family: var(--font); font-size: 14px; font-weight: 600;
  color: var(--text-mid); cursor: pointer;
  transition: color .12s, background .12s;
}
.mob-drawer-row:hover, .mob-drawer-row:active {
  color: var(--text); background: var(--card-bg-hover);
}
/* Items from under "Other": indented and quieter, so a flattened list still
   has a shape. */
.mob-drawer-row.is-sub { padding-left: 28px; font-size: 13px; color: var(--text-dim); }
.mob-drawer-row.is-sub:hover { color: var(--text-mid); }

.mob-drawer-foot {
  margin-top: auto; padding: 12px; border-top: 1px solid var(--border-soft);
  display: flex; flex-direction: column; gap: 8px;
}
.mob-drawer-foot .nav-pill { width: 100%; justify-content: center; margin: 0; }

.mob-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 198;
  display: none; opacity: 0; pointer-events: none;
  -webkit-tap-highlight-color: transparent;
  transition: opacity .38s ease;
}

/* ── the switch ─────────────────────────────────────────────────────────
   nav-collapsed is set by JS, not by a media query, so the bar and the
   topbar can never both be visible for a frame mid-transition. */
body.nav-collapsed .navbar { display: none; }
body.nav-collapsed { padding-top: 66px; }
body.nav-collapsed .mob-topbar { display: flex; }
body.nav-collapsed .mob-drawer { display: flex; }

/* The chat toggle's lane is only needed while the desktop bar is showing. */
@media (max-width: 1130px) {
  .navbar { padding-right: 32px; }
}

/* The wordmark is never dropped. Logo and name together are the brand; the
   pair measures ~200px, which fits the narrowest phone with room to spare. */


/* ═══ Server stats ═══════════════════════════════════════════════════════
   One band above the card holds the title, the live tab and the range. All
   three are positioned by the CARD, not by the page, so they stay in the same
   relationship at every width instead of drifting apart as the window changes.

   Sizes are driven by four tokens. The desktop values are the CEILING —
   everything only shrinks from there, because the desktop proportions were
   already right and growing into the window read as oversized.
   ═══════════════════════════════════════════════════════════════════════ */

.ss-wrap {
  /* main is a flex container here, so `width` alone loses to flex stretching —
     that is why the card ran edge to edge with no gutter. flex:0 0 auto makes
     the width authoritative; align-self keeps it centred once it stops
     stretching. */
  flex: 0 0 auto;
  align-self: center;
  /* 1080, up from 900.
     
     Server stats is ONE card on an otherwise empty page — no list beside it,
     nothing below — so it was carrying the most whitespace of any page on the
     site while showing the chart that most rewards being bigger. Everything
     around it is expressed in vw clamps, so widening the wrap scales the tab,
     the title and the range pills with it rather than leaving them adrift in a
     larger box. */
  width: min(1080px, calc(100vw - 40px));
  max-width: 1080px;
  margin: 0 auto;

  --ss-icon:    clamp(21px, 2.2vw, 27px);   /* the tab's donut */
  --ss-tab-padx: clamp(16px, 2.4vw, 26px);  /* a little wider than before */
  --ss-tab-h:   calc(var(--ss-icon) + 22px);
  /* The middle track: what the row keeps clear for the tab. It is the tab's
     own maximum width, so the title and the range can never be pushed into
     it — which is how they ended up overlapping on a narrow window. */
  --ss-reserve: clamp(148px, 26vw, 210px);
}

.ss-chart-wrap { margin: clamp(26px, 6vh, 60px) auto; position: static; width: 100%; }

/* The card is the anchor for everything above it. */
.ss-card-anchor { position: relative; margin-top: var(--ss-tab-h); }

/* ── the band ───────────────────────────────────────────────────────────
   Absolute against the card and exactly as tall as the tab, so its contents
   centre on the tab's icon rather than sitting a few pixels high — which is
   what made the title look unaligned. */
.ss-chart-head-outer {
  position: absolute; left: 0; right: 0; bottom: 100%;
  height: var(--ss-tab-h);
  display: grid;
  /* minmax(0, 1fr), NOT 1fr.
     
     A bare `1fr` is `minmax(auto, 1fr)` — it will not shrink below its
     content. So a long title on the left, or the range group on the right when
     "All-time" is showing, pushed its own track wider than the other and the
     middle track — the tab — slid off the card's centre. Nobody could see why:
     the tab looked misaligned while the thing actually misaligning it was a
     word at the far end of the row.
     
     Flooring both side tracks at zero makes them mathematically equal
     whatever they hold, so the middle is the middle. Always. */
  grid-template-columns: minmax(0, 1fr) var(--ss-reserve) minmax(0, 1fr);
  align-items: center;
  margin: 0;
}
.ss-chart-head-outer .ss-chart-title {
  grid-column: 1;
  display: flex; align-items: center; justify-content: center;
  width: auto; min-width: 0; min-height: 0;
  position: static; transform: none; margin: 0; padding: 0 8px;
  background: none; text-align: center; white-space: nowrap;
  /* The side tracks can now shrink, so the title has to be able to as well —
     otherwise it just overflows its track and the visual problem is back with
     no grid rule to blame. */
  overflow: hidden; text-overflow: ellipsis;
  font-family: var(--font-display);
  font-size: clamp(13.5px, 1.4vw, 16px); font-weight: 800;
  letter-spacing: -0.01em; color: var(--text);
}
.ss-head-gap { grid-column: 2; }
.ss-chart-head-outer .ah-chart-range {
  grid-column: 3;
  /* max-content, not auto: a grid item stretches by default, and this one has
     a visible track behind it. Stretched, the pill sat in a wide empty box. */
  width: max-content;
  justify-self: end;
  align-self: center;
}
/* Fixed, deliberately. The slider pill is positioned from a measurement of
   the active button, and it only re-measures on resize — so a button whose
   width changes with the viewport leaves the pill behind it mid-drag. */
.ss-chart-head-outer .ah-range-btn {
  font-size: 10px; padding: 5px 10px; letter-spacing: .03em;
}

/* ── the tab ────────────────────────────────────────────────────────────
   Sits in the reserved middle track and merges with the card's top border.
   Its width follows the same token as the reserve, so it can never grow into
   the range buttons on a small screen. */
.ss-live-tab {
  width: auto;
  min-width: var(--ss-reserve);
  max-width: var(--ss-reserve);
  padding: 10px var(--ss-tab-padx) 12px;
  gap: clamp(7px, 0.9vw, 10px);
}
.ss-tab-icon { width: var(--ss-icon); height: var(--ss-icon); }
.ss-live-tab { align-items: center; }
.ss-live-inner { display: inline-flex; align-items: center; line-height: 1; }
.ss-po {
  font-size: clamp(15px, 1.5vw, 18px);
  line-height: 1;
  display: inline-block;
  transform: translateY(0.045em);
}

/* ── the card ───────────────────────────────────────────────────────────
   Padding and plot height both follow the card's WIDTH, so the chart keeps
   its proportions as the card narrows. Tying the height to vh alone was what
   made the contents feel detached from the box around them. */
.ss-chart-card { padding: clamp(14px, 2.4vw, 24px); }
.ss-chart-card .ah-chart-body { height: clamp(220px, 32vw, 360px); }
.ss-chart-card .ah-svg { height: 100%; }

/* Narrow: the tab already says what the number is, so the title is the thing
   to drop — losing the range would cost a control, losing the tab the data. */
@media (max-width: 620px) {
  .ss-chart-head-outer { grid-template-columns: 0 var(--ss-reserve) 1fr; }
  .ss-chart-head-outer .ss-chart-title { display: none; }
}





/* A point that sits above the plot's ceiling, pinned to the top edge.
   The axis is built around the bulk of the data so one buyout spike cannot
   flatten it — but the spike still happened, so it is marked rather than
   dropped. The caret says "this continues past here"; the hover gives the
   real number. */
.ah-offscale {
  fill: none;
  stroke: var(--blue, #036fff);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: .85;
}


/* ═══ Homepage hero ══════════════════════════════════════════════════════ */

.home-hero {
  position: relative;
  text-align: center;
  padding: clamp(38px, 6vw, 64px) 20px clamp(26px, 3vw, 38px);
}
/* A soft bloom behind the wordmark, in the brand blue. Sized in vw so it
   scales with the type rather than sitting as a fixed blob on a wide screen. */
.home-hero::before {
  content: "";
  position: absolute;
  top: -46%; left: 50%;
  width: min(700px, 90vw); height: 340px;
  transform: translateX(-50%);
  background: radial-gradient(ellipse at center, rgba(3,111,255,0.10), transparent 68%);
  pointer-events: none;
}
.home-hero > * { position: relative; }

.home-hero-title {
  font-family: var(--font-display, var(--font));
  font-size: clamp(40px, 7.5vw, 76px);
  font-weight: 800;
  letter-spacing: -0.045em;
  line-height: 1;
  margin: 0;
  color: var(--text);
}
.home-hero-title .blue { color: var(--blue); }

.home-hero-sub {
  font-size: clamp(14px, 2vw, 19px);
  font-weight: 500;
  color: var(--text-dim);
  margin: clamp(14px, 2vw, 20px) 0 0;
}
/* The three nouns are the claim; lifting them out of the sentence is what
   makes it read as a promise rather than a description. */
.home-hero-sub b { color: var(--text-mid); font-weight: 600; }

.home-hero-stats {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: clamp(30px, 4vw, 44px);
}
.hh-stat { flex: 0 1 190px; padding: 0 14px; }
.hh-num {
  font-size: clamp(24px, 3.4vw, 34px);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  /* The count settles in from the placeholder without the row resizing. */
  transition: opacity .3s ease;
}
.hh-num.is-pending { opacity: .35; }
.hh-lab {
  font-size: 10.5px;
  font-weight: 800;
  /* The tracking is what threw it off. Letter-spacing adds a trailing space
     after the LAST character too, so a centred line sits half a space left of
     true centre — visible against a big number directly above it. Half the
     tracking as left padding puts it back. */
  letter-spacing: .16em;
  padding-left: .16em;
  text-align: center;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-top: 9px;
}
.hh-div {
  width: 1px;
  background: linear-gradient(180deg, transparent, var(--border), transparent);
}

@media (max-width: 560px) {
  .hh-stat { flex: 1 1 33%; padding: 0 6px; }
  .hh-div { display: none; }
}




/* ═══ Header right side ══════════════════════════════════════════════════
   Three controls that must read as one row: the promo pill, then either Sign
   in or the account wallet.

   Everything here is driven off one number. Setting each control's height
   separately is what left them a pixel or two apart no matter how carefully
   the paddings were matched — and the account wallet has different padding
   again, so a third value would have drifted the moment anyone signed in. */
:root { --nav-btn-h: 34px; }

.nav-right {
  align-items: center;
  /* The pair sits closer to each other than to the rest of the nav, so they
     read as a group. */
  gap: 12px;
}

/* .nav-wallet is deliberately NOT in this list. It is the absolutely
   positioned shell that GROWS downward into the menu, so a fixed height would
   clip the dropdown shut. Only its head — the part that is actually the
   button — gets the shared height. */
.nav-right .nav-pill,
.nav-right .btn-discord-signin,
.nav-right .nav-wallet-head {
  height: var(--nav-btn-h);
  box-sizing: border-box;
  margin: 0;
}
.nav-right .nav-pill,
.nav-right .btn-discord-signin {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  vertical-align: middle;
}
/* Inline images and SVGs sit on the TEXT BASELINE by default, which drops the
   whole button a couple of pixels relative to its neighbour and is why these
   never lined up. display:block takes them off the baseline entirely. */
.nav-right .nav-pill img,
.nav-right .nav-pill svg,
.nav-right .btn-discord-signin svg {
  display: block;
  flex-shrink: 0;
}
/* Sign in's own padding fought the fixed height; let the height win and centre
   the contents inside it. */
.nav-right .btn-discord-signin { padding: 0 16px; }
/* Signed in, the wallet's head is the control — it needs the same treatment or
   the pill is suddenly misaligned against a DIFFERENT neighbour. */
.nav-right .nav-wallet-head { padding-top: 0; padding-bottom: 0; }

.nav-right .nav-pill {
  /* A FIXED width, not a minimum.
     
     The two faces have different content — "Add to Discord" with a 22px mark
     against a short "Buy PRO" with none — so a min-width let the button resize
     mid-morph, and everything to its left shifted with it. Sized to hold the
     wider face, it stays put and only the contents change. ~1.5x the sign-in
     button, which is also the rhythm the pair wants. */
  width: 186px;
  flex: 0 0 186px;
  padding: 0 16px;
  gap: 8px;
}
/* The icon and the label are centred TOGETHER, not each in its own share of
   the width — that is what made the text look pushed to one side. The icon is
   given no flex room of its own, so the pair sits as one block in the middle. */
.nav-right .nav-pill .nav-pill-text {
  display: block;
  line-height: 1;
  /* Sora's cap height sits slightly high in its line box; this centres the
     visible letters rather than the box around them. */
  transform: translateY(0.5px);
}

/* The chat toggle is pinned to the top-right corner whether the panel is open
   or closed, so the row reserves its lane rather than letting the controls
   drift underneath it. 34px button + 8px inset + 8px clearance. */
@media (min-width: 1101px) {
  .navbar { padding-right: 82px; }
}

/* ── the promo pill ────────────────────────────────────────────────────
   Alternates between the bot invite and PRO. The PRO face is deliberately the
   loudest thing in the header — it is the only paid thing on the site, and a
   pill that looks like every other control gets read as navigation. */
/* ── the morph ──────────────────────────────────────────────────────────
   The colour change is a CROSS-FADE, not a swap.

   background-image cannot be transitioned, so the PRO gradient lives on a
   ::before layer that fades in over the Discord face. Both are painted at once
   during the change, which is what makes it read as the button becoming
   something else rather than two buttons taking turns. */
.nav-right .nav-pill { position: relative; isolation: isolate; }
.nav-right .nav-pill::before {
  content: "";
  position: absolute; inset: -1px;      /* covers the border too */
  border-radius: inherit;
  background-image: linear-gradient(100deg, #7c3aed, #d946ef 36%, #fbbf24 70%, #fde047);
  background-size: 220% 100%;
  opacity: 0;
  transition: opacity .5s cubic-bezier(.4,0,.2,1);
  z-index: -1;
  pointer-events: none;
}
.nav-right .nav-pill.is-pro::before {
  opacity: 1;
  animation: navProShift 6s ease-in-out infinite;
  animation-delay: var(--pro-delay, 0s);
}
/* The border and the surface fade on the same curve, so the whole face turns
   together instead of the fill arriving before the edge. */
.nav-right .nav-pill {
  transition: background-color .5s cubic-bezier(.4,0,.2,1),
              border-color .5s cubic-bezier(.4,0,.2,1),
              color .5s cubic-bezier(.4,0,.2,1),
              box-shadow .5s cubic-bezier(.4,0,.2,1),
              transform .14s;
}

.nav-pill.is-pro {
  border-color: transparent;
  color: #fff;
  background-color: transparent;
  /* The gradient itself is on ::before so it can cross-fade; this element only
     carries the colour and shadow, which CAN transition. Purple -> magenta ->
     amber -> yellow: the orange stop read muddy on the dark bar, and yellow
     keeps the warm end reading as premium rather than as a warning. */
  background-image: none;
  /* The glow is a blurred copy of the gradient itself, not a fixed magenta —
     see .nav-pill.is-pro::after below. A static shadow under a shifting
     gradient is what made the highlight look detached from the colour. */
  box-shadow: none;
}
@keyframes navProShift {
  0%, 100% { background-position: 0% 50%; }
  50%      { background-position: 100% 50%; }
}
/* Deliberately no brightness filter: PRO's face stays exactly as it is on
   hover. The halo answers instead — see the ::after rule further down. */
/* The highlight IS the gradient.
   
   A second copy of the same layer, blurred and pushed behind, so the glow
   moves and changes colour with the fill instead of sitting under it as a
   fixed magenta halo. It fades on the same curve as everything else, so on the
   Discord face there is nothing there at all. */
/* An EVEN halo.
   
   inset:0 with a downward nudge threw the glow low and left it thin along the
   top edge. A negative inset grows it equally on all four sides, and with no
   translate it stays concentric — so it surrounds the button rather than
   pooling under it, and because it is a child it rises WITH the button on
   hover instead of staying behind on the page. */
.nav-right .nav-pill::after {
  content: "";
  position: absolute; inset: -5px;
  border-radius: calc(var(--nav-btn-r, 10px) + 5px);
  background-image: inherit;
  background-size: 220% 100%;
  background-position: inherit;
  filter: blur(11px);
  opacity: 0;
  transform: none;
  transition: opacity .5s cubic-bezier(.4,0,.2,1);
  z-index: -2;
  pointer-events: none;
}
.nav-right .nav-pill.is-pro::after {
  background-image: linear-gradient(100deg, #7c3aed, #d946ef 36%, #fbbf24 70%, #fde047);
  opacity: .45;
  animation: navProShift 6s ease-in-out infinite;
  /* The halo is a blurred copy of the fill, so it has to share its phase or
     the glow lags behind the colour it is supposed to be cast by. */
  animation-delay: var(--pro-delay, 0s);
}
/* MEMBERS KEEP THE HALO.
   
   The face is calmed down — no gradient fill, magenta border, muted type —
   because selling PRO to somebody who already bought it looks careless. But
   killing the glow as well made "Check PRO" read as a disabled control rather
   than as the same button in a settled state. The halo is what says "this is
   still the PRO thing"; the quiet fill is what says "you already have it".
   
   Slightly softer than the selling face (.32 against .45) so it sits beside
   the account menu rather than competing with it, and it shares --pro-delay so
   every PRO surface on the page pulses on the same beat. */
.nav-right .nav-pill.is-pro.is-member::after {
  opacity: .32;
  animation: navProShift 6s ease-in-out infinite;
  animation-delay: var(--pro-delay, 0s);
}

/* PRO wears the site's own wordmark face, at the size the brand uses — it is
   the same voice as "DonutStats", which is what makes it read as ours rather
   than as an ad someone dropped into the header. */
/* .pro-face, not .is-pro.
   
   .is-pro flips at the START of the swap so the colour can begin fading. With
   the type on it too, "Add to Discord" jumped to PRO's 16.5px wordmark while
   it was still fully visible — so the outgoing label GREW instead of settling
   away. .pro-face is toggled inside apply(), while the face is transparent, so
   the type only ever changes on the label it belongs to. */
.nav-pill.pro-face .nav-pill-text {
  font-family: var(--font-display, var(--font));
  font-size: 16.5px;
  font-weight: 800;
  letter-spacing: -0.02em;
}
/* Already a member: the same pill, calmed right down. Selling something to
   somebody who has already bought it is the fastest way to look careless. */
.nav-pill.is-pro.is-member {
  background-image: none;
  background-color: var(--surface-3);
  border-color: rgba(217,70,239,0.45);
  color: #f0abfc;
  box-shadow: none;
  animation: none;
}

/* The face swap: out upward, in from below, one at a time. */
.nav-pill .nav-pill-text,
.nav-pill .nav-pill-ico {
  transition: opacity .22s cubic-bezier(.4,0,.2,1), transform .22s cubic-bezier(.4,0,.2,1);
}
/* ONE element carries the change: the mark and the label together.
   
   Animating them separately — even on identical curves — left the logo looking
   like it arrived on its own, because two transitions on two elements never
   quite agree and any layout shift hits them at different moments. Wrapping
   them means there is a single thing fading, and whatever is inside it comes
   and goes as part of that. */
.nav-pill-face {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  transition: opacity .26s cubic-bezier(.4,0,.2,1),
              transform .26s cubic-bezier(.4,0,.2,1),
              filter .26s cubic-bezier(.4,0,.2,1);
}
.nav-pill.is-swapping .nav-pill-face {
  opacity: 0;
  /* Shrinking slightly as it goes, rather than sliding away — a slide reads as
     "another item arrived", a settle reads as "this one changed". */
  transform: scale(0.92);
  filter: blur(1px);
}
@media (prefers-reduced-motion: reduce) {
  .nav-pill.is-pro { animation: none; }
  .nav-pill .nav-pill-text, .nav-pill .nav-pill-ico { transition: none; }
  .nav-right .nav-pill::after { animation: none; }
}

@media (max-width: 1130px) {
  .navbar { padding-right: 32px; }
  .nav-right .nav-pill { min-width: 0; }
}


/* ═══ Homepage: one screen, no scroll ════════════════════════════════════
   The hero, the carousel and the footer are meant to be the whole page. Fixed
   paddings only achieve that on the screen they were tuned for — on a laptop
   the footer falls below the fold, on a tall monitor there is dead space under
   it. So the vertical rhythm is expressed in vh and the page is a column that
   fills the viewport exactly.

   Scoped to the homepage with :has(), so no other page inherits a layout that
   assumes its content fits. */

/* The body is ALREADY a 100vh flex column with the footer pegged to the
   bottom. Giving main its own full-viewport min-height added a second screen's
   worth on top of that, which is what put the scrollbar there — the fix was
   causing the thing it was meant to remove.
   
   So main just flexes into the space the nav and footer leave, and centres its
   contents in whatever that turns out to be. No arithmetic, nothing to keep in
   sync with the header height, and it is correct at every viewport. */
body:has(.home-hero) { min-height: 100dvh; }
/* The FIRST SCREEN is the hero and the carousel — those still fill the viewport
   exactly, with the showcase beginning below the fold.
   
   main can no longer be the thing that fits, and must not clip: there is real
   content under it now, and an overflow:hidden here would simply cut the
   showcase off. So the fitting moves to a wrapper around the top block, and the
   page scrolls to reveal the rest — which is the point of putting it there. */
body:has(.home-hero) main {
  display: block;
  padding-bottom: 0;
}

/* The first screen: hero + carousel, exactly the viewport minus the navbar.
   
   --nav-h is MEASURED from the real navbar by home-view.js rather than derived
   from a stack of clamps. Every previous attempt guessed at it, and each guess
   showed up as either slack above the fold or the next section peeking into
   it — which is precisely what must not happen: the showcase should be a
   reveal, not a hint. */
.home-top {
  min-height: calc(100dvh - var(--nav-h, 51px));
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.home-top > .home-hero,
.home-top > .home-carousel { flex: 0 0 auto; }

/* Short windows: forcing a full screen there would squeeze the type to
   nothing, so it releases and scrolls like any other page. */
@media (max-height: 620px) {
  .home-top { min-height: 0; }
}

body:has(.home-hero) .home-hero {
  /* Same proportions as the fixed version at ~900px tall, but expressed
     against the viewport so they hold at any height. */
  padding-top: clamp(20px, 6vh, 64px);
  padding-bottom: clamp(14px, 3vh, 38px);
  flex-shrink: 0;
}
body:has(.home-hero) .home-hero-sub { margin-top: clamp(10px, 2vh, 20px); }
body:has(.home-hero) .home-hero-stats { margin-top: clamp(18px, 4vh, 44px); }

body:has(.home-hero) .home-carousel {
  padding: clamp(16px, 3.5vh, 34px) 0 clamp(18px, 4vh, 40px);
  flex-shrink: 0;
}

/* The wordmark and the counts shrink with the height too, so a short window
   scales the whole composition rather than cropping the bottom off it. */
body:has(.home-hero) .home-hero-title { font-size: clamp(34px, min(7.5vw, 9vh), 76px); }
body:has(.home-hero) .hh-num { font-size: clamp(20px, min(3.4vw, 4vh), 34px); }

/* Below this the cards genuinely cannot fit a screen, and forcing it would
   mean unreadable type. Scrolling is the honest answer there. */
@media (max-height: 560px) {
  body:has(.home-hero) main { min-height: 0; }
}


/* ── the bot face ───────────────────────────────────────────────────────
   It sits next to Sign in, which is full Discord blurple, so this cannot be
   the same or the two compete and neither wins. It reads as Discord's colour
   without shouting: a blurple-tinted surface with a blurple edge, lifting to a
   solid fill on hover — so it looks like something to click rather than
   another label in the row. */
/* The Discord face — a modern secondary button.
   
   The previous version stacked a highlight gradient, a corner bloom, two inset
   shadows and a drop shadow on the mark, which is a 2015 idea of "polished":
   lots of effects fighting for the same 34 pixels. Current products (Linear,
   Vercel, Stripe, Discord's own UI) do the opposite for a secondary action —
   one flat surface a step above the page, one hairline border, one crisp
   label — and let hover carry all the feedback. That is what this is now.
   
   It also cannot be blurple: Sign in already is, and two blues side by side
   means the weaker one just looks faded. The brand lives in the mark alone. */
.nav-right .nav-pill:not(.is-pro) {
  background-image: none;
  background-color: #191d2b;
  border: 1px solid rgba(255,255,255,0.09);
  color: #d5dae8;
  font-weight: 650;
  letter-spacing: -0.005em;
  box-shadow: none;
}
.nav-right .nav-pill:not(.is-pro) .nav-pill-ico {
  /* No glow. At 22px the mark is already the most colourful thing on the
     button; haloing it made it look like it was being pointed at. */
  filter: none;
  opacity: .95;
  transition: opacity .18s ease;
}

/* Hover is where the brand shows up — a blurple edge and a lift in the
   surface, without ever filling solid and colliding with Sign in. */
.nav-right .nav-pill:not(.is-pro):hover {
  background-color: #1a2033;
  border-color: rgba(88,101,242,0.55);
  color: #fff;
}

/* ── hover: it lifts ────────────────────────────────────────────────────
   Both faces rise a pixel and gain a shadow beneath, so the button reads as
   picked up rather than merely recoloured. The curve overshoots very slightly,
   which is what makes it feel sprung rather than mechanical.
   
   transform is on the PILL and the glow is on its own layer, so this never
   fights the face's swap animation — that was the mistake in the first pass at
   this, where hover and morph shared a property and each cancelled the other. */
.nav-right .nav-pill {
  transform: translateY(0);
  transition: background-color .5s cubic-bezier(.4,0,.2,1),
              border-color .5s cubic-bezier(.4,0,.2,1),
              color .5s cubic-bezier(.4,0,.2,1),
              box-shadow .22s cubic-bezier(.34,1.4,.64,1),
              transform .22s cubic-bezier(.34,1.4,.64,1);
}
/* Hover carries all the feedback, and it is three small things at once: the
   surface steps up, the border picks up the brand colour, and the label goes
   white. No new gradients, no glow — the button gets clearer, not busier.
   
   Brighter rather than darker: a surface moving toward the light is what makes
   a lift read as raised, while darkening reads as pressed and contradicts the
   movement. */
.nav-right .nav-pill:not(.is-pro):hover {
  transform: translateY(-2px);
  background-color: #232839;
  border-color: rgba(88,101,242,0.6);
  color: #fff;
  box-shadow: 0 6px 16px rgba(0,0,0,0.4);
}
.nav-right .nav-pill:not(.is-pro):hover .nav-pill-ico { opacity: 1; }
/* The label brightens and grows on the DISCORD face only. 1.04 is small on
   purpose — the button's width is fixed, so more would push text to the edges. */
.nav-right .nav-pill .nav-pill-face { transform-origin: center; }
.nav-right .nav-pill:not(.is-pro):hover .nav-pill-face { transform: scale(1.04); }
.nav-right .nav-pill:not(.is-pro):hover .nav-pill-text { color: #fff; }
/* PRO's label is white and stays white.
   
   brightness() on white type does nothing except tint it toward whatever is
   behind it, which on a moving gradient meant the words drifted in colour as
   the gradient travelled. The halo carries the hover instead. */
.nav-right .nav-pill.is-pro .nav-pill-text { color: #fff; }
.nav-right .nav-pill.is-pro:hover .nav-pill-text { color: #fff; filter: none; }
/* PRO's INSIDE does not react at all.
   
   No brightness, no colour shift, no growing label — the face is already the
   loudest thing in the header and lighting it further just made it look like
   it was flashing. Only the button lifts and its halo answers, which is enough
   to say "this is a button" without touching a design that is already doing
   its job. */
.nav-right .nav-pill.is-pro:hover { transform: translateY(-2px); }
/* Brighter, NOT bigger.
   
   A halo that grows on hover reads as the button swelling, which fights the
   2px lift already saying "raised". Holding its size and lifting only its
   intensity keeps the change to one idea. */
.nav-right .nav-pill.is-pro:hover::after {
  opacity: .62;
  transform: none;
}
.nav-right .nav-pill::after {
  transition: opacity .5s cubic-bezier(.4,0,.2,1),
              transform .22s cubic-bezier(.34,1.4,.64,1);
}
@media (prefers-reduced-motion: reduce) {
  .nav-right .nav-pill:hover { transform: none; }
}
.nav-right .nav-pill:active { transform: translateY(0) scale(0.985); }
/* Bigger than the 18px the markup asks for — at 22px it holds its own against
   the label instead of reading as a bullet point. width/height on the element
   are the HTML attributes; CSS wins. */
.nav-right .nav-pill .nav-pill-ico {
  width: 22px;
  height: 22px;
}
/* PRO carries no mark: the wordmark type is the identity there, and a Discord
   logo beside "Buy PRO" says the wrong thing entirely.
   
   It is collapsed rather than display:none'd, so it ANIMATES out with the rest
   of the morph — width, margin and opacity all on the same curve. display
   cannot be transitioned, so hiding it that way made the mark disappear on one
   frame while everything around it faded, which is the break. The button's
   width is fixed, so the label simply re-centres as the mark closes up. */

/* No animation of its own. The mark is shown or hidden outright at the moment
   the face is invisible mid-swap, so it never animates independently — the
   face's own fade is the only thing moving. */
.nav-right .nav-pill.pro-face .nav-pill-ico { display: none; }
/* The mark brightens with the rest rather than staying a dim logo on a lit
   button. */
.nav-right .nav-pill:not(.is-pro) img {
  opacity: .92;
  transition: opacity .14s;
}
.nav-right .nav-pill:not(.is-pro):hover img { opacity: 1; }


/* A margin of quiet around the chart's own controls.
   The card is the chart's hover surface, so travelling up to click Volume used
   to drag the crosshair across the plot the whole way. The JS treats anything
   inside these as "off the chart"; this padding widens that zone slightly, so
   approaching a button stops the tooltip a little BEFORE the cursor reaches
   it — you are not aiming at a moving target. */
/* The chart's controls get a margin of quiet around them, so travelling up to
   click one does not drag the crosshair across the plot.
   
   :not(#pl-legend) matters more than it looks. The player page's stat tabs ARE
   an .ah-legend — #pl-legend, absolutely positioned at bottom:100% so the strip
   sits exactly ON the card's top edge. Padding it moved the strip off that
   edge, which is the gap between the tabs and the card: the rule was aimed at
   the auction house's inline legend and landed on a positioned element with a
   completely different job.
   
   :not(:empty) because the balance legend is empty whenever a player has one
   series, and an empty box should occupy nothing. */
.ah-chart-card .ah-legend:not(:empty):not(#pl-legend) {
  padding: 8px 10px;
  margin: -8px -10px 0;
  border-radius: 10px;
}
.ah-chart-card .ah-legend:empty { display: none; }


/* ═══ .cd dropdown ═══════════════════════════════════════════════════════
   Ported from oxlicensing, in this site's tokens. The geometry, the rotating
   caret and the closing transition are its numbers, not new ones — the point
   of copying is that it already behaves correctly. */

.cd { position: relative; display: inline-block; user-select: none; }

/* The inline variant: the chart's title IS the button, so it carries no
   surface of its own — just the label and the caret. */
/* The button inherits the heading it replaced rather than imposing its own
   type. .pl-chart-title already sets the family, size and weight, and having
   this override them is what made "Players online" go thin. A control that
   replaces a heading should look exactly like the heading did. */
.cd-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
}
.cd-label { white-space: nowrap; }

/* A CSS triangle, exactly as oxlicensing draws it. */
.cd-arrow {
  width: 0; height: 0;
  /* A step larger, with the control. At 4px it was a speck beside a 16px
     title — the caret is what says this heading can be clicked, so it has to
     be visible enough to make that claim. */
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid var(--text-dim);
  flex-shrink: 0;
  transition: transform .15s, border-top-color .15s;
}
.cd-btn:hover .cd-arrow { border-top-color: var(--text-mid); }
.cd.open .cd-btn .cd-arrow { transform: rotate(180deg); }

/* Matched to the account menu, not invented.
   
   Same surface (#0e121b, a step darker than the control above it), same border,
   the shared control radius, and edge-to-edge square items whose hover is
   clipped by the container's corners — so the two dropdowns on the site are
   recognisably the same object rather than two designers' takes on one. */
/* The account menu's own panel: #0e121b (a step darker than the control above
   it), one hairline border, the shared control radius, and square edge-to-edge
   items whose hover is clipped by the container's corners. */
.cd-list {
  position: fixed;
  z-index: 9999;
  display: none;
  background: #0e121b;
  border: 1px solid var(--border);
  /* 16px, matching .nav-wallet.open — the wallet eases to the card's larger
     corner as it opens, and this panel is the same object in a different
     place. At 10px it read as a tooltip beside a menu that read as a card. */
  border-radius: 16px;
  padding: 0;
  overflow: hidden;
  box-shadow: 0 8px 28px rgba(0,0,0,.5);
  opacity: 1;
  transform: translateY(0) scale(1);
  transform-origin: top center;
  white-space: nowrap;
  transition: opacity .18s cubic-bezier(.4,0,.2,1),
              transform .18s cubic-bezier(.4,0,.2,1);
}
/* The list is TELEPORTED to <body> on open, so it stops being a descendant of
   .cd and a `.cd.open .cd-list` rule can never match it — which is why the
   caret flipped but nothing appeared. The open state is set inline by the JS
   instead; this is only the resting state. */
.cd-list.cd-open { display: block; }
/* Leaves the way it arrived, rather than vanishing. */
.cd-list.cd-closing {
  opacity: 0;
  transform: translateY(-6px) scale(.97);
  pointer-events: none;
}

/* Identical to .nav-wallet-item: same padding, family, weight, size, colour
   and hover timing. */
/* IDENTICAL to .nav-wallet-item, not merely similar.
   
   Same family, weight, size, padding, colour and hover timing — and now the
   same hover FILL (var(--surface-3), the account menu's) rather than a blue
   tint this dropdown had invented for itself. Two menus on one site being
   almost the same is worse than them being different: the eye notices that
   something moved without being able to say what.
   
   Scaled up a step from the account menu's 13px, because this one is the
   chart's own title control rather than a row in a corner panel — it is read
   at arm's length, not clicked at. */
.cd-item {
  display: flex; align-items: center; width: 100%;
  background: none; border: none; border-radius: 0;
  color: var(--text-mid);
  font-family: var(--font); font-weight: 600; font-size: 14px;
  padding: 12px 18px;
  cursor: pointer; text-align: left;
  transition: background .12s, color .12s;
}
.cd-item:hover { background: var(--surface-3); color: var(--text); }
/* Selected reads as BRIGHTER, not as a different colour — the account menu
   marks nothing and simply lifts whatever is under the cursor, so a coloured
   row here read as a stray link. */
.cd-item.selected { color: var(--text); }
.cd-item.selected:hover { background: var(--surface-3); }


/* ═══ Header controls: one family ════════════════════════════════════════
   Three controls sat side by side in three different shapes — a 9px pill, an
   8px button and a 16px rounded card — which is what made the row look
   assembled rather than designed. One radius token, applied to all three.

   Fully round would fight the site's language (every card and control here is
   a soft rectangle); 10px is the radius the chart pills and the search box
   already use, so the header now agrees with the rest of the page. */
:root { --nav-btn-r: 10px; }

.nav-right .nav-pill,
.nav-right .btn-discord-signin,
.nav-right .nav-wallet {
  border-radius: var(--nav-btn-r);
}
/* The wallet is the one that GROWS. It keeps the shared radius while closed
   and eases to the card's larger corner as it opens, so it reads as the same
   control expanding rather than a different element appearing. */
.nav-right .nav-wallet.open { border-radius: 16px; }

/* The avatar sits flush in the corner, not inset.
   
   With 5px of padding around it the circle floated inside a rounded rectangle
   and the two curves fought. Pulling the padding off the left and matching the
   avatar to the control's full height makes the circle BE the left edge — one
   silhouette instead of a shape inside a shape. */
.nav-right .nav-wallet-head {
  padding-left: 0;
  gap: 9px;
}
/* The SHELL curves around the circle, rather than the circle covering the
   shell.
   
   Oversizing the avatar to hide the border was the wrong way round: the shell
   has overflow:hidden, so anything taller than its inner box gets its top and
   bottom clipped — which is why the face looked flattened. Instead the avatar
   fits exactly inside the border, and the shell's left corners are rounded to
   a full half-height. The border then follows the circle's edge precisely and
   there is no straight edge to see. */
.nav-right .nav-wallet-avatar {
  width: calc(var(--nav-btn-h) - 2px);
  height: calc(var(--nav-btn-h) - 2px);
  margin: 0;
}
.nav-right .nav-wallet {
  border-top-left-radius: calc(var(--nav-btn-h) / 2);
  border-bottom-left-radius: calc(var(--nav-btn-h) / 2);
}
/* Opening it, the left side straightens out with the rest as the card grows —
   a semicircular edge on a tall menu would look like a mistake. */
.nav-right .nav-wallet.open {
  border-top-left-radius: 16px;
  border-bottom-left-radius: 16px;
}


/* The dropdown's chevron: the same mark the account menu uses, at a size that
   suits a heading rather than a 13px label. The CSS triangle it replaces was
   easy to miss next to type this large. */
.cd-chev {
  width: 20px; height: 20px;
  color: var(--text-dim);
  flex-shrink: 0;
  transition: transform .22s cubic-bezier(0.34,1.2,0.64,1), color .15s;
}
.cd-btn:hover .cd-chev { color: var(--text-mid); }
.cd.open .cd-chev { transform: rotate(180deg); }


/* ═══ Homepage entrance ══════════════════════════════════════════════════
   The same shellRise the search pages use — same curve, same duration, same
   10px rise — so arriving at the homepage feels like arriving at any other
   page rather than at a different site. Only the stagger is longer, because
   there are four things here instead of three and the last of them is a wide
   row of cards. */
/* The carousel is deliberately NOT in this list. It is already moving — it
   scrolls on its own and its cards fade at the edges — so a second entrance
   animation on top of that reads as two things happening at once. */
.home-hero-title,
.home-hero-sub,
.home-hero-stats {
  animation: shellRise .5s cubic-bezier(0.16,1,0.3,1) both;
}
.home-hero-title  { animation-delay: .02s; }
.home-hero-sub    { animation-delay: .09s; }
.home-hero-stats  { animation-delay: .16s; }

/* The three counts arrive in sequence within their own row, so the eye reads
   left to right rather than seeing the block appear whole. */
.home-hero-stats .hh-stat {
  animation: shellRise .45s cubic-bezier(0.16,1,0.3,1) both;
}
.home-hero-stats .hh-stat:nth-child(1) { animation-delay: .20s; }
.home-hero-stats .hh-stat:nth-child(3) { animation-delay: .26s; }
.home-hero-stats .hh-stat:nth-child(5) { animation-delay: .32s; }

@media (prefers-reduced-motion: reduce) {
  .home-hero-title, .home-hero-sub, .home-hero-stats,
  .home-hero-stats .hh-stat { animation: none; }
}


/* ═══ Embed mode (?embed=1) ══════════════════════════════════════════════
   The page as a bare panel, for framing inside the homepage showcase. Only
   chrome is removed — the content, the charts and every interaction are the
   real page, so whatever changes there shows up here without anyone
   remembering to update a copy. */
html.is-embed .navbar,
html.is-embed .footer,
html.is-embed .mob-topbar,
html.is-embed .mob-drawer,
html.is-embed .mob-overlay,
html.is-embed .ov-scroll-track {
  display: none !important;
}
html.is-embed, html.is-embed body {
  min-height: 0;
  background: transparent;   /* the frame's own card provides the surface */
}
html.is-embed main { padding-top: 14px; }
/* The side rails are positioned against the viewport, which inside a frame is
   the frame — they would sit on top of the content. */
html.is-embed .ah-feed-section,
html.is-embed .ps-feed-section { display: none !important; }


/* ═══ Homepage showcase ══════════════════════════════════════════════════ */

.hs-wrap {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(24px, 4vw, 56px);
  align-items: center;
  max-width: 1180px;
  margin: clamp(40px, 7vh, 88px) auto 0;
  padding: 0 24px clamp(40px, 7vh, 80px);
}

.hs-copy { text-align: center; }
.hs-title, .hs-sub {
  transition: opacity .22s cubic-bezier(.4,0,.2,1), transform .22s cubic-bezier(.4,0,.2,1);
}
.hs-title.is-swapping, .hs-sub.is-swapping { opacity: 0; transform: translateY(-4px); }
.hs-title {
  font-family: var(--font-display, var(--font));
  font-size: clamp(28px, 3.4vw, 40px);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.15;
  margin: 0;
  color: var(--text);
}
/* Bigger than body copy: this is a headline's second line, not a paragraph. */
.hs-sub {
  font-size: 17px;
  font-weight: 600;
  color: var(--text-mid);
  margin: 18px 0 0;
  line-height: 1.5;
}

.hs-proof {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--blue);
  margin: 22px 0 0;
}

/* Position, not navigation: it says the panel is one of three and moving on
   its own, without inviting a click it does not respond to. */
.hs-right { display: flex; flex-direction: column; gap: 12px; }
/* Above the frame and right-aligned to it: a caption for the panel rather than
   something sitting in the copy column looking like a control. */
/* The pips, with the active one TRAVELLING between them.
   
   A dot that simply lights up somewhere else tells you the panel changed but
   not which way it went. A marker that slides carries that, and telegraphs the
   rhythm — you can see the next change coming. */
/* width:max-content and auto margins.
   
   The row was a full-width block, so the travelling marker's left:0 was the far
   edge of the column rather than the first pip — it animated beautifully, in
   the wrong place. Shrinking the container to its contents makes left:0 the
   first pip by definition. */
.hs-dots {
  position: relative;
  display: flex;
  gap: 6px;
  width: max-content;
  margin: 0 auto;
}
.hs-pip {
  width: 20px; height: 3px; border-radius: 2px;
  background: rgba(3,111,255,0.16);
  transition: background .35s cubic-bezier(.4,0,.2,1);
  /* They are controls now, so they have to look like it — and the hit area is
     padded well beyond the 3px bar, because a 20x3 target is a miss waiting to
     happen. The padding is transparent and clipped to the box, so the row
     spacing is unchanged. */
  cursor: pointer;
  position: relative;
}
.hs-pip::after {
  content: "";
  position: absolute;
  inset: -9px -3px;          /* ~21px tall, the full gap between pips wide */
}
.hs-pip:hover { background: rgba(3,111,255,0.34); }
/* One element sliding over the row, rather than colouring each pip in turn —
   separate backgrounds could only cross-fade, never cross the gaps. */
.hs-dots::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 20px; height: 3px;
  border-radius: 2px;
  background: var(--blue);
  box-shadow: 0 0 10px rgba(3,111,255,0.55);
  transform: translateX(var(--pip-x, 0px));
  transition: transform .42s cubic-bezier(.34,1.1,.5,1);
}
@media (prefers-reduced-motion: reduce) {
  .hs-dots::after { transition: none; }
}

/* The frame reads as a browser window, which is what tells you at a glance
   that the thing inside is a real page rather than an illustration. */
/* A CEILING on the width, which is really a ceiling on the height.
   
   The frame keeps a fixed 1180:760 shape so every screen shows the same
   layout — but on a wide monitor the right column is nearly 950px, and that
   shape then stands 610px tall and dominates the section. Capping the width
   caps the height with it, and the panel stays a panel. */
.hs-frame {
  max-width: 760px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 14px;
  overflow: hidden;
  /* One soft shadow, not a stack.
     
     A single very large blur over a near-black background gets quantised into
     visible steps by the 8-bit colour ramp — that is the concentric ringing.
     Two smaller, tighter shadows read as one soft falloff and never band,
     because neither is spread far enough for a step to become visible. */
  /* A hairline lift plus a soft ambient blue, so the card separates from a very
     dark page without a hard outline. The inset highlight along the top edge is
     what makes it read as raised rather than pasted on. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.06),
    0 0 0 1px rgba(3,111,255,0.10),
    0 2px 6px rgba(0,0,0,0.30),
    0 14px 34px rgba(0,0,0,0.34),
    0 0 60px rgba(3,111,255,0.07);
  transition: box-shadow .3s ease;
}
.hs-frame-bar {
  display: flex; align-items: center; gap: 6px;
  padding: 10px 14px;
  background: var(--surface-3, #141824);
  border-bottom: 1px solid var(--border);
}
.hs-dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: rgba(255,255,255,0.13);
}
.hs-frame-url {
  margin-left: 10px;
  font-size: 11.5px;
  color: var(--text-dim);
  font-family: var(--font-mono, ui-monospace, monospace);
  opacity: .75;
}
/* The page's own background inside the chrome, so the frame reads as a window
   onto a site rather than a bordered rectangle with something in it. */
/* A FIXED SHAPE, not a fluid one.
   
   The height used to be clamp(320px, 42vh, 460px) while the width came from
   the grid — so the frame was a different shape on every screen, and the page
   inside was rendered at a different width each time. That is why the panel
   looked cropped on one monitor and roomy on another, and why "it is not
   cropping" and "it looks cropped" were both true: different visitors were
   genuinely seeing different amounts of the page.
   
   The aspect ratio is now constant and the whole thing scales. Everyone sees
   the same layout, the same amount of page and the same proportions — just
   larger or smaller. It is a photograph of a window, not a window. */
.hs-frame-body {
  position: relative;
  /* 16:9 — a screen, not a document.
     
     1180x760 is 1.55:1, which is closer to a sheet of paper than to anything
     you would look at a website on, and it made the panel stand tall enough to
     take over the section. A monitor's proportions read as "this is a screen"
     without anyone having to be told. */
  aspect-ratio: 16 / 9;
  background: var(--page-bg);
}
/* The stage the real view mounts into.
   
   Scaled down so a full page reads as a preview inside the frame rather than a
   cropped corner of one. transform-origin top-left plus a compensating width
   means the scaled content still fills the box — scaling alone would leave a
   gap on the right.
   
   pointer-events:none because it is a display: chart tooltips chasing a cursor
   across a panel that changes every few seconds is noise, and a half-read
   tooltip vanishing mid-swap is worse than none. */
.hs-stage {
  /* top/left, NOT inset:0.
     
     inset:0 also sets right and bottom — so with an explicit width and height
     below the box was over-constrained and grew past the frame, which is why
     the page sat down and to the right with all the slack above and left. It
     was centred correctly, inside a box far bigger than the window. */
  position: absolute; top: 0; left: 0;
  overflow: hidden;
  /* zoom, not transform: scale().
     
     A transform is a paint-time effect — the page still LAYS OUT at full size,
     so anything using position:fixed or getBoundingClientRect (the metric
     dropdown, the chart tooltip) computed viewport coordinates that no longer
     matched where it was drawn. That is the menu at full size and the tooltip
     far to the left: both were correct for the unscaled layout.
     
     zoom scales at LAYOUT time. Every coordinate inside is consistent, so
     popups position themselves correctly with no special cases, and the page is
     a true miniature of itself rather than a stretched copy. */
  /* Set by sizeStage() from the frame's real width — the page lays out at a
     fixed 1180x760 and the whole stage is scaled to whatever room there is, so
     every screen shows the identical layout at a different size. This value is
     only what applies for the frame or two before the first measurement. */
  zoom: 0.62;
  /* Size is set in PIXELS by JS, not with a percentage.
     
     calc(100% / 0.62) assumes a browser resolves a percentage against the
     parent BEFORE applying zoom. Chrome and Firefox do not agree on that, and
     when it goes the other way the stage ends up 1.6x too big — which is the
     "only a corner of the page is visible" symptom: the page was centred
     correctly inside a box far larger than the frame.
     
     Measuring the frame and dividing by the zoom leaves nothing to interpret:
     the stage is exactly the frame's size in its own coordinate space, in every
     browser. See sizeStage() in home-showcase.js. */
  /* Interactive now: the controls ARE the demonstration. Navigation is blocked
     in JS rather than here, because pointer-events:none would kill the range
     pills and the dropdown along with the links. */
  pointer-events: auto;
  background: var(--page-bg);
}
/* The panel ARRIVES rather than appearing.
   
   A scene change was a straight cut: one page vanished and the next was
   already there, which reads as a glitch rather than as a change. A short lift
   and fade says "this is the next one" without becoming an animation anybody
   has to sit through.
   
   The transform lives ONLY during the entrance. At rest the stage carries no
   transform at all, because a transformed ancestor becomes the containing
   block for position:fixed — which would put the metric dropdown and the chart
   tooltip in the wrong place, the exact bug the stage uses `zoom` to avoid. */
.hs-stage.is-entering {
  opacity: 0;
  transform: translateY(10px) scale(.985);
}
.hs-stage.is-entered {
  transition: opacity .34s ease, transform .34s cubic-bezier(.16,1,.3,1);
  opacity: 1;
  transform: none;
}

/* Measured, then shown — never the other way round.
   
   The fit runs a couple of frames after a view mounts, so without this the
   page appeared at its natural size and then jumped to its fitted one. That
   was the "too big at first" and, on every scene change, the flash.
   
   OPACITY rather than display or visibility: measuring needs real geometry,
   and a display:none element has none. The fade back in is short enough to
   read as the panel arriving rather than as an animation of its own. */
.hs-stage { transition: opacity .18s ease; }
.hs-stage.is-measuring { opacity: 0; }

/* Links are visibly not the point — they still exist, they just do not lead
   anywhere from inside the panel. */
/* (cursor is set below — these are pressable, they just do not navigate) */
/* A small settle when a blocked link is clicked, so nothing feels dead. */
.hs-stage.is-nudged { animation: hsNudge .26s ease; }

/* Everything that would navigate still LOOKS like a control inside the panel —
   the point of framing real pages is that they behave like real pages, and a
   pill that cannot be pressed is a picture of a pill. It goes nowhere, but it
   answers. */
.hs-stage a[href], .hs-stage [data-href], .hs-stage [data-link] { cursor: pointer; }
.hs-stage .hs-pressed {
  animation: hsPress .22s ease;
}
@keyframes hsPress {
  0%   { transform: scale(1); filter: brightness(1); }
  40%  { transform: scale(.94); filter: brightness(1.35); }
  100% { transform: scale(1); filter: brightness(1); }
}
@keyframes hsNudge {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(2px); }
}
/* The views bring their own page padding, which is too generous at this
   scale — the panel should show the content, not the margins. */
.hs-stage .container,
.hs-stage .ss-wrap { padding-top: 8px; }
/* Anything anchored to the viewport belongs to the real page, not to a panel
   inside it. */
.hs-stage .ah-feed-section,
.hs-stage .ps-feed-section,
.hs-stage .ov-scroll-track { display: none !important; }

@media (max-width: 900px) {
  .hs-wrap { grid-template-columns: minmax(0, 1fr); }
  /* The shape holds here too — a narrow screen gets a smaller frame, not a
     differently proportioned one. */
}


/* Peak-hour emphasis on the volume bars, after oxlicensing's Peak Request Time:
   hours at or above the day's average are solid, the rest step back.
   
   A quiet hour is a DIFFERENT TONE, not the same bar turned down. Opacity let
   the page background through, so the quiet bars picked up whatever was behind
   them and read as faded rather than as a second colour — oxlicensing's chart
   uses two solid fills, which is why its shape reads instantly. Same hue, half
   the presence, fully opaque. */
.ah-bar.is-quiet { fill: rgba(34,197,94,0.28); }
.ah-bar { transition: fill .15s; }
/* A quiet bar comes all the way up to the full green under the cursor — the
   same colour a busy one gets. The dimming exists to show the SHAPE of the
   day; while you are reading one hour it has nothing left to say, and a
   half-lit bar would leave you wondering whether the hover had registered. */
.ah-bar.is-quiet.is-hot { fill: rgb(34,197,94); }


/* ═══ Leaderboard: list / chart ══════════════════════════════════════════ */

/* The toggle, top right of the card. A segmented pair rather than two loose
   buttons, because these are two states of one setting — the same shape the
   range pills use, one size down. */
.lbp-mode {
  position: absolute; top: 18px; right: 18px;
  display: inline-flex; gap: 2px; padding: 3px;
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-radius: 10px;
}
.lbp-mode-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 26px; padding: 0;
  background: none; border: 0; border-radius: 7px;
  color: var(--text-dim); cursor: pointer;
  transition: background .14s, color .14s;
}
.lbp-mode-btn svg { width: 15px; height: 15px; }
.lbp-mode-btn:hover { color: var(--text); }
.lbp-mode-btn.on { background: var(--blue); color: #fff; }
.lbp-head { position: relative; }

/* ── the race chart ────────────────────────────────────────────────────── */
.lbp-chart { padding: 4px 20px 22px; }
.lbp-chart[hidden] { display: none; }
.lbr-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; margin: 0 0 12px;
}
.lbr-head-title {
  font-family: var(--font-display);
  font-size: 14px; font-weight: 800; color: var(--text);
}
.lbr-card { padding: 16px 14px 10px; }
.lbr-body { height: clamp(300px, 44vw, 460px); }
.lbr-svg { width: 100%; height: 100%; }

/* Stroked, never filled. One filled area reads as a quantity; forty overlapping
   ones read as mud — which is why the item chart's treatment is copied for the
   grid, the axes and the pills but NOT for the area. */
/* Two weights, and the gap between them is doing the work.
   
   Twenty lines at one weight is a bundle you have to untangle before you can
   read anything. Eight in front and twelve behind gives the chart a subject:
   the leaders are the story, the pack is the context they are pulling away
   from. Making the background quieter is what makes the foreground legible —
   there is nothing you can do to a line to make it stand out among twenty
   equals. */
.lbr-line {
  fill: none;
  stroke-width: 1.25;
  stroke-linejoin: round; stroke-linecap: round;
  opacity: .3;
  transition: opacity .14s, stroke-width .14s;
}
.lbr-line.is-top { stroke-width: 2.4; opacity: .95; }
/* Hovering one line pushes the rest back rather than highlighting the one —
   at this density, making forty lines quieter is what makes one visible. */
.lbr-svg:hover .lbr-line { opacity: .1; }
.lbr-svg:hover .lbr-line:hover { opacity: 1; stroke-width: 3.2; }
/* A fat invisible hit area over each line, so following one with the cursor
   does not require landing on 1.25 pixels. */
.lbr-line { stroke-linecap: round; paint-order: stroke; }
.lbr-svg .lbr-line { pointer-events: stroke; }

/* The connector from a moved label back to where its line really ends. Thin
   and dimmed: it is a pointer, not data, and it must never be mistaken for a
   forty-first player. */
.lbr-leader { fill: none; stroke-width: 1; opacity: .35; }
.lbr-face-bg { opacity: .9; }
.lbr-face image { image-rendering: pixelated; }
.lbr-face-name {
  font-family: var(--font); font-size: 11px; font-weight: 700;
  fill: var(--text-mid);
}

/* ═══ Live activity ══════════════════════════════════════════════════════
   Three tickers the site already runs, turned on their side. The CARDS are
   untouched — this only changes the axis they lay out along, because the shape
   of the space is different here: on their own pages these are rails beside a
   column, and in the panel they are the content on a wide screen. */
.la-wrap { padding-top: 18px; }
.la-sec { margin-bottom: 26px; }
/* Centred over the strip it labels. Left-aligned, each heading pointed at the
   start of a row that is deliberately running off both edges — the label
   belongs to the whole row, not to whichever card happens to be first. */
.la-sec-head { text-align: center; margin-bottom: 10px; }
.la-sec-title {
  font-family: var(--font-display);
  font-size: 15px; font-weight: 800; letter-spacing: -.01em; color: var(--text);
}

/* The clip is what the slide happens inside. Height from the card rather than
   a guess, so a change to the tile does not leave a gap or crop it. */
.la-rail .la-clip { height: auto; padding: 4px 6px 10px; margin-left: -6px; }

/* The one line that turns the ticker: laid out along X, so the JS measures
   width and writes translateX. Everything else about the card is unchanged. */
.la-rail .ah-feed {
  flex-direction: row;
  gap: 6px;                 /* == GAP / FEED_GAP_PX in the JS */
}
/* Newest enters from the LEFT, so the row reads the same direction as the
   column did: the arrival is at the start and the oldest falls off the end. */
.la-rail .ah-feed > * { flex: 0 0 auto; }

/* The strip is one row tall and clipped to it, so a card sliding in from the
   left is hidden until it is in place rather than appearing beside the rail.
   Height comes from the card itself — a fixed one would either crop the
   timestamp or leave a gap under it. */
.la-rail .ah-feed-clip { overflow: hidden; }
.la-rail { position: relative; }

/* A CARD NEEDS A WIDTH HERE, and that is the whole bug behind the one giant
   stretched tile.
   
   `.ah-feed-card` is `width: 100%`, which in the vertical rail means "the
   rail's 122px" — a real number, because the column gives it one. Laid out in
   a ROW the item has no width of its own, so 100% resolved against the strip
   and the first card took the entire thing; the rest were pushed off to the
   right where the clip hid them. It looked like one enormous card and an empty
   feed, when in fact every card was there and all of them were that wide.
   
   122px is the same tile the rails show, so the horizontal strip is genuinely
   the same list — only the direction changed. */
.la-rail .ah-feed { align-items: flex-start; }
.la-rail .ah-feed-item { flex: 0 0 auto; width: 122px; }

/* The row FADES OUT at both ends instead of being chopped.
   
   A strip wider than its frame has to end somewhere, and a hard edge makes the
   last card look broken — half a tile with a straight cut through it reads as a
   rendering fault, not as "there is more". A mask says the same thing the way
   every ticker does: the content continues, you are seeing a window onto it.
   
   Both ends, not just the right: cards arrive from the left, so an arrival
   would otherwise pop into existence at a hard edge too. */
.la-rail .la-clip {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 42px, #000 calc(100% - 42px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 42px, #000 calc(100% - 42px), transparent 100%);
}

/* ═══ Showcase scenes ════════════════════════════════════════════════════
   Each scene is the REAL page with parts hidden. Cropping rather than
   reimplementing means these panels cannot drift from the pages they show.

   The selectors below are the ACTUAL class names, checked against the
   templates — my first pass guessed at .pl-stat-grid, .ah-head and .ps-title,
   none of which exist, which is why the headings and stat cards kept showing
   through. The player stat block is .stat-grid; every search page shares
   .search-tagline / .search-subtext / .search-box. */

/* Player, split in two. */
.hs-stage.sc-player-top .pl-chart-wrap,
.hs-stage.sc-player-top .ah-feed-section { display: none !important; }

.hs-stage.sc-player-hist .player-header,
.hs-stage.sc-player-hist .stat-grid,
.hs-stage.sc-player-hist .player-error,
.hs-stage.sc-player-hist .pl-trades-head,
.hs-stage.sc-player-hist .ah-feed-section { display: none !important; }

/* Every page that carries the shared search furniture loses it — the panel is
   showing what we have, not inviting a search it cannot perform. */
.hs-stage.sc-ah .search-tagline,
.hs-stage.sc-ah .search-subtext,
.hs-stage.sc-ah .search-box,
.hs-stage.sc-ah .ah-search-slot,
.hs-stage.sc-ah .ah-search-row,
.hs-stage.sc-ah .ah-feed-section,
.hs-stage.sc-players .search-tagline,
.hs-stage.sc-players .search-subtext,
.hs-stage.sc-players .search-box,
.hs-stage.sc-players .search-error,
.hs-stage.sc-players .ps-feed-section,
.hs-stage.sc-vouches .search-tagline,
.hs-stage.sc-vouches .search-subtext,
.hs-stage.sc-vouches .search-box,
.hs-stage.sc-vouches .ah-search-slot,
.hs-stage.sc-vouches .ah-search-row { display: none !important; }

/* Back links are navigation the panel cannot perform. */
.hs-stage .ah-back { display: none !important; }

/* Even padding on every scene, and room for what sits at the bottom.
   
   The item page's next/previous controls were cut off because the stage is
   exactly the frame's height — a page slightly taller than the box loses its
   last row. A little breathing room top and bottom, and the stage scrolls when
   a page genuinely needs more (the spawner calculator is the tall one). */
/* Generous, even padding — the panel should look composed, not cropped. */
.hs-stage > * { padding: 26px 30px !important; box-sizing: border-box; }

/* Centred both ways.
   
   The views are built for a full page, so left inside a short box they sat
   against the top-left corner with all the slack below and to the right — which
   is what made the panels look misaligned rather than framed. A flex centre
   puts the slack evenly around the content instead. */
.hs-stage {
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.hs-stage > * { width: 100%; margin: auto; }
/* The spawner page is taller than the box, and centring a scrolling child hides
   its top. It starts at the top and scrolls from there. */
.hs-stage.sc-spawner { justify-content: flex-start; }
.hs-stage.sc-spawner > * { margin-top: 0; }

/* The spawner calculator is the one genuinely tall page — it is a form with a
   long result table, and cropping it would hide the part that makes the point.
   It alone scrolls, using the site's own scrollbar. */
.hs-stage.sc-spawner { overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; }
.hs-stage.sc-spawner { scrollbar-color: rgba(3,111,255,0.28) transparent; }
.hs-stage.sc-spawner::-webkit-scrollbar { width: 10px; }
.hs-stage.sc-spawner::-webkit-scrollbar-thumb {
  background: rgba(3,111,255,0.28);
  border-radius: 100px;
  border: 1px solid transparent;
  background-clip: padding-box;
}
.hs-stage.sc-spawner::-webkit-scrollbar-thumb:hover { background: rgba(3,111,255,0.42); background-clip: padding-box; }
.hs-stage.sc-spawner::-webkit-scrollbar-track { background: transparent; }

/* The address bar. A pill with the URL inside it, rather than text floating in
   the title band — the frame was reading as "a box with a caption" instead of
   "a browser". */
/* In the site's own palette rather than a generic dark pill — a faint
   blue-tinted surface with a blue hairline, the way every other inset control
   here is drawn. Clickable, so the URL can be copied. */
.hs-omni {
  flex: 1;
  margin-left: 8px;
  padding: 4px 12px;
  border-radius: 100px;
  /* #0e121b — the same recessed surface the account menu and the AH search
     dropdown use. A blue-tinted bar was lighter than everything around it and
     read as the brightest thing in the chrome. */
  background: #0e121b;
  border: 1px solid rgba(255,255,255,0.07);
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  cursor: pointer;
  position: relative;
  transition: background .16s, border-color .16s;
}
.hs-omni:hover { background: #121826; border-color: rgba(3,111,255,0.30); }
.hs-omni:active { background: #0b0f18; }
.hs-omni .hs-copy-ico {
  margin-left: auto;
  width: 13px; height: 13px;
  flex-shrink: 0;
  color: var(--text-dim);
  opacity: .65;
  transition: opacity .16s, color .16s;
}
.hs-omni:hover .hs-copy-ico { opacity: 1; color: var(--blue); }
.hs-omni.is-copied .hs-copy-ico { opacity: 0; }
/* The confirmation replaces nothing and moves nothing — a toast would be a
   second thing to look at for a one-word acknowledgement. */
.hs-copied {
  position: absolute;
  right: 12px;
  font-size: 10px; font-weight: 800;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--blue);
  opacity: 0;
  transition: opacity .18s ease;
  white-space: nowrap;
}
.hs-omni.is-copied .hs-copied { opacity: 1; }
.hs-omni .hs-frame-url {
  margin: 0;
  color: #8fb6ff;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}


/* The enchanted glint, for named server items that borrow a base item's
   texture. Minecraft's own glint is an animated overlay; this is the readable
   version of it — a violet sheen that sweeps across, enough to say "not the
   plain item" at 22px without turning into an animation people notice. */
.ah-img.is-enchanted {
  position: relative;
  filter: drop-shadow(0 0 4px rgba(168,85,247,0.55)) saturate(1.15);
}
.ah-img.is-enchanted::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(115deg, transparent 35%, rgba(196,140,255,0.55) 50%, transparent 65%);
  background-size: 280% 100%;
  mix-blend-mode: screen;
  animation: ahGlint 3.2s linear infinite;
  pointer-events: none;
}
@keyframes ahGlint {
  from { background-position: 140% 0; }
  to   { background-position: -140% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .ah-img.is-enchanted::after { animation: none; }
}


/* The scene label: centred over the frame, and big enough to read from across
   the room. It is the only thing naming what is on screen, so it should not
   look like a caption. */
.hs-scene {
  display: flex;
  justify-content: center;
  min-height: 30px;
  /* Space between the label and the card it names — sitting tight against the
     chrome it read as part of the browser rather than as a heading for it. */
  margin-bottom: 6px;
}
.hs-scene-title {
  font-family: var(--font-display, var(--font));
  font-size: 24px;
  font-weight: 800;
  letter-spacing: -0.025em;
  color: var(--text);
  transition: opacity .2s ease;
}

/* The tilt is applied inline by JS; this just gives it a resting transition so
   the card eases back rather than snapping when the pointer leaves. */
.hs-frame {
  transform: perspective(900px);
  /* .52s, and a gentler curve than before.
     
     This transition now does two jobs, not one: it eases the card home when
     the pointer leaves AND eases it into the lean when the pointer arrives
     (see wireTilt — the easing is left on for the first stretch of a hover).
     A little slower and flatter reads as weight rather than as lag; the
     duration is matched by ENTER_MS in the JS, so the handover to direct
     tracking happens exactly when the card has caught up. */
  transition: box-shadow .3s ease, transform .52s cubic-bezier(.16,1,.3,1);
  will-change: transform;
}
/* While the pointer is inside, the JS writes transform every frame and a
   transition would fight it — so easing is removed by a class, not by :hover.
   
   :hover was the bug in the snap-back: leaving the area ended :hover on the
   same frame the JS wrote transform 0, so the easing was still switched off at
   the exact moment it was needed. The class is removed AFTER the reset is
   written, so the card always animates home. */
.hs-frame.is-tilting { transition: box-shadow .3s ease; }
@media (prefers-reduced-motion: reduce) {
  .hs-frame { transform: none !important; }
}

/* Padding on the right column gives the tilt somewhere to begin — the lean
   starts as the pointer approaches the card rather than snapping on at its
   border, and it never reacts to the pointer being over the copy on the left. */
/* A wide catchment so the lean begins well before the pointer reaches the
   card — it should feel like the card noticing you approach, not a hover
   state switching on at the border. */
/* The tilt's catchment. Padding, then a matching negative margin, so the area
   that reacts to the pointer is far larger than the card while the layout is
   exactly as if it were not there.
   
   Widened again: the lean should begin as you approach, and starting it at
   arm's length is most of what makes the card feel like an object rather than
   a hover state. */
/* THE CATCHMENT IS A PSEUDO-ELEMENT, not padding and negative margin.
   
   `padding: 130px 150px; margin: -130px -150px` cancels out horizontally, but
   vertically the negative BOTTOM margin genuinely removes 130px from the
   section's height — so everything after it, the footer included, was pulled
   130px up the page. The hover area was correct and the layout paid for it.
   
   An absolutely positioned ::before is part of the element for hit-testing, so
   pointerenter/pointermove on .hs-right fire exactly as before, while the box
   contributes nothing to layout at all. Behind the content (z-index -1) so it
   can never intercept a click meant for the panel. */
.hs-right { position: relative; }
.hs-right::before {
  content: "";
  position: absolute;
  inset: -130px -150px;
  z-index: -1;
  pointer-events: auto;
}


/* PRO: the member's own subscription panel. */
.pro-owned-line { color: var(--text-mid); line-height: 1.6; margin: 0; }
.pro-owned-actions { margin-top: 14px; }
/* Cancelling is a quiet, secondary action — it should be findable, not
   inviting. Muted until hovered, and only then does it turn red. */
.pro-cancel {
  background: none;
  border: 1px solid var(--border);
  border-radius: 9px;
  padding: 8px 14px;
  font-family: var(--font);
  font-size: 12.5px;
  font-weight: 700;
  color: var(--text-dim);
  cursor: pointer;
  transition: color .15s, border-color .15s, background .15s;
}
.pro-cancel:hover { color: #f87171; border-color: rgba(248,113,113,0.45); }
/* Armed: the second click confirms. Red now, because at this point it will
   actually do something. */
.pro-cancel.is-armed {
  color: #fff;
  background: #dc2626;
  border-color: #dc2626;
}
.pro-cancel:disabled { opacity: .6; cursor: default; }

/* ── settings: the PRO page ────────────────────────────────────────────────
   
   VISIBLE TO EVERYONE, editable only with PRO. Hiding the page from non-members
   would mean nobody ever discovers what PRO changes, and the settings modal is
   exactly where a person goes looking. So the page shows precisely what you
   would get and refuses the click — which is a different and more honest thing
   than pretending it does not exist. */

/* The rail label wears the wordmark face, the same type as the header pill and
   the badge in chat — so PRO reads as one thing appearing in a third place
   rather than a menu item that happens to share a name. */
.set-pro-word {
  font-family: var(--font-display, var(--font));
  font-size: 14px;
  font-weight: 800;
  letter-spacing: -0.01em;
  background-image: linear-gradient(100deg, #7c3aed, #d946ef 36%, #fbbf24 70%, #fde047);
  background-size: 220% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* Shares --pro-delay with the header pill and the chat badge, so every PRO
     surface on the page is at the same point of the same 6s cycle. */
  animation: navProShift 6s ease-in-out infinite;
  animation-delay: var(--pro-delay, 0s);
}
/* The star follows the wordmark's warm end rather than staying --text-dim,
   or the icon reads as belonging to the rail and the label to something else. */
.set-nav-pro svg { color: #fbbf24; }

.set-pro-note {
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 14px;
  padding: 11px 13px;
  border: 1px solid rgba(217,70,239,0.28);
  border-radius: 10px;
  background: rgba(217,70,239,0.06);
  font-size: 12px; line-height: 1.5;
  color: var(--text-mid);
}
.set-pro-note.is-active {
  border-color: rgba(251,191,36,0.30);
  background: rgba(251,191,36,0.06);
}
.set-pro-chip {
  flex: 0 0 auto;
  padding: 3px 8px; border-radius: 999px;
  font-family: var(--font-display, var(--font));
  font-size: 11px; font-weight: 800; letter-spacing: 0.02em;
  color: #fff;
  background-image: linear-gradient(100deg, #7c3aed, #d946ef 36%, #fbbf24 70%, #fde047);
  background-size: 220% 100%;
  animation: navProShift 6s ease-in-out infinite;
  animation-delay: var(--pro-delay, 0s);
}
.set-pro-txt { flex: 1 1 auto; }
.set-pro-get {
  flex: 0 0 auto;
  height: 28px; padding: 0 12px;
  border: 1px solid rgba(217,70,239,0.45);
  border-radius: 8px;
  background: var(--surface-3);
  font-family: var(--font); font-size: 12px; font-weight: 700;
  color: #f0abfc; cursor: pointer;
  transition: border-color .18s ease, color .18s ease, transform .12s ease;
}
.set-pro-get:hover { border-color: rgba(217,70,239,0.8); color: #fff; }
.set-pro-get:active { transform: translateY(1px); }

/* LOCKED: readable, not editable.
   
   Deliberately NOT `opacity: .4` on the whole pane. The point of showing this
   page is that you can read what PRO changes, and dimming the titles and
   descriptions is exactly the part you must not dim. So the TEXT stays at full
   strength and only the controls read as inert. */
.set-page.is-locked .set-seg,
.set-page.is-locked .set-menu {
  opacity: .55;
  cursor: not-allowed;
}
.set-page.is-locked .set-sg,
.set-page.is-locked .set-menu-head { cursor: not-allowed; }

/* A refused click has to say so. Silence reads as a broken control, and
   "I clicked it and nothing happened" makes people conclude the site is
   broken rather than that the feature is bought. */
@keyframes setProBump {
  0%   { transform: translateX(0); }
  25%  { transform: translateX(-3px); }
  55%  { transform: translateX(3px); }
  100% { transform: translateX(0); }
}
.set-pro-note.is-bumped {
  animation: setProBump .32s cubic-bezier(.4,0,.2,1),
             navProShift 6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .set-pro-word, .set-pro-chip, .set-pro-note.is-bumped { animation: none; }
}
