/* ============================================================
 *  site.css — donutsmpstats' own pages.
 *
 *  Everything that lives inside <main> on this site and nowhere else: the
 *  auction house, the item page, leaderboards, the player profile, spawner and
 *  market, vouches, server stats, the homepage hero and showcase, /pro.
 *
 *  Loaded AFTER shell.css, so anything here wins a tie against the shell — that
 *  is the intended direction. A page may lean on the shell; the shell must
 *  never need a page.
 * ============================================================ */



/* 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 spinnerFadeIn { to { opacity: 1; } }
  @keyframes spin { to { transform: rotate(360deg); } }
  @keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
/* Clean switch: a quick, simple opacity settle (no slide/vertical morph) so
   changing boards reads as a crisp swap rather than a flash. */
@keyframes lbpCleanSwap {
  from { opacity: 0.35; }
  to   { opacity: 1; }
}
/* Staggered pop-in for the list rows on first load — each row grows its height
   AND fades up, so the panel visibly expands with every player that appears
   (no empty background reserved ahead of time). */
@keyframes lbpRowIn {
  from {
    opacity: 0;
    transform: translateY(6px);
    max-height: 0;
    margin-top: 0; margin-bottom: 0;
    padding-top: 0; padding-bottom: 0;
  }
  to {
    opacity: 1;
    transform: translateY(0);
    max-height: 70px;
    padding-top: 11px; padding-bottom: 11px;
  }
}
@keyframes ahSalesPulse {
  0%   { box-shadow: 0 0 0 0 rgba(34,197,94,0.45); }
  70%  { box-shadow: 0 0 0 6px rgba(34,197,94,0); }
  100% { box-shadow: 0 0 0 0 rgba(34,197,94,0); }
}
@keyframes ahSalesSlide {
  0% { opacity: 0; transform: translateY(-8px); }
  30%, 100% { opacity: 1; transform: translateY(0); }
}
@keyframes ahSalesFlash { 0% { opacity: 0; } 14% { opacity: 1; } 100% { opacity: 0; } }
@keyframes vchSkel { 0%,100% { opacity: .25; } 50% { opacity: .45; } }
@keyframes ah-pulse { 0%,100% { opacity: .5; } 50% { opacity: .85; } }
@keyframes mk-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes hsPress {
  0%   { transform: scale(1); filter: brightness(1); }
  40%  { transform: scale(.94); filter: brightness(1.35); }
  100% { transform: scale(1); filter: brightness(1); }
}
@keyframes hsNudge {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(2px); }
}
html::-webkit-scrollbar { width: 0; height: 0; display: none; }

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

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

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

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

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

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


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

  /* Vouch/scam pills pegged to the bottom-right of the header area, so they
     sit above the rightmost stat card (deaths) and stay out of the way. */
  /* bottom: 0, not 32px.
     These are absolutely positioned, so they sit OUTSIDE the flex flow — which is
     why align-items:flex-end on .player-header had no effect on them. The 32px
     was matched to the header's old padding-bottom; that padding is gone now, so
     it was just lifting them 32px off the baseline the character sits on.
     At 0 they bottom-align with .skin-box, and both get the grid's 12px below. */
  .vouch-pills {
    display: flex; gap: 8px;
    position: absolute; right: 0; bottom: 0;
  }
  .action-row { display: flex; gap: 10px; margin-top: 4px; }

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

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

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

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

/* REVERTED: main is no longer a query container.
   
   I made <main> one to fix a suspected mismatch between the window width and
   the content width — then measured it and found no mismatch at all (window
   1178, content 1177.6). The diagnosis was wrong, the change fixed nothing,
   and `container-type: inline-size` brings real containment with it, which is
   its own way for a layout to clip.
   
   The three rules converted to @container are put back to @media below. A
   change that did not fix the thing it was made for should not be left in on
   the chance it helps something else. */



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

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

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

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

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

/* Skeleton shimmer */
.crow-skeleton .chead-ph, .crow-skeleton .cname-ph, .crow-skeleton .cval-ph,
.chead-ph, .cname-ph, .cval-ph, .bbar-ph {
  background: linear-gradient(90deg, var(--surface-3) 25%, var(--surface-4) 37%, var(--surface-3) 63%);
  background-size: 400% 100%; animation: shimmer 1.4s ease infinite; border-radius: 4px;
}
/* The skeleton placeholder MUST match the real avatar, per context.
   It was hardcoded to 24px while .lcard-compact .chead is 20px — so skeleton
   rows stood 4px taller than the rows that replaced them. Across five rows the
   card shrank 20px the moment data arrived, the carousel band resized, the page
   reflowed, and that reflow is the flash on the homepage. A placeholder whose
   only job is to hold the right space has to hold the RIGHT space. */
.crow-skeleton .chead-ph { width: 24px; height: 24px; }
.lcard-compact .crow-skeleton .chead-ph { width: 20px; height: 20px; }
.crow-skeleton .cname-ph { flex: 1; height: 12px; }
.crow-skeleton .cval-ph { width: 48px; height: 12px; }

/* ── Homepage carousel wrapper ────────────────────────────────────────────── */
/* Horizontal clip (so the track scrolls invisibly) but generous vertical
   padding so a hovered/scaled card has room to grow without being cut off at
   the top or bottom. overflow stays hidden on X via the clip; the padding
   creates the vertical breathing space. */
/* Back to the original pair, deliberately.
   `overflow-x: clip; overflow-y: visible` is not strictly legal — a `visible`
   paired with a `clip` computes to `auto` — and I replaced it with `overflow:
   clip` plus a 24px clip margin. That was wrong twice: the cards need far more
   than 24px of vertical room for their hover lift and shadows, so it cropped
   them, and the real cause of the load flash was a skeleton placeholder 4px
   taller than the avatar that replaced it (see .chead-ph below). Fixing the
   right thing made this change unnecessary. */
.home-carousel { position: relative; overflow-x: clip; overflow-y: visible; padding: 34px 0 40px; }

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

/* centered header */
.lbp-head { padding: 32px 26px 8px; text-align: center; }
.lbp-head-swap { animation: lbpCleanSwap .2s ease both; }
.lbp-list-swap { animation: lbpCleanSwap .22s ease both; }@media (prefers-reduced-motion: reduce) {
  .lbp-head-swap, .lbp-list-swap { animation: none; }
}

.lbp-panel { animation: shellRise .5s cubic-bezier(0.16,1,0.3,1) both; }@media (prefers-reduced-motion: reduce) { .lbp-panel { animation: none; }
}

.lbp-badge {
  width: 60px; height: 60px; margin: 0 auto 14px; border-radius: 15px;
  background: linear-gradient(160deg, var(--surface-4), var(--surface-3));
  border: 1px solid var(--border-bright);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 1px 0 rgba(255,255,255,0.06) inset, 0 6px 18px rgba(0,0,0,0.4);
}
.lbp-badge img { width: 34px; height: 34px; image-rendering: pixelated; }
.lbp-title { font-size: 29px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.1; margin: 0; }
.lbp-sub { font-size: 13px; color: var(--text-mid); margin: 5px 0 0; }

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

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

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


/* list */
.lbp-list {
  padding: 10px 12px 8px;
  margin-top: 6px;
}
.lbp-row {
  display: flex; align-items: center; gap: 15px; padding: 11px 14px;
  border-radius: 11px; transition: background .12s;
}
.lbp-row-enter {
  animation: lbpRowIn .34s cubic-bezier(0.22,1,0.36,1) both;
  overflow: hidden;
}@media (prefers-reduced-motion: reduce) {
  .lbp-row-enter { animation: none; }
}

/* Alternating row tint so long lists are easier to scan. Odd rows stay clear,
   even rows get a faint fill; hover still lifts above both. */
.lbp-row:nth-child(even) { background: rgba(120,160,235,0.035); }
.lbp-row:hover { background: var(--card-bg-hover); }
.lbp-rank { width: 26px; text-align: center; font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; flex-shrink: 0; }
.lbp-phead { width: 36px; height: 36px; border-radius: 9px; image-rendering: pixelated; background: var(--surface-3); flex-shrink: 0; box-shadow: 0 1px 3px rgba(0,0,0,0.4); }
.lbp-name {
  max-width: 60%; min-width: 0; font-size: 15px; font-weight: 600; color: rgba(255,255,255,0.55); letter-spacing: -0.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lbp-name.clink { cursor: pointer; transition: color .12s; }
.lbp-name.clink:hover { color: #fff; }
.lbp-val { margin-left: auto; font-size: 15.5px; font-weight: 700; color: var(--blue-bright); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; flex-shrink: 0; }
.lb-empty { font-size: 13px; color: var(--text-dim); text-align: center; padding: 40px 0; }

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

/* skeleton reuse */
.lbp-row .chead-ph { width: 36px; height: 36px; border-radius: 9px; }
.lbp-row .cname-ph { flex: 1; height: 13px; }
.lbp-row .cval-ph { width: 54px; height: 13px; margin-left: auto; }@media (max-width: 560px) {
  .lbp-title { font-size: 24px; }
  .lbp-name { font-size: 14px; }
  .lbp-val { font-size: 14px; }
}


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

/* Sell stage header: label + /sell|/ah toggle (reuses .spc-method styles). */
.spc-stage-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 16px; }
.spc-stage-head .spc-stage-label { margin-bottom: 0; }@media (max-width: 700px) {
  .spc-stages { grid-template-columns: 1fr; }
  .spc-stage { padding: 18px 2px; }
  .spc-stage-produce { padding-top: 2px; }

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


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

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

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

/* base items — single row, no box: just icon over name */
.ah-base-section { margin-top: 24px; }
.ah-base-row { margin-top: 0; }
/* As many whole tiles as fit, never a sliced one.
   auto-fill works out how many columns of at least 68px the row can hold;
   anything past that wraps onto a second row, which is 0px tall and clipped.
   So the strip always ends on a complete tile — icon and name — and the count
   falls from 14 to 13 to 9 to 7 by itself as the window narrows.
   The min is max(68px, one-fourteenth) so a wide screen still lands on
   exactly 14 rather than making room for a fifteenth column that has nothing
   to put in it. */
.ah-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill,
    minmax(max(68px, calc((100% - 104px) / 14)), 1fr));
  grid-template-rows: auto;
  grid-auto-rows: 0;          /* every row after the first has no height */
  gap: 8px;
  overflow: hidden;           /* ...and is clipped rather than showing a sliver */
}
.ah-tile {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 8px 2px; min-width: 0;
  cursor: pointer; transition: transform .14s;
}
.ah-tile:hover { transform: translateY(-3px); }
.ah-tile:hover .ah-tile-name { color: var(--blue-bright); }
.ah-tile-ico { width: 34px; height: 34px; display: flex; align-items: center; justify-content: center; }
.ah-tile-name {
  font-size: 9.5px; font-weight: 600; color: var(--text-mid); text-align: center;
  line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
  transition: color .14s;
}
.ah-tile-ico .ah-img { width: 32px; height: 32px; }
.ah-card-ico .ah-img { width: 30px; height: 30px; }
.ah-hero-ico { display: inline-flex; flex-shrink: 0; }
.ah-hero-ico .ah-img { width: 48px; height: 48px; image-rendering: pixelated; }
.ah-hero-ico .ah-glyph { width: 60px; height: 60px; border-radius: 10px; font-size: 18px; }
.ah-card-ico .ah-glyph, .ah-tile-ico .ah-glyph { width: 30px; height: 30px; }

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

/* ── item detail ── */
/* The item page is short (a card + a chart) and was sitting top-aligned with a
   pile of dead space underneath. Centring it in the viewport — minus the header
   and footer — puts the chart where the eye already is.
   min-height, not height: a taller chart or a long name must still push the page
   down rather than overflow a fixed box. */
.ah-item {
  padding-top: 4px;
  min-height: calc(100vh - 210px);
  display: flex; flex-direction: column; justify-content: center;
}@media (max-height: 700px) { .ah-item { min-height: 0; justify-content: flex-start; }
}

/* The hero is now a ROW, not a card: only .ah-hero-card is boxed, and it sizes
   to its contents rather than stretching the full width. Everything else (the
   track button, the freshness) sits outside it on the page. */
.ah-hero {
  position: relative;
  display: flex; justify-content: flex-start; align-items: center; gap: 16px;
  margin: 40px 0 0; padding: 0;
  background: none; border: 0; border-radius: 0; box-shadow: none;
}
/* The hero card's shadow pointed down and shoved the stats card visually away;
   tightening it lets the two sit close without looking glued. */
.ah-hero-card { box-shadow: 0 6px 18px rgba(0,0,0,0.25) !important; }
/* Back is positioned against the HERO ROW, not the card — that's what keeps it
   out of the card's :hover. The padding is still a real hit area (the text alone
   was a ~50x16 target); it just isn't painted. */
.ah-hero > .ah-back {
  position: absolute; left: -8px; top: -26px; margin: 0;
  padding: 6px 10px; border-radius: 8px;
  background: none;
  z-index: 3;   /* above the card, so its hit area always wins */
}
/* The label nudges and brightens — no background wash. A translucent white block
   behind text on a dark page reads as a rendering artefact, not a hover. */
.ah-hero > .ah-back:hover .ah-back-inner { transform: translateX(-3px); color: var(--text); }
/* padding back to 16: the reserved space was only needed while .ah-hero-kind was
   absolutely positioned. It's in the flow now, so the card sizes to it. */
.ah-hero-card {
  position: relative; z-index: 2;
  display: inline-flex; align-items: center; gap: 18px;
  padding: 16px 26px 16px 20px;
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.28);
  /* No transition either — nothing changes on hover any more, and leaving one
     here would just be a rule waiting to animate the next thing someone adds. */
}@media (prefers-reduced-motion: reduce) { .ah-hero-card { transition: none; } .ah-hero-card:hover { transform: none; }
}

.ah-hero-info { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
/* Name sits ABOVE the price as a quiet caption — the price is the headline, so
   the hierarchy is size, not two competing bold lines. */
.ah-hero-name {
  font-size: 15px; font-weight: 700; color: var(--text-mid);
  letter-spacing: .02em; white-space: nowrap; text-align: center;
}
.ah-hero-info { align-items: center; }
.ah-hero-side { display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
.ah-pill { font-size: 11px; font-weight: 600; padding: 3px 9px; border-radius: 6px; }
.ah-pill.ok { color: var(--green); background: rgba(34,197,94,0.12); border: 1px solid rgba(34,197,94,0.3); }
.ah-pill.warn { color: var(--gold); background: rgba(245,158,11,0.10); border: 1px solid rgba(245,158,11,0.3); }
/* Above the freshness line, outside the card. The rate is the more useful of the
   two, so it takes the higher slot. */
/* What the headline number MEANS. Same shape as the profile view counter: a
   small pill pinned inside the card, qualifying the big number without competing
   with it.
     Current price — enough recent sales to trust it
     24hr average  — thin lately; a day's worth says roughly this
     Asking price  — nobody's buying; a hope, not a price
   The card is position:relative already (the icon glow uses it), so this anchors
   to the card, not the page. */
/* Centred under the price, in the flow. Bare coloured text — no pill: it's a
   caption on the number, and a chip would compete with what it describes. */
.ah-hero-kind {
  display: block; text-align: center; margin-top: 1px;
  font-size: 9.5px; font-weight: 700; letter-spacing: .02em;
  white-space: nowrap;
}
.ah-hero-kind.sold { color: var(--green); }
.ah-hero-kind.avg  { color: var(--blue-bright); }
/* Amber: an asking price is materially weaker evidence and should look like it. */
.ah-hero-kind.ask  { color: #eab308; }

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

/* Outside the card, at its bottom-right. Absolute so it can't affect its size. */
.ah-hero-fresh {
  position: absolute; left: calc(100% + 14px); bottom: 3px;
  font-size: 12px; color: var(--text-dim); opacity: .85;
  white-space: nowrap; pointer-events: none;
}
.ah-hero-price {
  font-family: var(--font-display);
  font-size: 34px; font-weight: 800; color: var(--green);
  line-height: 1; letter-spacing: -0.02em; white-space: nowrap;
}
.ah-hero-btn {
  height: 40px; padding: 0 18px; border-radius: 9px; border: 0; cursor: pointer;
  font-family: var(--font); font-size: 13px; font-weight: 700; transition: background .15s, opacity .15s;
}
.ah-hero-btn.track { background: var(--blue-dim); color: #fff; }
.ah-hero-btn.track:hover { background: var(--blue); }
.ah-hero-btn.untrack { background: var(--surface-3); color: var(--text-mid); border: 1px solid var(--border); }
.ah-hero-btn.untrack:hover { color: var(--text); border-color: var(--border-bright); }
.ah-hero-btn:disabled { opacity: .6; cursor: default; }
/* Player stats history: the outline of the whole unit (active tab + card) takes
   the SELECTED stat's colour (money green, shards purple, …) via --panel-c set
   in drawSeries, so the panel reads as one colour-coded block. */
.pl-chart-wrap .ah-chart-card { border-color: var(--panel-c, rgba(120,160,235,0.30)); }

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


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

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

.ah-sales-unit {
  font-size: 10.5px; color: var(--text-dim);
  font-family: var(--font); font-variant-numeric: tabular-nums;   /* see .ah-sales-price */
}
.ah-sales-time { font-size: 10px; color: var(--text-dim); justify-self: end; white-space: nowrap; min-width: 62px; text-align: right; }
.ah-sales-empty { font-size: 13px; color: var(--text-dim); text-align: center; padding: 26px 8px; }

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

/* Player chart: mirror the title. The switch is centered in the right area
   (right of the centered tab strip), pegged to the reserved tab width so it
   never shifts with tab count / tracked state. The AH item chart is unaffected. */
.pl-chart-wrap .ah-chart-head {
  width: calc(50% - var(--tabs-reserve, 467px) / 2);
  justify-content: center;
}
.pl-chart-wrap .ah-range-btn { font-size: 10px; padding: 5px 10px; letter-spacing: .03em; }

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

/* Top metric cards acting as selectors (tracked players only). */
/* Top metric cards acting as selectors (tracked players only). Hover matches the
   homepage sliding cards: grow from center, float shadow, bright blue ring. */
.stat-card-link { cursor: pointer; position: relative; transition: transform .2s cubic-bezier(0.22,1,0.36,1), box-shadow .2s ease, background .2s ease; }
.stat-card-link:hover {
  transform: scale(1.06);
  box-shadow: 0 26px 60px rgba(0,0,0,0.6), 0 10px 28px rgba(0,0,0,0.42), 0 0 0 1px var(--blue-border);
  z-index: 3;
}
/* Selected: an outline in the stat's OWN colour, no background fill. It's an
   ::before ring so picking a new stat animates the outline in with a little pop
   rather than snapping. */
.stat-card-link::before {
  content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  box-shadow: inset 0 0 0 1.5px var(--card-c, var(--blue));
  opacity: 0; transform: scale(1.04);
  transition: opacity .2s ease, transform .32s cubic-bezier(0.34,1.5,0.5,1);
}
.stat-card-link.is-selected::before { opacity: 1; transform: scale(1); }@media (max-width: 640px) {
  .pl-stat-tab { width: 36px; height: 27px; }
  .pl-stat-tab.is-on { height: 34px; }
  /* Not enough width to reserve the full tab row or center the title beside it —
     fall back to a natural centered strip and a left title. */
  #pl-legend { min-width: 0; }
  .pl-chart-title { width: auto; min-width: 0; justify-content: flex-start; }
  .pl-chart-wrap .ah-chart-head { width: auto; justify-content: flex-end; }
  .pl-stat-ic { width: 18px; height: 18px; }
}


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

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

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


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

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


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

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

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

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

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

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

.ah-ac-row {
  display: flex; align-items: center; gap: 10px; width: 100%;
  /* Square + full-bleed: the shell's radius does all the corner shaping. */
  background: none; border: 0; border-radius: 0; cursor: pointer;
  padding: 10px 20px; text-align: left;
  font-family: var(--font); color: var(--text-mid);
  transition: background .12s, color .12s;
}
.ah-ac-row:hover, .ah-ac-row.on { background: var(--surface-3); color: var(--text); }
.ah-ac-row .ah-img, .ah-ac-row .ah-glyph { pointer-events: none; }
.ah-ac-row .ah-img { width: 22px; height: 22px; image-rendering: pixelated; flex-shrink: 0; }
.ah-ac-row .ah-glyph { width: 22px; flex-shrink: 0; font-size: 10px; color: var(--text-dim); }
.ah-ac-name {
  font-size: 13.5px; font-weight: 600; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}

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

/* skeletons */
.ah-tile.sk { height: 60px; border-radius: 8px; background: var(--card-bg); }
.ah-card.sk { height: 58px; background: var(--card-bg); border: 1px solid var(--border-soft); }
.sk-hero { height: 180px; }
.ah-skeleton-row {
  display: grid;
  grid-template-columns: repeat(auto-fill,
    minmax(max(68px, calc((100% - 104px) / 14)), 1fr));
  grid-template-rows: auto;
  grid-auto-rows: 0;
  gap: 8px;
  overflow: hidden;
}
.ah-skeleton-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.ah-tile.sk, .ah-card.sk, .sk-hero { animation: ah-pulse 1.4s ease-in-out infinite; }@media (prefers-reduced-motion: reduce) { .ah-tile.sk, .ah-card.sk, .sk-hero { animation: none; }
}
@media (max-width: 760px) {
  /* No fixed count here any more — .ah-tiles fits whatever the width allows
     and clips the rest, at every size. */
  .ah-hero { flex-direction: column; }
  .ah-hero-price { font-size: 34px; }
}
@media (max-width: 620px) {
  .ah-cards, .ah-skeleton-cards { grid-template-columns: 1fr; }
}


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

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

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

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

/* leaderboard rows */
.vch-lb { display: flex; flex-direction: column; gap: 8px; }
.vch-lb-row {
  display: flex; align-items: center; gap: 12px; padding: 10px 14px;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 10px;
  cursor: pointer; transition: transform .14s, border-color .14s, background .14s;
}
.vch-lb-row:hover { transform: translateY(-2px); border-color: var(--blue-border); background: var(--card-bg-hover); }
.vch-rank { font-size: 12px; font-weight: 800; color: var(--text-dim); width: 22px; flex-shrink: 0; }
.vch-av { width: 32px; height: 32px; border-radius: 6px; flex-shrink: 0; image-rendering: pixelated; }
/* The name is now the ONLY thing in the middle, so it centres against the
   avatar instead of sitting high above a second line. */
.vch-lb-main { display: flex; align-items: center; min-width: 0; flex: 1; }
.vch-lb-name { font-size: 13.5px; font-weight: 700; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* The percentage and its breakdown, as one block on the right.
   
   The tallies are the two numbers the percentage is CALCULATED from, so they
   belong beside it rather than under the name — where they read as a caption
   for the player and forced the name into a two-line box. Stacked, they come
   out the same height as the figure they explain, which is what lets the whole
   row line up on one baseline. */
.vch-lb-right {
  display: flex; align-items: center; gap: 10px;
  flex-shrink: 0;
}
.vch-lb-sub {
  display: flex; flex-direction: column; gap: 2px;
  font-size: 10.5px; color: var(--text-mid);
  line-height: 1;
}
.vch-lb-sub .vch-tally { display: inline-flex; align-items: center; gap: 2px; }
.vch-lb-pct { font-size: 18px; font-weight: 800; display: flex; flex-direction: column; align-items: flex-end; line-height: 1; flex-shrink: 0; }
.vch-lb-pct-label { font-size: 9px; font-weight: 400; color: var(--text-dim); margin-top: 2px; }
.vch-lb-pct.good { color: var(--green); }
.vch-lb-pct.mid  { color: var(--gold); }
.vch-lb-pct.bad  { color: var(--red); }
.vch-lb-pct.none { color: var(--text-dim); }

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

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

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

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

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

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

.vch-list-count { font-size: 11.5px; color: var(--text-dim); }@media (max-width: 620px) {
  .vch-hero { justify-content: center; text-align: center; }
  .vch-hero-id { flex-direction: column; }
  .vch-hero-name { white-space: normal; }
}


/* cast bar */
/* The action, not a container for it.
   
   It was a full card whose entire contents were a label and two buttons — a
   box drawn around a decision rather than the decision itself. Now it is a
   quieter strip that belongs to the hero above it: no competing surface, a
   hairline top rule instead of a border on all four sides, and the buttons
   given the room they were asking for. */
.vch-cast {
  margin-top: 14px; padding: 16px 30px 18px;
  background: linear-gradient(180deg, rgba(120,160,235,0.035), transparent);
  border: 1px solid var(--border-soft);
  border-radius: 14px;
}
.vch-cast-self, .vch-cast-guest { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.vch-cast-label { font-size: 12.5px; font-weight: 600; color: var(--text-mid); }
.vch-cast-row { display: flex; gap: 10px; margin-top: 12px; }
/* The same control shape as the payment modal's method rows and oxverify's
   link buttons — surface, hairline border, 10px radius, inset highlight — so a
   decision on this page looks like a decision anywhere else on the estate. */
.vch-cast-btn {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--border); background: var(--surface-3); color: var(--text);
  font-family: var(--font); font-size: 13px; font-weight: 700; padding: 11px 18px;
  border-radius: 10px; cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.05);
  transition: border-color .14s, background .14s, color .14s, transform .1s;
}
.vch-cast-btn:active { transform: translateY(1px); }
.vch-cast-btn.pos:hover, .vch-cast-btn.pos.sel { border-color: rgba(34,197,94,0.5); background: rgba(34,197,94,0.12); color: var(--green); }
.vch-cast-btn.neg:hover, .vch-cast-btn.neg.sel { border-color: rgba(239,68,68,0.5); background: rgba(239,68,68,0.12); color: var(--red); }
.vch-cast-expand { margin-top: 14px; }
.vch-cast-expand textarea {
  width: 100%; min-height: 60px; resize: vertical; box-sizing: border-box;
  background: var(--page-bg); border: 1px solid var(--border); border-radius: 9px;
  color: var(--text); font-family: var(--font); font-size: 13px; padding: 10px 12px; outline: none;
}
.vch-cast-expand textarea:focus { border-color: var(--blue-border); }
.vch-cast-actions { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; }
.vch-cast-verdict { font-size: 12px; font-weight: 600; }
.vch-cast-verdict.pos { color: var(--green); }
.vch-cast-verdict.neg { color: var(--red); }
.vch-cast-submit {
  background: var(--blue-dim); color: #fff; border: 0; border-radius: 8px;
  font-family: var(--font); font-size: 12.5px; font-weight: 700; padding: 8px 18px; cursor: pointer;
}
.vch-cast-submit:hover { background: var(--blue); }
.vch-cast-submit:disabled { opacity: .6; cursor: default; }
.vch-cast-guest .btn-search { flex-shrink: 0; }

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


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

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

/* ── Recently-searched live rail (/players) — uses the AH live-sales feed
   classes (.ah-feed / .ah-feed-clip / .ah-feed-card) verbatim, so it looks and
   animates EXACTLY like the item sale history: skull, balance, name, timestamp
   under, and the whole-column slide on a new arrival. */
.ah-wrap.ps-wrap { position: relative; }
.ps-feed-section {
  position: absolute; top: 0;
  left: calc(50% - 50vw + 18px);
  width: 122px; margin: 0; padding: 0;
}
.ps-feed-section .ah-sec-title-row { padding: 0 0 0 2px; margin-bottom: 8px; }
.ps-feed-section .ah-sec-title { font-size: 11px; letter-spacing: .04em; opacity: .75; }@media (max-width: 1240px) { .ps-feed-section { display: none; }
}

/* Measured in JS against the top-cards grid now (see fitRail in
   player-search-view.js), the same as the Auction House. The old fixed 80vh had
   no relationship to where the cards end, so the strip stopped early on some
   viewports and ran past the pager on others. */
.ps-feed-section .ah-feed-clip { height: 100%; min-height: 240px; }

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


/* The verdict sits BESIDE the name, not above it.
   
   A sale's headline is a price and deserves its own line; a verdict is an
   attribute of the person, and above the name it read as a value they had
   rather than a judgement about them. On one line the pair is a name and its
   badge, which is what it is.
   
   The chip is the site's own control shape — the same surface, border, radius
   and inset highlight as the buttons in the payment modal and the Minecraft
   link flow — so it reads as part of the family rather than a bare glyph. */
.vch-card .ah-feed-price { display: none; }
.ah-feed-chip .vch-ico { width: 11px; height: 11px; }
.vch-card-pos .ah-feed-chip { background: var(--green); color: #08130c; }
.vch-card-neg .ah-feed-chip { background: var(--red);   color: #1a0808; }
.ps-feed-empty { font-size: 11px; color: var(--text-dim); padding: 8px 4px; }
/* Round the skulls inside the otherwise-square .ah-img slots so player heads
   match the rounded faces used everywhere else (base tiles, cards, feed). */
.ps-round { border-radius: 6px; }

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

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

/* Vertically centred between the nav and the footer. The page is short, so
   main's min-height left all the slack underneath it. */
/* Centred in the space that ACTUALLY remains, not in a guess at it.
   `body` is already a flex column with `min-height: 100vh` and the footer on
   `margin-top: auto`, so `main` occupies exactly the gap between nav and footer.
   Filling that and centring inside it gives equal space above and below with no
   magic number — `100vh - 300px` was a guess at the chrome height, and being
   wrong by any amount pushes the content off-centre by that much. */
/* flex:1 is the part that was missing.
   `display:flex` alone made main a flex CONTAINER, but main was still only
   min-height:60vh — so the content centred inside 60vh, which begins directly
   under the nav and looks top-aligned. body is a flex column with the footer on
   margin-top:auto, so main has to GROW to claim the gap before centring in it
   means anything. */
main:has(> .ss-wrap) { display: flex; flex: 1 1 auto; }

.ss-wrap {
  flex: 1 1 auto;
  display: flex; flex-direction: column; justify-content: center;
  width: 100%;
}@supports not selector(:has(*)) {
  .ss-wrap { min-height: calc(100vh - 300px); }
}


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

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

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


/* The router adds .view-entering for one frame after swapping, then removes it.
   Nothing ever styled it, so the fade it was written for never existed and the
   swap was a hard cut. */
.view-entering { opacity: 0; }

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


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

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

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


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

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

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

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

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

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

/* ── "list a spawner" button, bottom-right under the picker ─────────────── */
.mk-picker-foot {
  display: flex; justify-content: flex-end;
  max-width: 488px; margin: 14px auto 0;
  padding-bottom: 16px; border-bottom: 1px solid var(--border-soft);
}

/* ── listing card as a real Minecraft item ─────────────────────────────── */
.mk-item .mk-item-row { display: flex; align-items: center; gap: 14px; }
.mk-item-icon {
  position: relative; flex-shrink: 0;
  width: 68px; height: 68px;
  background: var(--card-bg-hover);
  border: 1px solid var(--border-soft); border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
}
.mk-egg { width: 46px; height: 46px; image-rendering: pixelated; }
/* Minecraft stack count: bottom-right, hard drop shadow like the game. */
.mk-stack {
  position: absolute; right: 4px; bottom: 2px;
  font-size: 13px; font-weight: 700; color: #fff;
  text-shadow: 2px 2px 0 rgba(0,0,0,0.85);
  font-variant-numeric: tabular-nums; pointer-events: none;
}
/* Trust overlay: seller head + vouch %, top-left of the icon. */
.mk-trust {
  position: absolute; top: -8px; left: -8px;
  display: flex; align-items: center; gap: 3px;
  padding: 2px 6px 2px 2px; border-radius: 999px;
  background: var(--card-bg); border: 1.5px solid var(--border);
  cursor: pointer; transition: transform .16s, border-color .16s;
}
.mk-trust:hover { transform: scale(1.06); }
.mk-trust-av { width: 18px; height: 18px; border-radius: 50%; image-rendering: pixelated; }
.mk-trust-pct { font-size: 10.5px; font-weight: 800; font-variant-numeric: tabular-nums; }
.mk-trust-good { border-color: #4ade80; } .mk-trust-good .mk-trust-pct { color: #4ade80; }
.mk-trust-ok   { border-color: #efa227; } .mk-trust-ok .mk-trust-pct   { color: #efa227; }
.mk-trust-poor { border-color: #f0685e; } .mk-trust-poor .mk-trust-pct { color: #f0685e; }
.mk-trust-none { border-color: var(--border-bright); } .mk-trust-none .mk-trust-pct { color: var(--text-dim); }
.mk-item-main { flex: 1; min-width: 0; }
/* Price in money-green with the donut coin, like donutsmp.bet. */
.mk-price-coin { display: flex; align-items: center; gap: 6px; color: var(--green); font-size: 18px; font-weight: 800; }
.mk-coin { width: 18px; height: 18px; image-rendering: pixelated; flex-shrink: 0; }
.mk-item .mk-sub { margin-top: 3px; }
/* Discord message button. */
.mk-btn-dc { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; }
.mk-dc-ico { width: 14px; height: 14px; }

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



/* ── list-a-spawner modal ───────────────────────────────────────────────── */
.mk-backdrop {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(0,0,0,0.8);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  display: flex; align-items: center; justify-content: center; padding: 24px;
  animation: mk-fade .16s ease both;
}
.mk-modal {
  width: min(560px, 100%);
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 16px;
  box-shadow: 0 30px 70px rgba(0,0,0,0.65);
  display: flex; flex-direction: column; overflow: hidden;
}
/* Head and foot follow the recipe modal: the title centred with a quiet line
   under it, and the foot on its own slightly darker band so the commit button
   sits in a footer rather than floating at the bottom of the form. */
.mk-modal-head { position: relative; padding: 18px 18px 14px; border-bottom: 1px solid var(--border-soft); text-align: center; }
.mk-modal-head h2 { margin: 0; font-size: 14px; font-weight: 800; letter-spacing: .03em; text-transform: uppercase; color: var(--text); }
.mk-modal-sub { margin: 4px 0 0; font-size: 11px; color: var(--text-dim); }
.mk-x { position: absolute; top: 12px; right: 12px; width: 26px; height: 26px; padding: 5px; border: none; background: none; color: var(--text-dim); cursor: pointer; border-radius: 7px; }
.mk-x:hover { color: var(--text); background: rgba(255,255,255,0.06); }
.mk-modal-body { padding: 16px 18px; }
.mk-modal-foot {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 18px;
  border-top: 1px solid var(--border-soft);
  background: rgba(0,0,0,0.22);
}
/* The one place the warning belongs: next to the button that commits you. On
   the board it was wallpaper by the third visit. */
.mk-modal-note { flex: 1; font-size: 10.5px; color: var(--text-dim); line-height: 1.4; }
.mk-modal-note b { color: var(--text-mid); font-weight: 700; }

.mk-form { display: flex; flex-direction: column; gap: 12px; }
.mk-f { display: flex; flex-direction: column; gap: 5px; }
.mk-f-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
/* Spawner, quantity and price read as one sentence, so they share one row.
   The spawner name is the longest of the three and gets the extra width. */
.mk-f-row3 { display: grid; grid-template-columns: 1.2fr 0.8fr 1fr; gap: 10px; align-items: end; }
/* The site dropdown, sized to sit level with the inputs beside it. */
.mk-cd { width: 100%; }
.mk-cd .cd-btn { width: 100%; justify-content: space-between; padding: 9px 11px; font-size: 13px; }
.mk-cd .cd-list { width: 100%; }

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

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


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


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




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

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

.pro-head { text-align: center; padding: 26px 0 34px; }
.pro-title {
  font-family: var(--font-display);
  font-size: clamp(26px, 4.4vw, 36px); font-weight: 700;
  letter-spacing: -.025em; margin: 0 0 10px; color: var(--text);
}
.pro-sub {
  color: var(--text-mid); font-size: 14.5px; margin: 0 auto;
  max-width: 52ch; line-height: 1.6;
}

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

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

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

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

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

.pro-restore {
  display: block; margin: 4px auto 0;
  background: none; border: 0; color: var(--text-dim);
  font-family: var(--font); font-size: 12.5px; cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px;
}
.pro-restore:hover { color: var(--text-mid); }@media (max-width: 620px) {
  .pro-plans { grid-template-columns: 1fr; }
  .pro-perk { flex-direction: column; gap: 4px; }
  .pro-perk-name { flex: none; }
}
@media (min-width: 1241px) {
  .ah-feed-section,
  .ps-feed-section {
    top: 26px;
    bottom: 26px;
    height: auto !important;   /* overrides the inline height the views set */
  }
  .ah-feed-section .ah-feed-clip,
  .ps-feed-section .ah-feed-clip { height: 100%; }
}


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

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

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

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


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


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

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

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

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

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

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

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

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

/* ── the card ───────────────────────────────────────────────────────────
   Padding and plot height both follow the card's WIDTH, so the chart keeps
   its proportions as the card narrows. Tying the height to vh alone was what
   made the contents feel detached from the box around them. */
.ss-chart-card { padding: clamp(14px, 2.4vw, 24px); }
.ss-chart-card .ah-chart-body { height: clamp(220px, 32vw, 360px); }
.ss-chart-card .ah-svg { height: 100%; }@media (max-width: 620px) {
  .ss-chart-head-outer { grid-template-columns: 0 var(--ss-reserve) 1fr; }
  .ss-chart-head-outer .ss-chart-title { display: none; }
}@media (max-width: 560px) {
  .hh-stat { flex: 1 1 33%; padding: 0 6px; }
  .hh-div { display: none; }
}

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

body:has(.home-hero) .home-carousel {
  padding: clamp(16px, 3.5vh, 34px) 0 clamp(18px, 4vh, 40px);
  flex-shrink: 0;
}@media (max-height: 560px) {
  body:has(.home-hero) main { min-height: 0; }
}@media (prefers-reduced-motion: reduce) {
  .home-hero-title, .home-hero-sub, .home-hero-stats,
  .home-hero-stats .hh-stat { animation: none; }
}

html.is-embed, html.is-embed body {
  min-height: 0;
  background: transparent;   /* the frame's own card provides the surface */
}
html.is-embed main { padding-top: 14px; }
/* The side rails are positioned against the viewport, which inside a frame is
   the frame — they would sit on top of the content. */
html.is-embed .ah-feed-section,
html.is-embed .ps-feed-section { display: none !important; }


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

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

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

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

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


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

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

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

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



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/* Tools: the panel shows the GRID. The heading and the PRO footnote are chrome
   the showcase already narrates in its own label and lead, so they only take
   room from the thing worth seeing. */
.hs-stage.sc-tools .tl-head,
.hs-stage.sc-tools .tl-foot { display: none !important; }

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

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

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

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


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

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


/* Padding on the right column gives the tilt somewhere to begin — the lean
   starts as the pointer approaches the card rather than snapping on at its
   border, and it never reacts to the pointer being over the copy on the left. */
/* A wide catchment so the lean begins well before the pointer reaches the
   card — it should feel like the card noticing you approach, not a hover
   state switching on at the border. */
/* The tilt's catchment. Padding, then a matching negative margin, so the area
   that reacts to the pointer is far larger than the card while the layout is
   exactly as if it were not there.
   
   Widened again: the lean should begin as you approach, and starting it at
   arm's length is most of what makes the card feel like an object rather than
   a hover state. */
/* THE CATCHMENT IS A PSEUDO-ELEMENT, not padding and negative margin.
   
   `padding: 130px 150px; margin: -130px -150px` cancels out horizontally, but
   vertically the negative BOTTOM margin genuinely removes 130px from the
   section's height — so everything after it, the footer included, was pulled
   130px up the page. The hover area was correct and the layout paid for it.
   
   An absolutely positioned ::before is part of the element for hit-testing, so
   pointerenter/pointermove on .hs-right fire exactly as before, while the box
   contributes nothing to layout at all. Behind the content (z-index -1) so it
   can never intercept a click meant for the panel. */
.hs-right { position: relative; }
.hs-right::before {
  content: "";
  position: absolute;
  /* TOP and SIDES only — never below.
     
     An absolutely positioned box does not affect layout, but it DOES extend
     the scrollable area, and this one reached 130px past the bottom of the
     showcase. That is the empty band that appeared under the footer on the
     home page: nothing was rendered there, the page was simply scrollable into
     space the catchment had claimed.
     
     Losing the reach BELOW the panel costs almost nothing — the showcase is
     the last thing on the page, so approaching it from underneath means
     scrolling up into it, by which point the pointer is already over the panel
     itself. Above and to the sides is where people actually arrive from. */
  top: -130px;
  left: -150px;
  right: -150px;
  bottom: 0;
  z-index: -1;
  pointer-events: auto;
}


/* PRO: the member's own subscription panel. */
.pro-owned-line { color: var(--text-mid); line-height: 1.6; margin: 0; }
.pro-owned-actions { margin-top: 14px; }
/* Cancelling is a quiet, secondary action — it should be findable, not
   inviting. Muted until hovered, and only then does it turn red. */
.pro-cancel {
  background: none;
  border: 1px solid var(--border);
  border-radius: 9px;
  padding: 8px 14px;
  font-family: var(--font);
  font-size: 12.5px;
  font-weight: 700;
  color: var(--text-dim);
  cursor: pointer;
  transition: color .15s, border-color .15s, background .15s;
}
.pro-cancel:hover { color: #f87171; border-color: rgba(248,113,113,0.45); }
/* Armed: the second click confirms. Red now, because at this point it will
   actually do something. */
.pro-cancel.is-armed {
  color: #fff;
  background: #dc2626;
  border-color: #dc2626;
}
.pro-cancel:disabled { opacity: .6; cursor: default; }
/* ── the balance chart's loading state ──────────────────────────────────────
   
   The card used to stay `hidden` until history arrived, so the page had a gap
   and then something appeared in it. Nothing said "this is coming", which is
   why a two-second wait read as a broken page and a ten-second one read as
   nothing happening.
   
   Now the frame — title, legend, axis, range pills — is on screen immediately
   and the PLOT AREA carries a shimmer. The wait reads as work, and because the
   skeleton occupies exactly the space the chart will, nothing jumps when the
   real line lands.
   
   Scoped to .pl-chart-loading rather than the site-wide .is-loading, which is
   already used on <body> for the page transition — two states with one name is
   how one of them ends up cancelling the other. */
.pl-chart-loading .ah-svg,
.pl-chart-loading .ah-chart-body { position: relative; }

.pl-chart-loading .ah-chart-body::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 10px;
  pointer-events: none;
  /* A band travelling across a faint base. Deliberately low-contrast: this
     sits where data will be, and a bright sweep would read as content. */
  background:
    linear-gradient(90deg,
      rgba(255,255,255,0) 0%,
      rgba(255,255,255,0.055) 45%,
      rgba(255,255,255,0.055) 55%,
      rgba(255,255,255,0) 100%);
  background-size: 220% 100%;
  animation: plChartShimmer 1.25s ease-in-out infinite;
}

@keyframes plChartShimmer {
  0%   { background-position: 120% 0; }
  100% { background-position: -120% 0; }
}

/* The line itself fades in when it arrives, so the transition from skeleton to
   data is a settle rather than a pop. */
.pl-chart-wrap .ah-svg { transition: opacity .28s ease; }
.pl-chart-loading .ah-svg { opacity: .35; }

@media (prefers-reduced-motion: reduce) {
  .pl-chart-loading .ah-chart-body::after { animation: none; opacity: .5; }
  .pl-chart-wrap .ah-svg { transition: none; }
}

/* ── Tools page ─────────────────────────────────────────────────────────────
   
   A card per tool. The shape is the estate's — same surface, border, radius
   and inset highlight as the payment modal's method rows and the vouch cast
   bar — so this reads as part of the site rather than a page bolted on.
   
   Each card carries ONE accent, set inline as a token reference, and every
   coloured part derives from it. Adding a tool is choosing an icon and a
   token; there are no per-card colour rules to keep in step. */

.tl-wrap { max-width: 980px; margin: 40px auto 0; padding: 0 16px 64px; }

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

/* auto-fill, so the grid reflows on its own at every width: no breakpoint to
   maintain, and no width where the last card is stranded on a row it half
   fills. */
/* THREE FIXED COLUMNS, not auto-fill.
   
   The rows mean something here: two free tools then a PRO one, three times
   over. auto-fill would reflow that into whatever fits and the pattern — which
   is the argument for PRO — would dissolve at some window widths and reappear
   at others. The array order is the layout, so the grid has to hold it. */
.tl-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px;
}

.tl-card {
  display: flex;
  flex-direction: column;
  padding: 16px;
  border-radius: var(--radius-card, 14px);
  background: var(--card-bg);
  border: 1px solid var(--border);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: border-color .18s ease, transform .18s ease, box-shadow .18s ease;
}
.tl-card:hover {
  /* The accent appears on HOVER only — nine cards each shouting their own
     colour at rest is a fruit salad; on hover it means "this one".
     
     Mixed with TRANSPARENT rather than with --border, which is itself nearly
     see-through: mixing into a translucent colour averages the alpha down and
     the outline stayed faint however much colour went in. Same trap the
     crafting page's green hit. */
  border-color: color-mix(in srgb, var(--tl-c) 80%, transparent);
  background: color-mix(in srgb, var(--tl-c) 7%, var(--card-bg));
  transform: translateY(-2px);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.06),
              0 0 0 1px color-mix(in srgb, var(--tl-c) 30%, transparent),
              0 8px 22px rgba(0,0,0,0.34),
              0 0 20px color-mix(in srgb, var(--tl-c) 14%, transparent);
}
.tl-card:active { transform: translateY(0); }

/* NOT BUILT YET. Dimmed as a whole rather than greyed to nothing: the point of
   listing an unbuilt tool is that people read what it will do and want it, so
   the text has to stay legible. It is the CARD that recedes, not the words. */
.tl-card.is-soon { opacity: .72; cursor: default; }
.tl-card.is-soon:hover { transform: none; border-color: var(--border); box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); }

/* PLACEHOLDER SLOT (status:'new'). A deliberate empty tile in the grid, not a
   broken one: centred label, a dashed outline so it reads as "reserved" rather
   than "failed to load", and inert to hover and click. The accent tints the
   dash faintly so a row of them is not monochrome. */
.tl-card-new {
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 6px;
  cursor: default;
  background:
    repeating-linear-gradient(-45deg,
      color-mix(in srgb, var(--tl-c, var(--border)) 4%, transparent) 0 10px,
      transparent 10px 20px),
    var(--card-bg);
  border-style: dashed;
  border-color: color-mix(in srgb, var(--tl-c, var(--border)) 30%, var(--border));
  box-shadow: none;
  min-height: 132px;
}
.tl-card-new:hover { transform: none; box-shadow: none; }
.tl-new-mark {
  font-weight: 700;
  font-size: 13px;
  letter-spacing: .02em;
  color: color-mix(in srgb, var(--tl-c, var(--text)) 78%, var(--text));
}
.tl-new-soon {
  font-size: 10.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-dim);
  opacity: .7;
}

/* A refused click has to say so — silence reads as a broken card. Same
   reasoning as the locked PRO settings rows. */
@keyframes tlNudge {
  0%   { transform: translateX(0); }
  25%  { transform: translateX(-3px); }
  55%  { transform: translateX(3px); }
  100% { transform: translateX(0); }
}
.tl-card.is-nudged { animation: tlNudge .32s cubic-bezier(.4,0,.2,1); }

.tl-card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
.tl-flags { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }

.tl-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; flex: 0 0 auto;
  border-radius: 12px;
  background: color-mix(in srgb, var(--tl-c) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--tl-c) 34%, transparent);
}
/* SAME BOX FOR EVERY ICON, and bigger than it was.
   
   The textures are all 16x16, but how much of that square an item actually
   FILLS varies enormously — a stick is a thin diagonal, a nugget is a few
   pixels in the middle, a spawner fills the frame. Rendered at 22px the sparse
   ones were barely visible next to the dense ones and the row looked broken.
   
   34px in a 46px tile gives the thin items enough pixels to read while the
   dense ones still have breathing room, and object-fit: contain guarantees
   every icon occupies the identical box whatever its own proportions.
   
   pixelated because Minecraft art smoothed at this size turns to mush — the
   hard edges ARE the artwork. */
.tl-icon img {
  width: 34px; height: 34px;
  object-fit: contain;
  image-rendering: pixelated;
  display: block;
}

/* The PRO mark is the SAME gradient and the SAME 6s drift as the header pill,
   the chat badge and the settings page, and it shares --pro-delay — so every
   PRO surface on the screen is at the same point of the same cycle. A second
   PRO treatment that merely resembles the first is worse than none. */
.tl-pro {
  padding: 3px 8px; border-radius: 999px;
  font-family: var(--font-display, var(--font));
  font-size: 10.5px; 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);
}
.tl-pro-inline { display: inline-block; vertical-align: baseline; }

.tl-tag, .tl-soon {
  font-size: 10.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  padding: 3px 8px; border-radius: 999px;
}
.tl-tag {
  color: var(--tl-c);
  background: color-mix(in srgb, var(--tl-c) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--tl-c) 30%, transparent);
}
/* "Soon" is deliberately COLOURLESS. It is a status, not a feature, and giving
   it the card's accent would make an unbuilt tool look like the highlighted
   one on the row. */
.tl-soon {
  color: var(--text-dim);
  background: var(--surface-3, rgba(255,255,255,0.04));
  border: 1px solid var(--border);
}

.tl-name { font-size: 15.5px; font-weight: 700; color: var(--text); margin-bottom: 6px; }
.tl-desc { font-size: 12.5px; line-height: 1.55; color: var(--text-mid); }

.tl-foot { text-align: center; margin: 26px 0 0; font-size: 12.5px; color: var(--text-dim); }
.tl-foot-link { color: var(--text-mid); text-decoration: none; border-bottom: 1px solid var(--border); }
.tl-foot-link:hover { color: var(--text); border-bottom-color: var(--text-mid); }

/* THREE ACROSS AT EVERY SIZE.
   
   The grid is 3x3 on purpose — rows mean something, and collapsing to two
   columns and then one turned nine tools into a long scroll where the grouping
   was gone. It now SCALES instead: the cards get smaller, the shape stays.
   
   Only the padding and type shrink; the layout is the layout. */
/* TWO ACROSS on a narrow screen, not three.
   
   Three columns held the 3x3 shape but squeezed each card to about a third of a
   phone's width — the names wrapped to three lines and the icons shrank to
   nothing. Two gives each card room to stay readable, which matters more than
   preserving a grid nobody can read. */
/* Four across on desktop steps down to three on a tablet before dropping to
   two on a phone — four columns on a ~800px screen crushed every card the way
   three used to crush them on a phone. */
@media (max-width: 1100px) {
  .tl-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 900px) {
  .tl-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tl-card { padding: 12px; gap: 9px; }
  .tl-name { font-size: 12.5px; }
  .tl-desc { display: none; }        /* the name and icon carry it at this size */
  .tl-ico { width: 38px; height: 38px; }
  .tl-ico img { width: 26px; height: 26px; }
}
@media (max-width: 560px) {
  .tl-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
  .tl-card { padding: 11px 9px; gap: 8px; }
  .tl-name { font-size: 12px; line-height: 1.25; }
  .tl-ico { width: 34px; height: 34px; }
  .tl-ico img { width: 24px; height: 24px; }
  .tl-wrap { margin-top: 20px; }
  .tl-pro { font-size: 8px; padding: 1px 4px; }
}
@media (prefers-reduced-motion: reduce) {
  .tl-card { transition: border-color .18s ease; }
  .tl-card:hover { transform: none; }
  .tl-pro { animation: none; }
  .tl-card.is-nudged { animation: none; }
}

/* ── Big Price Changes ──────────────────────────────────────────────────────
   
   Built on the crafting calculator's shape: same hero, same panel, same
   sliders, same tiles, same pager. Only the parts that differ live here —
   the was/now pair, the percentage and the sales count.
   
   Two tools answering different questions should still feel like one site, and
   reusing the layout means a fix to either page fixes both. */

.mv-wrap { max-width: 700px; }

/* Two sliders side by side: direction and window. */
.mv-controls { gap: 10px; flex-wrap: wrap; }

.mv-row { grid-template-columns: 288px 84px auto 34px; }

/* The old price is dimmed and the new one carries the accent — the eye should
   land on what it costs NOW, with the previous figure as context. */
.cf-tile.is-was .ah-feed-card { opacity: .62; }
.cf-tile.is-was .ah-feed-price { color: var(--text-dim); }
.cf-tile.is-down .ah-feed-card {
  border-color: color-mix(in srgb, var(--red) 75%, transparent) !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.06),
              0 0 0 1px color-mix(in srgb, var(--red) 26%, transparent) !important;
}
.cf-tile.is-down .ah-feed-price { color: var(--red); }
.cf-tile.is-up .ah-feed-card {
  border-color: color-mix(in srgb, var(--green) 75%, transparent) !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.06),
              0 0 0 1px color-mix(in srgb, var(--green) 26%, transparent) !important;
}
.cf-tile.is-up .ah-feed-price { color: var(--green); }

.mv-pct {
  font-family: var(--font-mono); font-size: 15px; font-weight: 700;
  font-variant-numeric: tabular-nums; text-align: right;
  align-self: center; line-height: 1;
}
.mv-pct.is-down { color: var(--red); }
.mv-pct.is-up { color: var(--green); }

/* The volume is shown, not just filtered on — a reader who can see the trade
   behind a move can judge it where one told "it passed a threshold" cannot. */
.mv-sales {
  display: flex; flex-direction: column; align-items: flex-end; gap: 1px;
  font-family: var(--font-mono); font-size: 12px; color: var(--text-mid);
  align-self: center; white-space: nowrap;
}
.mv-sales em {
  font-style: normal; font-size: 9px; font-weight: 600;
  letter-spacing: .05em; text-transform: uppercase; color: var(--text-dim);
}

.mv-chart-btn svg { width: 18px; height: 18px; }
/* The chart drops out of the row grid and spans it, so it is not squeezed into
   one column's width. */
.mv-chart { grid-column: 1 / -1; padding: 0 6px 12px; }
.mv-chart-card { margin: 0; }
.mv-chart-loading { padding: 20px 0; text-align: center; font-size: 12.5px; color: var(--text-dim); }

.mv-empty {
  padding: 28px 18px; text-align: center;
  font-size: 13px; color: var(--text-mid);
}

@media (max-width: 760px) {
  .mv-row { grid-template-columns: minmax(0, 1fr) 70px 34px; }
  .mv-sales { display: none; }
}

/* ── Crafting Calculator ────────────────────────────────────────────────────
   
   A LIST, not a stack of cards. Twenty-five rows each in its own bordered box
   is twenty-five frames competing with the thing inside them; a plain row with
   a hairline under it lets the items themselves carry the page.
   
   Every row is the same sentence: these + these = that, for this much. It is
   the shape the decision already has in somebody's head, so the page does not
   have to be learned before it can be used. */

/* Sized to the ROWS, not the page. A 900px panel around a 558px row left a
   wide empty margin inside it on both sides — the panel is meant to be the
   list's edge, not a box the list floats in. */
.cf-wrap { max-width: 660px; margin: 40px auto 0; padding: 0 16px 64px; }

.cf-head { text-align: center; padding: 0 0 18px; }
.cf-h1 {
  font-family: var(--font-display);
  font-size: 28px; font-weight: 800; letter-spacing: -0.02em;
  margin: 0; color: var(--text);
}
.cf-sub { font-size: 14px; color: var(--text-mid); margin: 6px auto 0; max-width: 560px; line-height: 1.5; }
.cf-sub b { color: var(--text); font-weight: 600; }

.cf-controls { display: flex; justify-content: center; margin-bottom: 20px; }

/* ── the list ── */
/* A PANEL, like the leaderboard's.
   
   Rows separated only by hairlines on the page background read as floating —
   there was nothing saying where the list began or ended, so it looked like
   loose content rather than a thing. The same contained panel the leaderboard
   page uses gives it an edge to sit inside, and then the rows need no dividers
   of their own: alternating rows do the separating, which is quieter than a
   line and never collides with the tile borders. */
.cf-list {
  display: flex; flex-direction: column;
  /* IDENTICAL to .lbp-panel — same background, border, 18px corners and the
     same three-layer shadow. Two panels on one site that are nearly the same
     is worse than one that is exactly the same. */
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 1px 0 rgba(255,255,255,0.05) inset,
              0 4px 24px rgba(0,0,0,0.5),
              0 20px 60px rgba(0,0,0,0.35);
}

.cf-row {
  display: grid;
  grid-template-columns: 288px 18px 74px 96px 34px;
  justify-content: center;
  align-items: center; gap: 12px;
  padding: 10px 4px;
}
/* Zebra striping instead of dividers — quieter than a line, and it cannot
   collide with the tile borders the way a hairline did. */
.cf-row:nth-child(odd) { background: rgba(255,255,255,0.016); }

/* Grows leftward from the equals sign. wrap-reverse so a recipe with more
   tiles than fit spills onto a line ABOVE rather than below — the bottom row
   stays level with the '=' and the result, which is what keeps the alignment
   reading straight. */
.cf-in {
  display: flex; align-items: center; justify-content: flex-end;
  flex-wrap: wrap-reverse; gap: 6px; min-width: 0;
}
.cf-out-cell { display: inline-flex; }
.cf-row:hover { background: rgba(255,255,255,0.018); }

/* ── the equation ── */

/* The live sales card, at list size. Width is capped rather than fixed so a
   long item name ellipses instead of stretching the row. */
.cf-tile { width: 74px; flex: 0 0 auto; }
.cf-tile .ah-feed-card { gap: 2px; padding: 7px 5px 6px; cursor: default; }
.cf-tile .ah-feed-ico .cf-ico,
.cf-tile .cf-ico { width: 26px; height: 26px; object-fit: contain; image-rendering: pixelated; display: block; }
.cf-tile .ah-feed-price { font-size: 11px; }
.cf-tile .ah-feed-seller {
  font-size: 9px; max-width: 64px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The result wears the accent so the eye finds the end of the equation without
   reading it.
   
   MIXED WITH TRANSPARENT, NOT WITH --border-bright. That token is
   rgba(120,160,235,0.18) — almost entirely transparent — so mixing green INTO
   it averaged the alpha down to nearly nothing and the border stayed invisible
   however much green went in. Twice. Mixing with `transparent` sets the alpha
   directly and predictably instead. */
/* Only the RESULT is highlighted, and in green. It is the thing you are
   deciding about; the ingredients are the price of getting there. Same
   two-class specificity as the rule above, and later, so it wins. */
.cf-tile.is-out .ah-feed-card {
  border-color: color-mix(in srgb, var(--green) 85%, transparent) !important;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.06),
    0 0 0 1px color-mix(in srgb, var(--green) 30%, transparent),
    0 0 14px color-mix(in srgb, var(--green) 16%, transparent) !important;
}
.cf-tile.is-out .ah-feed-price { color: var(--green); }

.cf-op {
  font-family: var(--font-mono); font-size: 13px; color: var(--text-dim);
  flex: 0 0 auto; user-select: none;
}
.cf-op-eq { color: var(--text-mid); font-weight: 700; }
.cf-more { font-family: var(--font-mono); font-size: 11px; color: var(--text-dim); }

/* ── the number ── */
.cf-profit {
  text-align: right;
  font-family: var(--font-mono); font-size: 15px; font-weight: 700;
  color: var(--green); font-variant-numeric: tabular-nums;
  white-space: nowrap; line-height: 1.1;
}

/* ── the crafting table button ── */
/* The item itself is the button. A labelled control would need a word for
   something the picture already says to anybody who has opened a bench. */
.cf-craft {
  align-self: center;
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  background: transparent; border: 1px solid transparent; border-radius: 9px;
  cursor: pointer; opacity: .55;
  transition: opacity .16s ease, background .16s ease, border-color .16s ease;
}
.cf-craft img { width: 22px; height: 22px; image-rendering: pixelated; display: block; }
.cf-craft:hover {
  opacity: 1;
  background: var(--surface-3, rgba(255,255,255,0.05));
  border-color: var(--border);
}


/* The pager sits OUTSIDE the panel in the leaderboard's own .lbp-more wrapper,
   so both pages put their controls in the same place with the same buttons. */
.cf-pager { gap: 10px; }
/* Previous mirrors the arrow so it travels the way the button goes. */
.cf-prev svg { transition: transform .18s; }
.cf-prev:hover svg { transform: translateX(-3px); }

.cf-foot { text-align: center; margin: 18px auto 0; max-width: 600px; font-size: 12px; color: var(--text-dim); line-height: 1.6; }

@media (max-width: 720px) {
  .cf-row { grid-template-columns: minmax(0, 1fr) 14px auto 84px 30px; gap: 8px; padding: 8px 2px; }
  .cf-tile { width: 62px; }
  .cf-tile .ah-feed-seller { display: none; }
  .cf-profit { font-size: 13.5px; }
}

/* ── recipe modal ──────────────────────────────────────────────────────────
   The recipe in its real grid. Position is the half of a recipe a list cannot
   express, and it is the half somebody needs standing at the bench. */
.cr-backdrop {
  position: fixed; inset: 0; z-index: 9000;
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
  background: rgba(0,0,0,0.62);
  backdrop-filter: blur(3px);
  animation: crFade .16s ease;
}
@keyframes crFade { from { opacity: 0; } to { opacity: 1; } }

.cr-modal {
  position: relative;
  width: 100%; max-width: 400px;
  padding: 20px;
  background: var(--card-bg);
  border: 1px solid var(--border-bright);
  border-radius: 16px;
  box-shadow: 0 20px 60px rgba(0,0,0,0.55);
}
.cr-close {
  position: absolute; top: 10px; right: 12px;
  width: 28px; height: 28px; padding: 0;
  background: transparent; border: 0; border-radius: 8px;
  color: var(--text-dim); font-size: 20px; line-height: 1; cursor: pointer;
}
.cr-close:hover { color: var(--text); background: var(--surface-3, rgba(255,255,255,0.05)); }

.cr-head { text-align: center; margin-bottom: 16px; }
.cr-head-name { display: block; font-size: 15px; font-weight: 700; color: var(--text); }
.cr-head-method { display: block; margin-top: 3px; font-size: 11px; color: var(--text-dim); }


/* ── the crafting bench ────────────────────────────────────────────────────
   
   Drawn the way the GAME draws it, not the way the rest of the site draws
   things: the grey panel, the recessed slots with a dark top-left edge and a
   light bottom-right one, the arrow, the result slot on its own.
   
   Deliberately NOT themed to match the site. Somebody looking at this is about
   to stand in front of a real crafting table, and the closer the picture is to
   what they will see there, the less translation they have to do. This is the
   one place on the site where looking like Minecraft beats looking like us.
   
   Three by three ALWAYS, even for a 2x2 recipe — a bench has nine slots, and
   cropping to the used ones would move the ingredients somewhere they are not. */
.mc-bench {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  padding: 12px;
  border-radius: 8px;
  /* Minecraft's own panel grey, with the classic raised border: light on the
     top-left, dark on the bottom-right. */
  background: #c6c6c6;
  border-top: 3px solid #fefefe;
  border-left: 3px solid #fefefe;
  border-bottom: 3px solid #555;
  border-right: 3px solid #555;
}

.mc-grid { display: grid; grid-template-columns: repeat(3, 40px); gap: 3px; }

/* A slot is SUNKEN — dark on the top-left, light on the bottom-right, the
   exact inverse of the panel. That inversion is the whole trick; it is what
   makes a flat grey square read as a hole. */
.mc-slot {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px;
  background: #8b8b8b;
  border-top: 2px solid #373737;
  border-left: 2px solid #373737;
  border-bottom: 2px solid #fff;
  border-right: 2px solid #fff;
}
.mc-slot .cf-ico { width: 30px; height: 30px; object-fit: contain; image-rendering: pixelated; }
.mc-slot.is-result { width: 46px; height: 46px; }
.mc-slot.is-result .cf-ico { width: 34px; height: 34px; }

/* The stack count, exactly as the game renders it: white, hard-shadowed
   down-right, hugging the bottom-right of the slot. */
.mc-count {
  position: absolute; right: 1px; bottom: -1px;
  font-size: 13px; font-weight: 700; line-height: 1;
  color: #fff;
  text-shadow: 2px 2px 0 rgba(0,0,0,0.85);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

.mc-arrow { display: inline-flex; color: #555; }
.mc-arrow svg { width: 26px; height: 26px; }

.mc-out { display: inline-flex; }

/* Every recipe now draws in the nine slots — a shapeless recipe is still made
   on a bench, the game just does not mind which squares you use. An empty slot
   is drawn, not omitted: the gaps are part of the instruction. */


.cr-note { text-align: center; margin: 14px 0 0; font-size: 11.5px; color: var(--text-dim); }

.cr-list { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }
.cr-list-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  gap: 10px; align-items: center;
  padding: 4px 0; font-size: 12px;
}
.cr-list-name { color: var(--text-mid); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cr-list-meta { font-size: 10.5px; color: var(--text-dim); font-family: var(--font-mono); }
.cr-list-cost { font-family: var(--font-mono); color: var(--text); font-variant-numeric: tabular-nums; }
.cr-list-row.is-total { margin-top: 5px; padding-top: 8px; border-top: 1px solid var(--border); }
.cr-list-row.is-total .cr-list-name { color: var(--text); font-weight: 600; }
.cr-list-row.is-sell .cr-list-name { color: var(--text); font-weight: 600; }
.cr-list-row.is-sell .cr-list-cost { color: var(--green); font-weight: 700; }

@media (prefers-reduced-motion: reduce) { .cr-backdrop { animation: none; } }

/* The reason an empty crafting list is empty. A strict threshold and a quiet
   market look identical from outside, so the page says which. */
.cf-why { display: inline-block; margin-top: 8px; font-size: 12px; color: var(--text-dim); line-height: 1.5; }

/* ── dev recompute button (testing only) ────────────────────────────────────
   
   Only rendered when this browser has been given the internal secret, so a
   normal visitor never sees it. Deliberately looks like a debug control —
   dashed border, muted — because something that fires a heavy job should not
   look like an ordinary button somebody clicks idly.
   
   (Lost once already: a block rewrite of the crafting styles replaced
   everything between two markers and took this with it. Kept at the END of the
   file for that reason — appended blocks are not caught by a replace between
   earlier landmarks.) */
.dev-refresh {
  display: inline-flex; align-items: center; gap: 7px;
  height: 36px; padding: 0 13px; margin-left: 4px;
  background: transparent;
  border: 1px dashed var(--surface-4, rgba(255,255,255,0.18));
  border-radius: 10px;
  font-family: var(--font-mono); font-size: 11.5px; font-weight: 600;
  color: var(--text-dim); cursor: pointer;
  transition: color .16s ease, border-color .16s ease;
}
.dev-refresh:hover:not(:disabled) { color: var(--text); border-color: var(--text-dim); }
.dev-refresh:disabled { cursor: default; opacity: .75; }
.dev-refresh-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--gold); flex: 0 0 auto; }
.dev-refresh.is-busy .dev-refresh-dot { animation: devPulse 1s ease-in-out infinite; }
@keyframes devPulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) { .dev-refresh.is-busy .dev-refresh-dot { animation: none; } }

/* A 'ready' tool: no Soon badge, but the page is not built, so the card still
   does not navigate. Dimmed a touch less than a Soon card — it is nearly here
   rather than an idea — and it still nudges on click, because a card that does
   nothing at all reads as broken. */
.tl-card.is-soon { opacity: .82; }

/* NO transition on the list. The rows are already in memory, so a sort or page
   change is instant — a fade over instant content does not smooth anything, it
   delays it, and that delay is what read as lag. */

/* The tool pages use the site's own page heading (.search-tagline /
   .search-subtext, as on the homepage and players page). Only the spacing
   differs — there is no search box under it here. */
/* The same rhythm as the players page: the shared .search-shell owns the top
   padding and the tagline/subtext sizes, so the only thing set here is the gap
   down to the controls — there is no search box under this one to provide it. */
.cf-head.search-shell { margin: 0 auto; text-align: center; }
.cf-head .search-subtext { max-width: 620px; margin-left: auto; margin-right: auto; margin-bottom: 22px; }

/* The tiles ARE the row. A hover tint on top of cards that already have their
   own borders drew a second, larger rectangle around each line — the "weird
   square box". The hairline divider is all the separation a list needs. */
.cf-row:hover { background: rgba(255,255,255,0.035); }

/* The sort control is the site's own .ah-chart-range. Only the pill's motion
   needs declaring here — chart.js drives its own instances, this one is ours. */
.cf-sort { display: inline-flex; position: relative; }
.cf-sort .ah-range-slider { transition: transform .22s cubic-bezier(0.16,1,0.3,1), width .22s cubic-bezier(0.16,1,0.3,1); }
@media (prefers-reduced-motion: reduce) {
  .cf-sort .ah-range-slider { transition: none; }
}

/* The crafting tiles borrow .ah-feed-card, which brings the live feed's
   arrival glow with it — that rule flashes the FIRST card so your eye catches
   a new sale landing. Here nothing is arriving, so on a sort change the first
   tile lit up for no reason. Inheriting a class means inheriting its
   behaviour; this is the one piece that had to be switched off. */
/* ── KILL THE INHERITED ARRIVAL GLOW ────────────────────────────────────────
   
   .ah-feed-item:first-child .ah-feed-card runs `feedNew` with fill-mode BOTH,
   so its last keyframe sticks permanently — that is the blue outline, not a
   flash. And because each tile is the first child of its own wrapper, the first
   ingredient AND the result both got it.
   
   The selector has to MATCH that specificity to win: .cf-tile.ah-feed-item is
   two classes like .ah-feed-item:first-child, and site.css loads after
   shell.css, so the tie goes to this. A single .cf-tile lost silently. */
.cf-tile.ah-feed-item .ah-feed-card {
  animation: none;
  border-color: var(--border);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
}

/* The three numbers under the bench. Cost and sells-for are the pair the row
   deliberately drops — same profit can mean 4K → 271K or 4M → 4.27M, and this
   is the right place to say which, one click into a recipe already chosen. */
.cr-figures {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 8px; margin-top: 16px;
}
.cr-fig {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 9px 6px; border-radius: 10px;
  background: var(--surface-3, rgba(255,255,255,0.035));
  border: 1px solid var(--border);
}
.cr-fig-label {
  font-size: 9.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--text-dim);
}
.cr-fig-val { font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--text); }
/* Only the profit is coloured. Three green numbers would make the one that
   matters compete with the two that are merely context. */
.cr-fig.is-profit {
  background: color-mix(in srgb, var(--green) 12%, transparent);
  border-color: color-mix(in srgb, var(--green) 26%, transparent);
}
.cr-fig.is-profit .cr-fig-val { color: var(--green); font-weight: 700; }

/* The figure is picked out of the sentence the way the homepage subtitle picks
   out its nouns — the number is the point, the words are scaffolding. */
.cr-rate { text-align: center; margin: 10px 0 0; font-size: 11.5px; color: var(--text-dim); }
.cr-rate b { color: var(--text); font-weight: 600; }
.cr-fuel b { color: var(--text); font-weight: 600; }

/* ── the stonecutter ────────────────────────────────────────────────────────
   One input, one output — and the saw blade, because a stonecutter is
   recognised by its blade long before anybody reads a label. */
.mc-bench.is-cutter { gap: 16px; }
.mc-cutter-in { display: flex; flex-direction: column; align-items: center; gap: 5px; }
/* The blade: a pale disc with teeth cut into it, sitting under the input the
   way the real saw does. Drawn rather than an icon so it inherits the panel's
   own greys and cannot go missing. */
.mc-blade {
  width: 34px; height: 12px;
  background: repeating-linear-gradient(90deg, #d8d8d8 0 3px, #8b8b8b 3px 5px);
  border-top: 2px solid #fefefe;
  border-bottom: 2px solid #555;
  border-radius: 2px;
}

/* ── the furnace ────────────────────────────────────────────────────────────
   Input above, fuel below, flame between — the game's own arrangement. */
.mc-bench.is-furnace { gap: 16px; }
.mc-furnace-col { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.mc-flame { display: inline-flex; width: 16px; height: 16px; }
.mc-flame svg { width: 16px; height: 16px; fill: #e8a33d; }
/* The fuel slot is EMPTY and stays empty. Fuel is a real cost this page does
   not price — smelting profit is overstated by whatever the coal cost — and an
   empty, labelled slot says that better than a sentence nobody reads. */
.mc-fuel { opacity: .55; }

@media (prefers-reduced-motion: reduce) {
  .mc-flame svg { fill: #e8a33d; }
}

/* Minecraft's own flame, from the game's GUI atlas. Pixelated and scaled to
   match the slots — a smooth flame beside pixel-art item textures is the one
   thing on this panel that would look fake. */
.mc-flame img { width: 16px; height: 16px; image-rendering: pixelated; display: block; }
/* The fuel slot is filled now that fuel is actually priced. */
.mc-fuel { opacity: 1; }
.cr-fuel { text-align: center; margin: 12px 0 0; font-size: 11.5px; color: var(--text-dim); line-height: 1.5; }

/* Fuel gets a soft orange GLOW rather than an outline — it is part of the cost
   but not something you place in the grid, so it should read as adjacent to the
   equation rather than a step in it. Deliberately fainter than the result's
   green: the result is the answer, this is a detail. */
.cf-tile.is-fuel .ah-feed-card {
  border-color: color-mix(in srgb, #e8a33d 28%, var(--border));
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04),
              0 0 10px color-mix(in srgb, #e8a33d 12%, transparent);
}
.cf-tile.is-fuel .ah-feed-price { color: #e8a33d; }

/* ── Flood Zones ────────────────────────────────────────────────────────────
   Same panel and hero as the crafting calculator and the movers page. */

.fl-wrap { max-width: 720px; }

.fl-hero { align-items: center; text-align: center; padding: 26px 20px; }
/* The headline is the page. One number, big enough that it is read before
   anything else on the screen. */
.fl-big {
  font-family: var(--font-display); font-size: 46px; font-weight: 800;
  letter-spacing: -0.03em; line-height: 1; color: var(--blue-bright);
}
.fl-big-sub { margin-top: 8px; font-size: 14px; color: var(--text-mid); }
.fl-big-meta { margin-top: 5px; font-size: 11.5px; color: var(--text-dim); font-family: var(--font-mono); }

.fl-section { margin-top: 26px; }
.fl-h2 {
  font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-dim); margin-bottom: 10px; text-align: center;
}

.fl-regions { padding: 6px 14px; }
.fl-region {
  display: grid; grid-template-columns: 92px minmax(0, 1fr) 54px;
  align-items: center; gap: 12px; padding: 9px 0;
}
.fl-region + .fl-region { border-top: 1px solid var(--border); }
.fl-region-name { font-size: 13px; color: var(--text); }
/* The bar carries the comparison; the number is confirmation. Sorted worst to
   best, so the shape says "this varies a lot" before anything is read. */
.fl-bar { height: 8px; border-radius: 4px; background: var(--surface-3, rgba(255,255,255,0.05)); overflow: hidden; }
.fl-bar-fill {
  display: block; height: 100%; border-radius: 4px;
  background: linear-gradient(90deg, #2a7fd4, #37c2ff);
}
.fl-region-pct {
  font-family: var(--font-mono); font-size: 12.5px; color: var(--text);
  text-align: right; font-variant-numeric: tabular-nums;
}
.fl-note { text-align: center; margin: 12px 0 0; font-size: 12.5px; color: var(--text-dim); }
.fl-note b { color: var(--text); font-weight: 600; }

/* ONE padding value, every side, text included.
   
   The canvas used to centre itself inside a padded panel, so the gap left and
   right depended on the window width while top and bottom stayed fixed —
   visibly lopsided. A grid with the axes as their own tracks keeps the spacing
   identical all the way round, and the legend sits inside the same box rather
   than hanging off the bottom of it. */
.fl-mapwrap { padding: 12px; align-items: stretch; gap: 10px; }

/* SIZED TO FIT THE SCREEN, not the container.
   
   A square map in a 700px column is 700px tall, which on a laptop pushes the
   legend and half the map below the fold — you would have to scroll to see a
   picture whose whole point is being seen at once.
   
   Capping the WIDTH by viewport height keeps it square and fully visible on
   any screen: 62vh leaves room for the heading, the key and the legend. */
.fl-map-inner { max-width: min(100%, 62vh); margin: 0 auto; }

.fl-map-inner {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) 20px;
  gap: 6px;
  width: 100%;
}
.fl-map-box { position: relative; grid-column: 2; grid-row: 1; min-width: 0; }

/* The axes read like every other chart on the site: the extremes and the
   middle, in the same muted mono. */
.fl-axis { display: flex; font-family: var(--font-mono); font-size: 9.5px; color: var(--text-dim); }
.fl-axis-x {
  grid-column: 2; grid-row: 2;
  justify-content: space-between; align-items: flex-start;
}
.fl-axis-z {
  grid-column: 1; grid-row: 1;
  flex-direction: column; justify-content: space-between; align-items: flex-end;
  padding-right: 2px;
}
/* Pixelated on purpose: each pixel IS a 2,500-block cell, so smoothing would
   blur real boundaries into ones that do not exist. */
.fl-map {
  width: 100%; aspect-ratio: 1;
  image-rendering: pixelated;
  border-radius: 10px; border: 1px solid var(--border);
  display: block; cursor: crosshair;
}
.fl-tip {
  position: absolute; pointer-events: none; z-index: 5;
  display: flex; flex-direction: column; gap: 2px;
  padding: 7px 10px; border-radius: 8px;
  background: var(--page-bg); border: 1px solid var(--border-bright);
  box-shadow: 0 6px 20px rgba(0,0,0,0.5);
  font-size: 11px; color: var(--text-dim); white-space: nowrap;
}
.fl-tip b { font-size: 12.5px; color: var(--text); }

.fl-legend {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  font-size: 10.5px; color: var(--text-dim);
}

/* The region key. Colour says which region a cell belongs to; brightness says
   how busy it is — so the key explains the hue and the bar below explains the
   lightness. Two scales, two legends, neither doing the other's job. */
.fl-regions-key {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 6px 14px; font-size: 11px; color: var(--text-mid);
}
.fl-key { display: inline-flex; align-items: center; gap: 6px; }
.fl-key i { width: 10px; height: 10px; border-radius: 3px; display: block; }
.fl-legend-bar {
  width: 130px; height: 8px; border-radius: 4px;
  background: linear-gradient(90deg, rgb(22,30,46), rgb(52,160,236));
}

@media (max-width: 560px) {
  .fl-big { font-size: 38px; }
  .fl-region { grid-template-columns: 74px minmax(0, 1fr) 48px; gap: 9px; }
}

/* ── chart moments ──────────────────────────────────────────────────────────
   The dates that explain a line. Drawn under the data on purpose: a marker
   that obscures what it explains has the priority backwards. */
.ah-moment {
  stroke: var(--text-dim);
  stroke-width: 1;
  stroke-dasharray: 3 4;
  opacity: .55;
}
/* Where borrowed history hands over to our own. Different colour because it is
   a different KIND of fact — not something that happened on the server, but a
   change in who measured it. */
.ah-moment.is-source { stroke: var(--gold); opacity: .7; }
.ah-moment-label {
  font-family: var(--font-mono);
  font-size: 9px;
  fill: var(--text-dim);
  pointer-events: none;
}

/* The RTP legend runs dark to amber — a different scale from the flood map's
   blue, because it measures a different thing. Same picture, two questions. */
.fl-legend-bar.is-rtp { background: linear-gradient(90deg, rgb(24,27,34), rgb(240,152,54)); }

/* ── RTP map legends, in the corners ────────────────────────────────────────
   The map is square and the page is not, so the corners are space that exists
   anyway. Putting the legends there keeps them beside the thing they explain
   instead of below it, and the panel stays as tall as the map. */
/* OUTSIDE the map, not floating on it. Overlaying the legends put them on top
   of the data they describe and meant the map had to leave corners empty for
   them — a legend should sit beside the picture, not in it. */
.rtp-key {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 6px 14px;
  font-size: 11px; color: var(--text-mid);
}

/* Vertical, bottom right, reading DOWN from often to rarely — the same
   direction as the bar it labels. */
.rtp-scale {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  font-size: 10.5px; color: var(--text-dim);
}
.rtp-scale-bar {
  width: 130px; height: 8px; border-radius: 4px;
  background: linear-gradient(90deg, rgb(20,23,30), rgb(232,248,240));
}

/* The tooltip is positioned by transform now, so it must not also be pushed
   around by a margin. */
.fl-tip { transform-origin: top left; }

/* ── Recipe Book ────────────────────────────────────────────────────────────
   The calculator's shell — hero, slider, panel — with a search instead of a
   list, because this page answers a question you arrive with rather than one
   you browse. */

.rb-wrap { max-width: 720px; }

.rb-input {
  width: 100%; max-width: 420px; height: 42px; padding: 0 16px;
  background: var(--card-bg);
  border: 1px solid var(--border); border-radius: 12px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
  font-family: var(--font); font-size: 14px; color: var(--text);
  outline: none; transition: border-color .16s ease, box-shadow .16s ease;
}
.rb-input::placeholder { color: var(--text-dim); }
.rb-input:focus {
  border-color: color-mix(in srgb, var(--blue-bright) 60%, transparent);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.05),
              0 0 0 1px color-mix(in srgb, var(--blue-bright) 26%, transparent);
}

.rb-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 8px;
}
.rb-card {
  display: flex; align-items: center; gap: 11px;
  padding: 10px 12px; text-align: left;
  background: var(--card-bg);
  border: 1px solid var(--border); border-radius: 12px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
  cursor: pointer; transition: border-color .16s ease, background .16s ease;
}
.rb-card:hover {
  border-color: color-mix(in srgb, var(--blue-bright) 55%, transparent);
  background: color-mix(in srgb, var(--blue-bright) 6%, var(--card-bg));
}
.rb-card-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; flex: 0 0 auto;
  border-radius: 9px; background: var(--surface-3, rgba(255,255,255,0.04));
}
.rb-card-ico .cf-ico { width: 24px; height: 24px; }
.rb-card-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.rb-card-name {
  font-size: 13px; font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rb-card-method { font-size: 10.5px; color: var(--text-dim); }

.rb-controls { justify-content: center; }

.rb-pop-head {
  text-align: center; font-size: 11px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim);
  margin-bottom: 10px;
}

/* The same shape as the item page's list: rank, icon, name, count. "Most
   searched" is the same idea there, and a row of word-pills would have been a
   weaker version of something the site already does well. */
.rb-poplist { padding: 4px 0; }
.rb-poprow {
  display: grid; grid-template-columns: 26px 34px minmax(0, 1fr) auto;
  align-items: center; gap: 12px;
  width: 100%; padding: 9px 14px;
  background: transparent; border: 0; text-align: left; cursor: pointer;
  transition: background .14s ease;
}
.rb-poprow + .rb-poprow { border-top: 1px solid var(--border); }
.rb-poprow:hover { background: rgba(255,255,255,0.03); }
.rb-poprank {
  font-family: var(--font-mono); font-size: 11.5px; color: var(--text-dim);
  text-align: right;
}
.rb-poprow-name {
  font-size: 13.5px; color: var(--text); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rb-popn {
  display: flex; flex-direction: column; align-items: flex-end; gap: 1px;
  font-family: var(--font-mono); font-size: 12.5px; color: var(--blue-bright);
  white-space: nowrap;
}
.rb-popn em {
  font-style: normal; font-size: 9px; font-weight: 600;
  letter-spacing: .05em; text-transform: uppercase; color: var(--text-dim);
}

/* ── the brewing chain ──────────────────────────────────────────────────────
   Brewing is rarely one step: Long Fire Resistance is four brews deep. Each
   row is one brew, and the result of a row is the start of the next — so the
   route reads top to bottom the way you would actually do it. */
.br-chain { display: flex; flex-direction: column; gap: 8px; }
.br-step {
  display: flex; align-items: flex-start; justify-content: center; gap: 8px;
  padding: 9px 10px; border-radius: 10px;
  background: rgba(0,0,0,0.22);
  border: 1px solid var(--border);
}
.br-num {
  font-family: var(--font-mono); font-size: 10px; color: var(--text-dim);
  min-width: 12px; padding-top: 13px;
}
.br-bottle { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 66px; }
.br-bottle em {
  font-style: normal; font-size: 9px; color: var(--text-dim);
  text-align: center; line-height: 1.25;
}
.br-plus, .br-eq {
  font-family: var(--font-mono); font-size: 12px; color: var(--text-dim);
  padding-top: 13px;
}
.br-eq { color: var(--text-mid); font-weight: 700; }

/* A TAG slot: "any of these" rather than a specific item. It gets a word
   instead of an icon, because there is no single picture that is honest for a
   choice — showing one member would tell somebody to use that one. */
.mc-slot.is-tag { background: #7a7a7a; }
.mc-any {
  font-size: 9px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: #2c2c2c; text-shadow: 0 1px 0 rgba(255,255,255,0.35);
}

/* The brewing stand: reagent above, potion below, the drip between them. */
.mc-bench.is-stand { gap: 16px; }
.mc-stand-col { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.mc-stand-drip {
  width: 2px; height: 14px;
  background: linear-gradient(180deg, #d8a0e8, rgba(216,160,232,0.15));
}

/* What the dashed box on the RTP map means. The ring is the single most
   surprising fact on that page — most of the world is unreachable by /rtp —
   so it gets a sentence rather than being left to inference. */
.rtp-note {
  text-align: center; margin: 0; max-width: 460px;
  font-size: 11.5px; color: var(--text-dim); line-height: 1.55;
}
.rtp-note b { color: var(--text-mid); font-weight: 600; }

/* ── RTP map, tiled ─────────────────────────────────────────────────────────
   Region tiles rather than a pixel heatmap: the fine grain was noise, and the
   thing worth seeing is the shape of the regions and which are busy. */

.rtp-panel { padding: 16px; gap: 14px; align-items: stretch; }

/* The stage carries the axis letters, so the canvas can be exactly square and
   the labels do not eat into it. */
.rtp-stage {
  position: relative;
  padding: 18px;
  max-width: min(100%, 60vh);
  margin: 0 auto;
  width: 100%;
}
.rtp-map-box { position: relative; }
.rtp-canvas {
  width: 100%; aspect-ratio: 1; display: block;
  border-radius: 8px;
  border: 1px solid var(--border);
  cursor: crosshair;
}

/* +X / -X / +Z / -Z, the way the in-game maps label them. */
.rtp-ax {
  position: absolute;
  font-family: var(--font-mono); font-size: 10px; font-weight: 600;
  color: var(--text-dim); letter-spacing: .04em;
}
.rtp-ax-t { top: 2px; left: 50%; transform: translateX(-50%); }
.rtp-ax-b { bottom: 2px; left: 50%; transform: translateX(-50%); }
.rtp-ax-l { left: 0; top: 50%; transform: translateY(-50%); }
.rtp-ax-r { right: 0; top: 50%; transform: translateY(-50%); }

/* The key carries each region's landing count — "which colour is this" is a
   question the map already answers, "how busy is it" is not. */
.rtp-key .fl-key b {
  font-family: var(--font-mono); font-size: 10.5px;
  color: var(--text-dim); font-weight: 600; margin-left: 3px;
}

/* The type picker is the shared .cd dropdown; it only needs centring here. */
.rb-type-cd { margin: 0 auto; }

/* ── the recipe book's search, borrowed from the item page ──────────────────
   Same shell, same suggestion list — the control people already use to find an
   item, on a page asking the same question. */
/* NOT display:flex. An earlier version of this rule made it a flex container,
   and the search form inside is absolutely positioned — so as a flex item it
   collapsed to zero width and the whole bar rendered as a 1px vertical line.
   A block, so the slot inside can take its 460px and centre. */
.rb-search { display: block; margin: 0 auto 14px; }
.rb-ac-method {
  margin-left: auto; font-size: 10.5px; color: var(--text-dim);
  white-space: nowrap;
}

/* ── potions, tinted ────────────────────────────────────────────────────────
   The game draws a potion as a coloured liquid with the glass bottle over it.
   One grey bottle for all 45 is why a brewing chain read as three identical
   icons — the colour IS the information.
   
   A mask lets one texture be any colour, so this costs four images rather than
   forty-five. */
.pot { position: relative; display: inline-block; width: 26px; height: 26px; }
.pot-liq {
  position: absolute; inset: 0;
  background: var(--pc, #385dc6);
  -webkit-mask: url('/itemicons/potion_overlay.png') center/contain no-repeat;
  mask: url('/itemicons/potion_overlay.png') center/contain no-repeat;
}
.pot-glass {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  image-rendering: pixelated;
}

/* ── the brewing stand ──────────────────────────────────────────────────────
   
   Laid out the way the game lays it out: blaze powder top left, ingredient top
   centre, three bottles arcing along the bottom with the middle one LOWER, and
   the bubbles running between them.
   
   The first attempt put the flame beside the bottles and lifted the middle one,
   which is the arc upside down. It read as a diagram of a stand rather than a
   stand, which is exactly the difference worth caring about here.
   
   Three bottles because a stand brews three at once. Showing one would have
   somebody costing a batch wrong by a factor of three. */
.br-flow { display: flex; flex-direction: column; align-items: center; gap: 2px; }

.br-stand { position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px; }
.br-step-n {
  position: absolute; top: 5px; left: 7px; z-index: 2;
  font-family: var(--font-mono); font-size: 10px; color: #4a4a4a;
}

/* The stand is positioned from the REAL screen's coordinates, scaled 2x.
   Vanilla slots sit at fuel(17,17) ingredient(79,17) bottles(56,51)(79,58)
   (102,51), bubbles(60,44) arrow(97,16), so every offset below is
   (vanillaCoord - 10) * 2. Doing it this way rather than with rows and gaps is
   the difference between "looks like a brewing stand" and "is one" — the arc of
   the bottles and the big gap between the fuel and the ingredient are both
   things the eye notices immediately and neither survives a flexbox row. */
.mc-bench.is-stand {
  position: relative;
  width: 224px; height: 140px;
  padding: 0; gap: 0;
  display: block;
}
.br-stand .mc-slot {
  position: absolute;
  width: 32px; height: 32px;
}
.br-stand .mc-slot .cf-ico { width: 24px; height: 24px; }
.br-stand .mc-slot .pot { width: 24px; height: 24px; }

.br-fuel { left: 14px; top: 14px; }
/* Empty until somebody puts fuel in — as it is in game before you do. */
.mc-blaze-slot { opacity: .75; }
.br-ing  { left: 138px; top: 14px; }
/* Outer bottles high, middle low — the arc the stand actually seats them in. */
.br-b1 { left: 92px;  top: 82px; }
.br-b2 { left: 138px; top: 96px; }
.br-b3 { left: 184px; top: 82px; }

/* Bubbles rise on the LEFT, between the fuel and the bottles. Static: the stand
   is being explained, not simulated, and a looping animation on a reference
   page is a distraction that never stops. */
.br-bubbles {
  position: absolute; left: 100px; top: 68px;
  width: 24px; height: 40px;
  display: flex; flex-direction: column-reverse;
  align-items: center; justify-content: flex-start; gap: 4px;
}
.br-bubbles i {
  width: 4px; height: 4px; border-radius: 50%;
  background: #d8a0e8; opacity: .8; display: block;
}
.br-bubbles i:nth-child(2) { width: 6px; height: 6px; }
.br-bubbles i:nth-child(3) { opacity: .55; }
.br-bubbles i:nth-child(4) { opacity: .3; }

/* The brew-progress arrow, down the right of the ingredient slot. */
.br-arrow {
  position: absolute; left: 176px; top: 12px;
  width: 18px; height: 56px;
  color: #8f8f8f;
}
.br-arrow svg { width: 100%; height: 100%; fill: currentColor; }

.br-stand-cap { font-size: 10.5px; color: var(--text-dim); text-align: center; }

/* Between stands: the result of one is the input of the next. */
.br-down { display: inline-flex; color: var(--text-dim); padding: 2px 0; }
.br-down svg { width: 15px; height: 15px; }

/* ── mobile ─────────────────────────────────────────────────────────────── */
@media (max-width: 760px) {
  /* iOS zooms the page when a focused input's text is under 16px, and never
     zooms back. Every input on the site is therefore at least 16px on a phone —
     the alternative is disabling zoom entirely, which breaks it for people who
     need it. */
  input, select, textarea { font-size: 16px !important; }

  /* No scrollbars on a phone. A touch screen scrolls by dragging, so the track
     is a strip of chrome that only ever covers content. */
  * { scrollbar-width: none; -ms-overflow-style: none; }
  *::-webkit-scrollbar { width: 0; height: 0; display: none; }
}

/* The centred logo is a link home, so it should look like one. */
.mob-topbar-logo { cursor: pointer; }

/* The recipe book's tile grid: the item page's own .ah-tiles, but ten across
   and five deep rather than one clipped row — this page is a browse, not a
   teaser strip, so the extra rows should actually show. */
/* The tile icons must be .ah-img — that is the class .ah-tile-ico sizes. Using
   our own .cf-ico here left them unsized, which is why one arrow filled a
   whole cell. */
.rb-tilepanel { padding: 14px; }
.rb-tiles .ah-tile-ico .cf-ico,
.rb-tiles .ah-tile-ico img { width: 30px; height: 30px; image-rendering: pixelated; }
.rb-tiles .ah-tile-ico .pot { width: 30px; height: 30px; }
.rb-tiles {
  grid-template-columns: repeat(10, minmax(0, 1fr));
  grid-auto-rows: auto;          /* the item page clips to one row; here they all show */
  overflow: visible;
}
@media (max-width: 900px) { .rb-tiles { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
@media (max-width: 560px) { .rb-tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* ── crafting / brewing ─────────────────────────────────────────────────────
   Two buttons with the actual blocks on them. A dropdown hid one of only two
   choices behind a click, and the whole page is either one or the other, so
   the choice should be visible and one tap away. */
/* Under the search now: it filters what the box searches, so it belongs after
   it rather than reading as step one of a form. */
.rb-tabs { display: flex; justify-content: center; gap: 10px; margin: 0 0 18px; }
.rb-tab {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 9px 16px; border-radius: 12px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
  font-family: var(--font); font-size: 13.5px; font-weight: 600;
  color: var(--text-dim); cursor: pointer;
  transition: color .16s ease, border-color .16s ease, background .16s ease;
}
.rb-tab img {
  width: 22px; height: 22px; image-rendering: pixelated;
  opacity: .55; transition: opacity .16s ease;
}
.rb-tab:hover { color: var(--text-mid); border-color: var(--text-dim); }
.rb-tab:hover img { opacity: .8; }
/* The selected one is lit, not merely darker — with two buttons the difference
   has to be obvious at a glance or neither looks chosen. */
.rb-tab.is-on {
  color: var(--text);
  border-color: color-mix(in srgb, var(--blue-bright) 60%, transparent);
  background: color-mix(in srgb, var(--blue-bright) 10%, var(--card-bg));
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.06),
              0 0 0 1px color-mix(in srgb, var(--blue-bright) 22%, transparent);
}
.rb-tab.is-on img { opacity: 1; }

/* ── a tag slot, cycling ────────────────────────────────────────────────────
   "Any planks" is thirteen items. Showing one would be telling somebody to use
   that one; showing the word "any" tells them nothing about what qualifies.
   Cycling shows the actual answer, one member at a time.
   
   All slots share a single timer, so a two-member tag and a forty-eight-member
   one change on the same beat — separate timers drift, and a bench flickering
   at several speeds at once is worse than not animating at all. */
.mc-slot.is-cycle { position: relative; }
.mc-cyc {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity .28s ease;
}
.mc-cyc.on { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  /* No fade, and the first member simply stays — an animation nobody asked for
     is worse than a still picture. */
  .mc-cyc { transition: none; }
}

/* The recipe book's cards are the item page's, two across. */
.rb-cards { margin-top: 4px; }
@media (max-width: 620px) { .rb-cards { grid-template-columns: 1fr; } }

/* ── mobile: the pager belongs to the card ──────────────────────────────────
   On a narrow screen a button floating below a panel reads as unrelated to it.
   Pulling it flush and giving it the panel's own edges makes it the bottom of
   the card, which is where it was in the old layout and where it looks like it
   belongs. */
@media (max-width: 760px) {
  .lbp-more {
    margin-top: -1px;                /* sit ON the panel's border, not below it */
    padding: 10px 14px 12px;
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-top: 0;
    border-radius: 0 0 18px 18px;
    box-shadow: 0 4px 24px rgba(0,0,0,0.5);
  }
  /* The panel loses its own bottom corners so the two read as one card. */
  .lbp-panel, .cf-list { border-radius: 18px 18px 0 0; }
  .lbp-more-btn { width: 100%; justify-content: center; }
}

/* ── mobile: the account control, opening upward ────────────────────────────
   It lives at the FOOT of the drawer, so a menu dropping down would open into
   the bottom edge of the screen. Flipped to open upward, where the space is. */
.mob-drawer-foot #nav-auth-slot { width: 100%; }
.mob-drawer-foot .btn-discord-signin { width: 100%; justify-content: center; }

/* COLUMN-REVERSE, not repositioning.
   
   The menu is not a floating panel — it sits in normal flow inside the shell
   and grows it downward, which is why the shell has all the borders and the
   body has none. Absolutely positioning it "upward" would have detached it
   from the shell it is drawn as part of.
   
   Reversing the order puts the body above the head instead, so the shell still
   grows as one piece and simply grows the other way. */
.mob-drawer-foot .nav-wallet {
  width: 100%;
  display: flex;
  flex-direction: column-reverse;
}
.mob-drawer-foot .nav-wallet-head { width: 100%; justify-content: space-between; }
/* The chevron points the way the menu will actually go. */
.mob-drawer-foot .nav-wallet-chevron { transform: rotate(180deg); }
.mob-drawer-foot .nav-wallet.open .nav-wallet-chevron { transform: rotate(0deg); }

/* ── mobile: nothing may be wider than the screen ───────────────────────────
   The homepage sat off-centre on a phone because something inside it was wider
   than the viewport — the page then scrolls sideways and every centred element
   is centred on the WIDER box, not on the screen, so the whole thing looks
   shifted left.
   
   The carousel is the obvious candidate: it is deliberately full-bleed and its
   track is wider than the page by design. Clipping at the body means an
   overflowing child can no longer drag the layout sideways, whichever child it
   turns out to be. */
@media (max-width: 760px) {
  html, body { overflow-x: hidden; max-width: 100%; }
  /* The carousel keeps its own clipping; this just stops it escaping. */
  .home-carousel, .home-top { max-width: 100vw; }
  /* Anything centred with auto margins now centres on the real viewport. */
  .search-shell, .cf-wrap, .tl-wrap, .ah-wrap, .lbp-panel { max-width: 100%; }
}

/* ── mobile: the board picker belongs to the card ───────────────────────────
   
   On a narrow screen the rail became a scrolling strip floating above the
   panel, with its own gap — two separate objects where the old layout had one.
   
   Given the panel's own background and border and sat flush on top of it, the
   card reads as it used to: pick a board at the top, rows in the middle, View
   more at the bottom, all one thing. */
@media (max-width: 760px) {
  .lbp-layout { gap: 0; }
  .lbp-rail-wrap { order: -1; }
  .lbp-rail {
    margin-bottom: -1px;             /* sit ON the panel's border, not above it */
    padding: 10px 10px 11px;
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-bottom: 0;
    border-radius: 18px 18px 0 0;
    box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
  }
  /* The panel loses its top corners so the two meet as one card. It already
     loses its bottom corners to the pager rule above. */
  .lbp-panel { border-radius: 0; margin-top: 0; }
  .lbp-tab { flex: 0 0 auto; }
}

/* ============================================================================
   /sell — the server's /sell (worth) prices, browsable + per-item history.
   Built only on existing tokens; no new colours.
   ============================================================================ */
.sl-wrap { max-width: 860px; margin: 0 auto; padding: 28px 16px 60px; }

.sl-head { margin-bottom: 18px; }
.sl-h1 { font-size: 26px; font-weight: 800; letter-spacing: -0.01em; margin: 0 0 6px; color: var(--text); }
.sl-sub { font-size: 13.5px; color: var(--text-mid); margin: 0 0 16px; line-height: 1.5; }
.sl-sub code {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 12px; padding: 1px 5px; border-radius: 5px;
  background: rgba(0,0,0,0.28); color: var(--blue-bright);
}

/* Search box — the shared search shell's look, scoped so it needs no markup
   from search-bar.js. */
.sl-search { position: relative; }
.sl-search input {
  width: 100%; box-sizing: border-box;
  padding: 12px 14px; font-size: 14.5px;
  color: var(--text); background: var(--card-bg);
  border: 1px solid var(--border); border-radius: 12px;
  outline: none; transition: border-color .16s ease, box-shadow .16s ease;
}
.sl-search input::placeholder { color: var(--text-dim); }
.sl-search input:focus {
  border-color: var(--blue-border);
  box-shadow: 0 0 0 3px var(--blue-fill);
}

.sl-meta {
  display: flex; align-items: center; gap: 8px;
  font-size: 12px; color: var(--text-dim);
  margin: 0 2px 14px;
}
.sl-meta .sl-dot { opacity: .5; }

/* The list is a stack of rows: icon · name · price. */
.sl-list { display: flex; flex-direction: column; gap: 6px; }
.sl-row {
  display: grid;
  grid-template-columns: 34px 1fr auto;
  align-items: center; gap: 12px;
  padding: 9px 14px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 11px;
  text-decoration: none; color: inherit;
  transition: border-color .14s ease, background .14s ease, transform .14s ease;
}
.sl-row:hover {
  border-color: var(--blue-border);
  background: var(--card-bg-hover);
  transform: translateY(-1px);
}
.sl-row-ico { display: flex; align-items: center; justify-content: center; width: 34px; height: 34px; }
.sl-ico { width: 30px; height: 30px; image-rendering: pixelated; object-fit: contain; }
.sl-row-name { font-size: 13.5px; color: var(--text); font-weight: 600; }
.sl-row-price {
  font-size: 14px; font-weight: 700; color: var(--green);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.sl-row-price.is-none { color: var(--text-dim); font-weight: 500; font-size: 12.5px; }

.sl-more {
  text-align: center; font-size: 12px; color: var(--text-dim);
  padding: 14px 0 2px;
}

/* ── one item ── */
.sl-item { max-width: 720px; }
.sl-item-head { display: flex; align-items: center; gap: 16px; margin: 10px 0 20px; }
.sl-item-ico {
  display: flex; align-items: center; justify-content: center;
  width: 64px; height: 64px; flex: 0 0 64px;
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: 14px;
}
.sl-ico-lg { width: 44px; height: 44px; image-rendering: pixelated; object-fit: contain; }
.sl-item-title { min-width: 0; }
.sl-item-price { display: flex; align-items: baseline; gap: 10px; margin-top: 4px; }
.sl-big { font-size: 24px; font-weight: 800; color: var(--green); font-variant-numeric: tabular-nums; }
.sl-since { font-size: 12px; color: var(--text-dim); }
.sl-none { font-size: 14px; color: var(--text-dim); }

.sl-chart-card {
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: 16px; padding: 16px 14px; margin-bottom: 22px;
}
.sl-chart { min-height: 240px; }

/* ── change log ── */
.sl-changes-title { font-size: 15px; font-weight: 700; color: var(--text); margin: 0 0 12px; }
.sl-change {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 14px; margin-bottom: 6px;
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: 10px;
}
.sl-change-date { font-size: 12.5px; color: var(--text-mid); }
.sl-change-move {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 13.5px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.sl-change-move.is-up   { color: var(--green); }
.sl-change-move.is-down { color: var(--red); }
.sl-change-arrow { font-size: 10px; }
.sl-stable { font-size: 13px; color: var(--text-dim); padding: 4px 2px; }

@media (max-width: 560px) {
  .sl-row { grid-template-columns: 30px 1fr auto; gap: 9px; padding: 8px 11px; }
  .sl-ico { width: 26px; height: 26px; }
  .sl-row-name { font-size: 12.5px; }
  .sl-item-head { gap: 12px; }
  .sl-item-ico { width: 52px; height: 52px; flex-basis: 52px; }
  .sl-ico-lg { width: 36px; height: 36px; }
}

/* ── /compare — two players, one screen ──────────────────────────────────── */
.cmp-wrap { max-width: 1080px; margin: 0 auto; padding: 26px 18px 60px; }
.cmp-head { text-align: center; margin-bottom: 22px; }
.cmp-h1 { margin: 0; font-size: 26px; font-weight: 800; color: var(--text); }
.cmp-sub { margin: 6px 0 0; font-size: 13px; color: var(--text-dim); }

/* Left is always player A, right is always player B — the skins, the chart
   colours and every card's diagonal all agree on that, so the layout only has
   to be learned once. */
/* The two players sit on the SAME five-column rhythm as the stat grid below,
   in columns 2 and 4. That keeps them centred on the page while closing the
   canyon that 1fr-auto-1fr opened between them — the eye compares two things
   far more easily when they are near each other. */
.cmp-players {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 12px; align-items: start;
}
.cmp-side-a { grid-column: 2; }
.cmp-vs { grid-column: 3; }
.cmp-side-b { grid-column: 4; }
.cmp-side { display: flex; flex-direction: column; align-items: center; gap: 10px; }
/* Centred against the FULL height of the two columns. It used to be nudged
   down with a fixed padding, which only looked right at one window width. */
.cmp-players { align-items: stretch; }
.cmp-vs {
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 800; letter-spacing: .1em; color: var(--text-dim);
}

/* The magnifier lives INSIDE the field. As a button beside it, the control
   read as two things and the input sat off-centre under the skin above it. */
.cmp-search { position: relative; width: 100%; max-width: 240px; }
.cmp-search-ic {
  position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; color: var(--text-dim); pointer-events: none;
}
.cmp-search-in {
  width: 100%; box-sizing: border-box;
  padding: 8px 11px 8px 32px; border-radius: 9px;
  background: rgba(0,0,0,0.32);
  border: 1px solid var(--border); color: var(--text);
  font-family: var(--font); font-size: 13px; text-align: left;
}
.cmp-search-in:focus { border-color: var(--blue-border); outline: none; }
.cmp-search-in:focus + .cmp-search-ic, .cmp-search:focus-within .cmp-search-ic { color: var(--text-mid); }

/* Vouch pills are links here. */
.cmp-pill-link { cursor: pointer; transition: filter .16s, transform .16s; }
.cmp-pill-link:hover { filter: brightness(1.15); transform: translateY(-1px); }

/* The skin card is the PLAYER PAGE's .skin-box, reused wholesale — same size,
   border, spinner and fade. Only the empty state before a player is picked is
   local to this page. */
.cmp-skin-box { width: 150px; height: 190px; }
.cmp-skin-box:not(.skin-box) {
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: var(--radius-card);
  display: flex; align-items: center; justify-content: center;
}
.cmp-skin-empty { font-size: 12px; color: var(--text-dim); }
.cmp-name { font-size: 15px; font-weight: 700; color: var(--text); }
.cmp-vouches { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; }

.cmp-empty {
  margin: 26px auto; max-width: 420px; text-align: center;
  font-size: 13px; color: var(--text-dim);
}

/* One chart, two lines. Two charts with independent scales would make a 2x gap
   look like no gap at all. Uses the player page's chart shell, so only the
   spacing above it is set here. */
.cmp-chart-wrap { margin-top: 26px; }

/* Both lines SOLID and in their player's colour. The chart's second series is
   normally a dashed asking price, where dashing means "a different kind of
   number" — here the two series are the same kind of number for two different
   people, so dashing one would say something untrue about it. Colour alone
   carries the difference, matching each player's side of the page. */
.cmp-chart-wrap .ah-line { stroke: #4ea1ff; }
.cmp-chart-wrap .ah-line2 { stroke: #f0a63a; stroke-dasharray: none; opacity: 1; }
.cmp-chart-wrap .ah-pt { fill: #4ea1ff; }
/* The area fill under the primary line would tint the whole plot toward player
   A, which reads as A being the subject and B the comparison. Neither is. */
.cmp-chart-wrap .ah-grad-top, .cmp-chart-wrap .ah-grad-bot { stop-opacity: 0; }

/* ── the stat cards ──────────────────────────────────────────────────────
   The profile page's .card and .stat-head, kept as-is, with one addition: a
   hairline running BOTTOM-LEFT to TOP-RIGHT, A's value above it and B's below.
   That matches the side each player's skin is on.

   The direction is easy to get backwards. A linear-gradient's colour BAND runs
   perpendicular to its axis, so `to top right` draws a line from top-left to
   bottom-right — the opposite of what it reads like. `to bottom right` is what
   produces a bottom-left-to-top-right divider. */
.cmp-grid {
  margin-top: 26px;
  display: grid; grid-template-columns: repeat(5, 1fr);
  /* Roomier than the profile grid on purpose: each card here carries two
     numbers, a divider and a face, so the cards need more air between them to
     stop the whole grid reading as one busy field. */
  gap: 16px;
}
.cmp-card {
  position: relative; overflow: hidden;
  border: 1px solid var(--border-bright);
  padding: 12px 13px 13px;
}

.cmp-split { position: relative; height: 58px; margin-top: 2px; }

/* Same family as .stat-value on the profile — same size, weight and numeric
   font — so a figure reads as the same kind of thing on both pages. Colour is
   the STAT's colour for both players; the winner is separated by opacity, not
   by inventing a second colour language. */
/* Each value is boxed into its OWN half of the card, and the halves do not
   overlap the diagonal. Width is capped at 62% with the text aligned to its
   outer corner, so even the longest figure runs away from the line rather than
   across it — no shrinking or truncating the text to make it fit. */
.cmp-val {
  position: absolute; font-size: 17px; font-weight: 700;
  font-family: var(--font-num, var(--font));
  font-variant-numeric: tabular-nums; line-height: 1.1;
  color: var(--text);
  max-width: 62%;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  cursor: default;
  /* Above the losing-half overlay, which is painted after them in source
     order and would otherwise wash out the very numbers it is shading. */
  z-index: 2;
}
.cmp-val.blue { color: var(--blue); }
.cmp-val.green { color: var(--green); }
.cmp-val.red { color: var(--red); }
.cmp-val.gold { color: var(--gold); }
.cmp-val.purple { color: var(--purple); }
/* The loser is dimmed, and its half of the card is shaded.
   The shade spans the WHOLE card, not the value area. Sizing it to the value
   box and bleeding it outward drew a floating diagonal band that stopped
   partway down the card — it read as a rendering artefact rather than as one
   half being shaded. Anchored to the card, the diagonal lines up exactly with
   the divider and the two halves are unmistakable. */
.cmp-val.is-lose { opacity: .4; font-weight: 600; }
.cmp-val.is-win { opacity: 1; }

.cmp-card::before {
  content: ''; position: absolute; inset: 0;
  pointer-events: none; z-index: 0;
  background: rgba(0,0,0,0.28);
  opacity: 0;
}
/* clip-path, NOT a mask gradient.
   A gradient mask has to be told where 50% is, and on a non-square card that
   midpoint does not reliably land on the corner-to-corner diagonal the divider
   draws — so the shaded area and the line disagreed. Worse, where `mask` is
   unsupported the rule degrades to no mask at all and the WHOLE card darkens.
   A polygon names the three corners of the triangle outright: it cannot drift,
   and there is nothing to degrade.

   The divider runs bottom-left -> top-right, so the halves are the top-left
   triangle (player A's value) and the bottom-right one (player B's). */
.cmp-win-a::before {           /* A wins -> shade B's half, bottom-right */
  opacity: 1;
  clip-path: polygon(100% 0, 100% 100%, 0 100%);
}
.cmp-win-b::before {           /* B wins -> shade A's half, top-left */
  opacity: 1;
  clip-path: polygon(0 0, 100% 0, 0 100%);
}

/* Winner's face, top-right of the card — same spot on every card, so the grid
   can be scanned down that column alone to see who leads what. */
.cmp-face {
  position: absolute; top: 9px; right: 9px;
  width: 20px; height: 20px; border-radius: 5px;
  image-rendering: pixelated;
  box-shadow: 0 0 0 1.5px var(--card-bg), 0 2px 6px rgba(0,0,0,0.45);
  pointer-events: none; z-index: 2;
}

.cmp-val-a { top: 0; left: 2px; text-align: left; }
.cmp-val-b { bottom: 0; right: 2px; text-align: right; }

/* ONE divider, spanning the whole card corner to corner — the edge the tinted
   half used to draw, kept as the line itself. The fill is gone and so is the
   separate hairline inside .cmp-split: three ways of saying the same thing was
   two too many. Which player is ahead is carried entirely by the highlighted
   number now. */
.cmp-card::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to bottom right,
    transparent calc(50% - 0.5px), var(--border-bright) 50%, transparent calc(50% + 0.5px));
}

@media (max-width: 1100px) { .cmp-grid { grid-template-columns: repeat(4, 1fr); } }
@media (max-width: 900px) { .cmp-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 900px) {
  /* Three columns keeps the pair adjacent rather than stranding them at the
     edges once there is less room. */
  .cmp-players { grid-template-columns: 1fr auto 1fr; }
  .cmp-side-a { grid-column: 1; }
  .cmp-vs { grid-column: 2; }
  .cmp-side-b { grid-column: 3; }
}
@media (max-width: 620px) {
  .cmp-players { grid-template-columns: 1fr; }
  .cmp-side-a, .cmp-vs, .cmp-side-b { grid-column: 1; }
}

/* The Compare button sits in .player-info, directly under the name and just to
   the right of the 3D skin box — an action attached to THIS player. The vouch
   pills stay pushed to the far right, so the header reads as identity and
   action on the left, reputation on the right, with real space between them. */
.player-info .pl-compare-btn { margin-top: 10px; }
.pl-compare-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--blue-border);
  background: color-mix(in srgb, var(--blue) 12%, transparent);
  color: var(--blue-bright);
  font-family: var(--font); font-size: 11.5px; font-weight: 700;
  letter-spacing: .01em;
  cursor: pointer; text-decoration: none;
  transition: background .16s, border-color .16s, color .16s, transform .16s;
}
.pl-compare-btn:hover {
  background: color-mix(in srgb, var(--blue) 22%, transparent);
  border-color: var(--blue);
  color: #fff;
  transform: translateY(-1px);
}
.pl-compare-btn:active { transform: translateY(0); }
.pl-compare-btn svg { width: 13px; height: 13px; }

/* Names the gap in DonutSMP's own feed, next to the history it qualifies —
   quiet, because it is a caveat, not a warning. */
.pl-trades-note {
  margin-left: 10px; font-size: 10.5px; color: var(--text-dim);
  align-self: center; opacity: .85;
}

/* ── list-a-spawner modal bits ───────────────────────────────────────────── */
/* Our own quantity stepper. The native number spinner is a different widget in
   every browser and matches nothing else here. */
.mk-step { display: flex; align-items: stretch; gap: 0; }
.mk-step input {
  flex: 1; min-width: 0; width: 100%; box-sizing: border-box;
  text-align: center; border-radius: 0;
  border-left: none; border-right: none;
  font-variant-numeric: tabular-nums;
}
.mk-step-b {
  width: 32px; flex-shrink: 0;
  border: 1px solid var(--border);
  background: rgba(255,255,255,0.04); color: var(--text-mid);
  font-size: 15px; font-weight: 700; line-height: 1; cursor: pointer;
  transition: background .16s, color .16s;
}
.mk-step-b:first-child { border-radius: 9px 0 0 9px; }
.mk-step-b:last-child { border-radius: 0 9px 9px 0; }
.mk-step-b:hover { background: rgba(255,255,255,0.09); color: var(--text); }
/* Belt and braces: kill the native spinner if the field is ever a number type. */
.mk-step input::-webkit-outer-spin-button,
.mk-step input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.mk-step input[type=number] { -moz-appearance: textfield; }

/* The dropdown panel is TELEPORTED to <body> when it opens, so `.mk-cd .cd-list`
   would never match it — it is no longer inside the modal. cd-dropdown copies
   the trigger's marker class onto the panel instead, and that is what this
   targets. Solid surface + a real shadow so it reads as floating above the
   modal rather than blending into whatever is behind it. */
.cd-list.is-mk {
  background: #141b28;
  border: 1px solid var(--border-bright);
  box-shadow: 0 24px 60px rgba(0,0,0,0.6), 0 4px 14px rgba(0,0,0,0.4);
}

/* Per-unit suffix on a stacked listing. */
.mk-per {
  font-size: 11px; font-weight: 600; color: var(--text-dim);
  margin-left: 3px; letter-spacing: .02em;
}
/* When it was listed, tucked into the card's top-right. */
.mk-when {
  position: absolute; top: 10px; right: 12px;
  font-size: 10.5px; color: var(--text-dim); opacity: .8;
}
.mk-card { position: relative; }

/* Two-line tooltip: who on top, the exact figure under it, both centred. */
.hover-tip.is-rich { text-align: center; line-height: 1.35; }
.hover-tip .tip-who { display: block; font-weight: 800; color: var(--text); }
.hover-tip .tip-val {
  display: block; margin-top: 2px;
  font-variant-numeric: tabular-nums; color: var(--text-dim); font-size: 11px;
}

/* ── list-a-spawner modal: the donutdiscords palette ─────────────────────
   Matching the Pick-your-server / List-your-server modals, which are the
   reference for how a modal should look across the network.

   The rules that make it read right, learned there:
     • Only ONE end is darkened. Tinting the header AND the footer letterboxes
       the content between two bars — the header stays on the modal surface and
       the footer takes the darker band.
     • Fields are LIGHTER than the modal and blue-tinted, not black. A
       near-black input on a dark navy card reads as a hole punched in the
       surface; a lifted, faintly blue field reads as something to type in. */
.mk-modal { background: var(--card-bg); }
.mk-modal-head { background: transparent; }
.mk-modal-body { background: transparent; }
.mk-modal-foot { background: rgba(0,0,0,0.28); }

.mk-form input,
.mk-form textarea,
.mk-step input,
.mk-cd .cd-btn {
  background: rgba(120,160,235,0.08);
  border: 1px solid rgba(120,160,235,0.16);
  border-radius: 10px;
  color: var(--text);
  padding: 11px 13px;
  font-family: var(--font); font-size: 13.5px;
}
.mk-form input::placeholder,
.mk-form textarea::placeholder { color: rgba(160,175,205,0.55); }
.mk-form input:focus,
.mk-form textarea:focus,
.mk-cd .cd-btn:focus {
  outline: none;
  border-color: rgba(120,160,235,0.4);
  background: rgba(120,160,235,0.12);
}
/* The stepper reuses the same field surface so the row reads as one control. */
.mk-step input { border-left: none; border-right: none; border-radius: 0; }
.mk-step-b {
  background: rgba(120,160,235,0.08);
  border: 1px solid rgba(120,160,235,0.16);
  color: var(--text-mid);
}
.mk-step-b:hover { background: rgba(120,160,235,0.16); color: var(--text); }
.mk-f-l { color: var(--text-mid); font-size: 11.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
.cd-list.is-mk { background: #1b2236; border-color: rgba(120,160,235,0.18); }

/* The reason under an empty state — quiet, but present, so a blank page can
   always say which kind of blank it is. */
.fl-why { margin-top: 6px; font-size: 11.5px; color: var(--text-dim); line-height: 1.5; }

/* Sits inline with the section title, not out by the pager — a caveat belongs
   next to the thing it qualifies. */
.pl-trades-title .pl-trades-note {
  margin-left: 8px; font-size: 10.5px; font-weight: 500;
  color: var(--text-dim); opacity: .85; letter-spacing: 0;
  text-transform: none; vertical-align: middle;
}
