/* ============================================================
 *  shell.css — the frame every donut site shares.
 *
 *  Nav, footer, mobile drawer, account menu, promo pill, settings and link
 *  modals, the .cd dropdown, the live feed rail, chart.js's renderer, and the
 *  shared control vocabulary (.card, .pill, .btn-*, .search-box).
 *
 *  THE TEST for anything added here: would it still be on screen with <main>
 *  emptied, or is it already used by two different pages? If not, it belongs in
 *  the site's own stylesheet.
 *
 *  EDITED ON donutsmpstats ONLY, then copied to the other sites. Editing a copy
 *  in place is how two navs that are almost the same get created.
 *
 *  Load order: tokens.css -> shell.css -> <site>.css
 *  shell-version: 1
 * ============================================================ */


@keyframes ndVeil { from { opacity: 0; } to { opacity: 1; } }

/* 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); }
}
@keyframes searchShake {
  0%,100% { transform: translateX(0); }
  20% { transform: translateX(-5px); }
  40% { transform: translateX(5px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(3px); }
}
@keyframes feedPulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
@keyframes feedSkel { 0%,100% { opacity: .30; } 50% { opacity: .5; } }
@keyframes mcGlint {
  from { background-position: 160% 0; }
  to   { background-position: -60% 0; }
}
@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);
  }
}
@keyframes ah-delta-a { 0%, 69% { opacity: 1; } 72%, 100% { opacity: 0; } }
@keyframes ah-delta-b { 0%, 69% { opacity: 0; } 72%, 100% { opacity: 1; } }
/* 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; } }
@keyframes set-confirm-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes set-confirm-rise {
  from { opacity: 0; transform: translateY(10px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
@keyframes ox-nudge {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-4px); }
  40%, 60% { transform: translateX(4px); }
}
@keyframes proTagShift {
  0%, 100% { background-position: 0% 50%; }
  50%      { background-position: 100% 50%; }
}
@keyframes navProShift {
  0%, 100% { background-position: 0% 50%; }
  50%      { background-position: 100% 50%; }
}
@keyframes ahGlint {
  from { background-position: 140% 0; }
  to   { background-position: -140% 0; }
}

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

/* ── 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);

  /* CENTRED ON THE PAGE, not on the leftover space.
   *
   * The grid is `1fr auto 1fr`, and a bare 1fr is minmax(AUTO, 1fr) — it will
   * not shrink below its content. So the moment one side outgrows its track
   * (a long username, the promo pill changing face, "Sign in" becoming an
   * account chip) that side pushes its own track wider and slides the middle
   * one off centre. The links then MOVE as you sign in, or every seven seconds
   * as the pill morphs. Exactly the bug that put the server-stats tab off
   * centre, in a different file.
   *
   * Taking the links out of the flow removes the dependency entirely: they are
   * positioned against the navbar itself, so no width on either side can reach
   * them. The navbar is position:sticky, which is a positioned ancestor, so it
   * is already the containing block.
   */
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}

/* Taking .nav-links out of the flow left the grid with THREE columns and only
   TWO items — so nav-right fell into column 2, the middle, and the sign-in chip
   and promo pill centred themselves alongside the links. The two remaining
   items are pinned explicitly rather than left to auto-placement. */
.nav-left  { grid-column: 1; justify-self: start; }
.nav-right { grid-column: 3; justify-self: end; }

/* Dropdown entries are links only so they can leave the origin — they are rows
   in a menu, not links in prose, and the underline was the browser's default
   showing through on the one kind of nav item that is a real <a>. */
.nav-dropdown-menu a { text-decoration: none; }

.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;
}@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;
}
.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); }@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;
}

/* ---------- 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; }
}
@media (max-width: 720px) {
  .nav-links { display: none; }
}

  .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); }
  .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); }
  .card { text-align: center; }

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

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

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

/* 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-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 hit area and the nudge were living in donutstats' SITE css, scoped to
   `.ah-hero > .ah-back` — so every other use of this control got the markup and
   the transition but none of the movement, and read as a dead link. Both are
   generic now. The negative margin keeps the label optically flush with the
   content beneath it while the padding still gives a real target: the text
   alone is a ~50x16 hit area. */
.ah-back { padding: 6px 10px; border-radius: 8px; margin-left: -10px; }
.ah-back:hover .ah-back-inner { transform: translateX(-3px); color: var(--text); }
/* 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;
}

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

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

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

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

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

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

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

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

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


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


.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); }

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

/* ── 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;
  /* A fixed, full-viewport layer at opacity 0 is still IN FRONT of the page and
     still receives every click. settings.js works around it by setting
     display:none after the transition, which means any other modal built on
     this class has to remember to do the same — and one that forgets leaves the
     site looking frozen until a refresh. Making the closed state
     non-interactive fixes it for every user of the class at once; the timed
     display:none is still fine on top of this. */
  pointer-events: none;
}
.set-backdrop.open { opacity: 1; pointer-events: auto; }

/* 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. */
  /* NO scrollbar-width here. Chromium 121+ treats the standard property as
     opt-in to the standard scrollbar and then IGNORES every
     ::-webkit-scrollbar rule below — which is why the end-cap arrows kept
     coming back however many sub-parts were set to display:none. The
     webkit rules do the work in Chromium and Safari; Firefox, which has no
     arrows to remove, gets the thin treatment in the @supports block after
     this rule. */
}
.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; }
@supports (-moz-appearance: none) {
  .set-menu-body { scrollbar-width: thin; scrollbar-color: var(--border-bright) transparent; }
}

/* No end caps on ANY inner scroll area. The page's own scrollbar is an overlay
   with a bare thumb and no buttons, so a panel that sprouts little arrow boxes
   is the one place the native control shows through and gives the platform
   away. Scoped to modals and menus rather than `*`, which would also hit
   anything a future embed brings with it. */
.set-backdrop ::-webkit-scrollbar-button,
.set-menu-body::-webkit-scrollbar-button,
.ox-modal ::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
.set-backdrop ::-webkit-scrollbar-corner,
.ox-modal ::-webkit-scrollbar-corner { 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; }
.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;
}
.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); }
/* 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;
}
#view, main > .view, [data-view] { transition: opacity .16s ease; }

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


/* 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 (min-width: 1101px) {
  .navbar { padding-right: 82px; }
}





/* ════════════════════════════════════════════════════════════════════════
   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; }@media (max-width: 1130px) {
  .navbar { padding-right: 32px; }
}






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




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



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


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


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


/* 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;
}@media (prefers-reduced-motion: reduce) {
  .ah-img.is-enchanted::after { animation: none; }
}


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

/* ══ HOMEPAGE HERO ═══════════════════════════════════════════════════════════
 *
 * SHARED, so every donut site opens the same way: the wordmark, the blue bloom
 * behind it, and a strip of four figures under a one-line claim.
 *
 * It lives in the shell rather than in each site's CSS because it is the first
 * thing anyone sees on any of them, and four near-copies of it is exactly the
 * drift the shell exists to prevent. What differs per site is only the WORDS
 * and the four numbers — markup and config, not style.
 *
 * The wrapper that positions it (`.home-top`) stays with the SITE: donutstats
 * fills the first screen with a hero plus a carousel, donutdiscords with a hero
 * plus a tag row, and those want different vertical arrangements. The hero
 * itself is identical; the room around it is a page decision.
 *
 * donutstats currently also defines these in its own site.css. Identical values,
 * so nothing changes while both exist — delete them there when convenient and
 * this becomes the only copy.
 * ═══════════════════════════════════════════════════════════════════════════ */
/*
 * donutstats' homepage hero, copied WHOLE this time.
 *
 * My first pass dropped eight rules — the `body:has(.home-hero) …` block, which
 * is where the real sizing lives. Those express the hero's rhythm in VH rather
 * than fixed pixels, so the wordmark and the counts scale with the window
 * instead of being cropped on a short one. Without them the hero rendered at
 * the fallback sizes and simply looked like a smaller, different hero. I lost
 * them by splitting the extracted text on line starts, which silently took the
 * `body:has(…)` rules along with the `.home-top` ones they happened to sit
 * beside — a reminder that CSS should be cut by parsing it, not by slicing text.
 *
 * Carousel rules are the only deliberate omission: there is no carousel here,
 * and `.home-top` with it, since that wrapper exists to fit hero + carousel to
 * exactly one viewport.
 */




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




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


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); }

/* 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); }@media (max-height: 560px) {

  body:has(.home-hero) main { min-height: 0; }
}




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

