/* Palworld Market — light tech theme.
 *
 * Cues taken from the game's own inventory screen: a bright outdoor scene
 * showing through translucent panels, cyan as the single accent, thin vertical
 * bars beside section labels, tick marks at panel corners, and warm gold
 * reserved strictly for currency.
 *
 * The sky is drawn in CSS rather than fetched. A photograph would cost a
 * request, date badly, and fight the panels for attention; layered gradients
 * stay out of the way and cost nothing.
 *
 * Standalone rather than sharing LRT's 49KB stylesheet: different site,
 * different job, and coupling them means every change there risks breaking
 * here.
 */

:root {
  --sky-0:    #2b8fd6;      /* deep blue overhead */
  --sky-1:    #6fbde9;
  --sky-2:    #b5e0f5;
  --sky-3:    #e4f4fb;      /* haze at the horizon */
  --grass-1:  #86b94b;      /* sunlit tips */
  --grass-2:  #5d9433;
  --grass-3:  #47762a;      /* shaded base */

  --panel:    rgba(255,255,255,.80);
  --panel-2:  rgba(255,255,255,.42);
  --line:     rgba(23,111,138,.22);
  --line-2:   rgba(23,111,138,.11);

  --text:     #16333f;
  --muted:    #5b7c8d;
  --accent:   #0e93b3;      /* the game's UI cyan, darkened for light ground */
  --accent-d: #0a7692;
  --accent-l: #35c3e2;
  --gold:     #b5820f;
  --warn:     #b9760d;
  --bad:      #c4444d;
  --good:     #2f8a52;
  --radius:   10px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  color: var(--text);
  font: 15px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  min-height: 100vh;
  background: var(--grass-3);
}

/* The scene, in two fixed layers: sky above, grass below.
 *
 * Built from gradients rather than a photograph. A real landscape at the size
 * this needs is a megabyte the page does not otherwise need, it dates the
 * moment the game's art does, and at low opacity behind panels almost none of
 * its detail survives anyway. Layered gradients read as sky-and-field at a
 * glance and cost nothing.
 *
 * Fixed pseudo-elements rather than `background-attachment: fixed` on body —
 * iOS Safari has never handled that property properly and either drops the
 * image or jitters it while scrolling. This gives the same "panels float over
 * a landscape" effect everywhere, and keeps the horizon where the eye expects
 * it however far down the page you are. */
body::before {
  content: "";
  position: fixed; inset: 0; z-index: -2; pointer-events: none;
  background:
    /* cloud banks, largest and softest first */
    radial-gradient(620px 190px at 18% 14%, rgba(255,255,255,.92), transparent 66%),
    radial-gradient(420px 130px at 30% 21%, rgba(255,255,255,.80), transparent 68%),
    radial-gradient(760px 210px at 86% 9%,  rgba(255,255,255,.70), transparent 66%),
    radial-gradient(340px 110px at 62% 27%, rgba(255,255,255,.55), transparent 70%),
    /* haze thickening toward the horizon, as in the key art */
    linear-gradient(180deg, transparent 44%, rgba(255,255,255,.55) 68%, rgba(255,255,255,.25) 76%),
    linear-gradient(180deg,
      var(--sky-0) 0%, var(--sky-1) 26%, var(--sky-2) 52%, var(--sky-3) 72%);
}

/* Grass. Its own layer so the horizon can be a soft blend rather than a line,
   and so the blades sit ONLY over the field instead of tinting the whole sky. */
body::after {
  content: "";
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background:
    /* a suggestion of blades — steep, thin, and barely there */
    repeating-linear-gradient(96deg,
      rgba(255,255,255,.10) 0 1px, transparent 1px 9px),
    linear-gradient(180deg,
      transparent 70%,
      rgba(134,185,75,.35) 74%,
      var(--grass-1) 80%,
      var(--grass-2) 90%,
      var(--grass-3) 100%);
  /* Clip the blade texture to the field: without this it would streak the sky. */
  -webkit-mask-image: linear-gradient(180deg, transparent 72%, #000 82%);
          mask-image: linear-gradient(180deg, transparent 72%, #000 82%);
}

/* The trade screen IS the page, and it is three columns of dense list — so it
   should have the monitor, not a 1,480px column with a field of grass either
   side of it. On a 2,560px display the old cap left each list about 430px
   wide, which truncated half the item names for no reason at all.
   Everything else that uses .wrap is a header or a footer and reads fine at
   any width; the footer paragraph caps its own measure so the prose does not
   stretch to 200 characters a line.

   2100, not 2400. The first pass at this filled a 2,560px monitor edge to edge
   and read as overwhelming rather than roomy — a list only needs to be wide
   enough that nothing truncates, and past that the extra width is just further
   for the eye to travel between an item and its price. This takes about 15%
   off each side column, which lands them near 820px: still no truncation, and
   the whole trade fits in one comfortable span. */
.wrap { max-width: 2100px; margin: 0 auto; padding: 0 16px; }
/* The footer sits on the grass, and muted blue-grey text on saturated green
   is unreadable — the blade texture runs straight through the letterforms. It
   gets the same translucent panel every other block of text on this page has,
   rather than being made white and left to fight the stripes. */
.foot { padding-bottom: 26px; }
.foot p {
  max-width: 78ch;
  background: rgba(255,255,255,.80);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  border: 1px solid var(--line); border-radius: 8px;
  padding: 11px 15px; color: var(--text); font-size: 13px;
  box-shadow: 0 1px 2px rgba(16,60,80,.08);
}

/* ---------------------------------------------------------------- header */
/* The key art is the same scene the page background imitates, so it is left
   vivid at the top and dissolved into the drawn sky at the bottom rather than
   being boxed off with a border. The join should be invisible. */
.hero {
  position: relative;
  margin-bottom: 24px;
  background:
    linear-gradient(180deg,
      rgba(43,143,214,.10) 0%,
      rgba(120,190,232,.30) 55%,
      rgba(181,224,245,.80) 86%,
      var(--sky-2) 100%),
    url("https://cdn.cloudflare.steamstatic.com/steam/apps/1623730/library_hero.jpg") center 22% / cover no-repeat;
}
/* Tall enough that the art reads as a scene rather than a stripe, but still
   short enough that the Bank card is visible without scrolling on a laptop. */
.hero-inner {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 16px; padding: 118px 0 22px; flex-wrap: wrap;
}
@media (max-width: 780px) { .hero-inner { padding-top: 64px; } }
.brand { display: flex; align-items: flex-end; gap: 14px; }
/* Capped in BOTH directions: Steam's logo.png varies wildly in aspect ratio
   between games, and an uncapped height would push the header off the page. */
.brand img {
  width: 176px; max-width: 44vw; height: auto; max-height: 86px;
  object-fit: contain; object-position: left bottom; display: block;
  filter: drop-shadow(0 2px 6px rgba(255,255,255,.9));
}
.brand .wordmark {
  margin: 0; font-size: 32px; letter-spacing: -.5px; color: #fff;
  text-shadow: 0 2px 10px rgba(8,42,64,.65);
}
/* Header text sits on photography, so it carries its own contrast rather than
   relying on whatever pixel happens to be behind it. */
.brand .sub {
  font-size: 13px; color: rgba(255,255,255,.88);
  border-left: 2px solid var(--accent-l); padding-left: 12px;
  text-shadow: 0 1px 6px rgba(8,42,64,.8);
}
.brand .sub strong { display: block; color: #fff; font-size: 15px; }

.who {
  color: rgba(255,255,255,.88); font-size: 14px; text-align: right;
  text-shadow: 0 1px 6px rgba(8,42,64,.8);
}
.who strong { color: #fff; }

/* Section label with the game's left-edge accent bar. */
h2 {
  font-size: 12px; margin: 0 0 14px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--muted);
  border-left: 3px solid var(--accent); padding-left: 9px; line-height: 1.2;
}

/* ----------------------------------------------------------------- cards */
.card {
  position: relative;
  background: var(--panel);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 18px;
  margin-bottom: 18px;
  box-shadow: 0 1px 2px rgba(16,60,80,.06), 0 10px 26px rgba(16,60,80,.07);
}
/* Corner ticks, as on the game's panels. Purely decorative, so hidden from
   assistive tech by being pseudo-elements with no content of substance. */
.card::before, .card::after {
  content: ""; position: absolute; width: 11px; height: 11px; pointer-events: none;
}
.card::before {
  top: 6px; left: 6px;
  border-top: 2px solid var(--accent-l); border-left: 2px solid var(--accent-l);
}
.card::after {
  bottom: 6px; right: 6px;
  border-bottom: 2px solid var(--accent-l); border-right: 2px solid var(--accent-l);
}

.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 18px; }
@media (max-width: 780px) { .cards { grid-template-columns: 1fr; } }

.stat { margin-bottom: 0; }
.stat .label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .9px; }
.stat .value { font-size: 30px; font-weight: 700; margin-top: 4px; letter-spacing: -.5px; }
.stat.coin .value { color: var(--gold); }
.stat .sub { color: var(--muted); font-size: 12px; margin-top: 8px; }

.hint  { color: var(--muted); font-size: 13px; margin: -6px 0 14px; }
.muted { color: var(--muted); }
.hidden { display: none; }

/* ------------------------------------------------------- gold transfer */

/* Two purses, side by side, deliberately identical in weight. The same coin
   lives in two places and the old card made one of them look like a footnote:
   the bank in 30px type, "2,070 coins on you in game" in 12px grey underneath.
   Reading it, you would not guess both numbers were yours. */
.purses {
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
}
.purse {
  background: var(--panel-2); border: 1px solid var(--line-2);
  border-radius: 8px; padding: 11px 12px;
  display: flex; flex-direction: column; gap: 8px;
}
.purse .label {
  color: var(--muted); font-size: 10px; text-transform: uppercase;
  letter-spacing: .9px; line-height: 1.3;
}
.purse-v { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* The game's own Gold Coin art, at the size of the number beside it. A glyph
   or an emoji would be a different currency drawn from a different world. */
.coin-ico {
  width: 26px; height: 26px; flex: 0 0 26px; object-fit: contain;
  filter: drop-shadow(0 1px 1px rgba(16,60,80,.25));
}
.purse .value {
  font-size: 24px; font-weight: 700; letter-spacing: -.4px; color: var(--gold);
  font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis;
}
.btn.wide { width: 100%; text-align: center; }
@media (max-width: 420px) { .purses { grid-template-columns: 1fr; } }

/* The exact-amount row is the rarer move now, so both its buttons are ghosts —
   the two primary actions are the ones above it. */
.xfer { display: flex; gap: 7px; margin-top: 11px; flex-wrap: wrap; }
.xfer .qty { flex: 1 1 70px; width: auto; min-width: 0; }
.xfer .btn { flex: 0 0 auto; }

/* Small enough to sit inside a history row's meta line without setting the
   row height. */
.btn.tiny {
  padding: 2px 8px; font-size: 10.5px; font-weight: 700;
  letter-spacing: .3px; text-transform: uppercase; border-radius: 5px;
}

/* ----------------------------------------------------------- trade screen
 *
 * Bag on the left, market on the right, till in the middle — a shop counter,
 * and the reason the tabs are gone. Tabs made you choose which half of a trade
 * to look at, which is exactly the comparison you are trying to make.
 *
 * The two outer columns are `1fr` and the middle is fixed. The lists are the
 * part that benefits from width; the till holds three cards of known size and
 * letting it stretch just spreads a short label across a long line.
 *
 * `align-items: start` matters — without it the grid stretches all three to
 * the tallest, and the till's three cards float in the middle of an 800px
 * column of nothing. */
.trade {
  display: grid;
  /* `minmax(0, 1fr)`, not `1fr`. A grid track's default minimum is min-content,
     and a table of 600 rows has a min-content width of whatever its widest
     untruncatable cell needs — so plain `1fr` let the two lists push the grid
     to 1,944px inside a 1,480px page and the whole thing scrolled sideways.
     Zero as the minimum lets the columns actually take the width they are
     given, which is what makes the ellipsis on the names do its job. */
  /* The till grows with the page but never runs away with it: below about
     1,650px it is a flat 330, and past that it takes a fifth of the width up
     to 430. Fixed at 330 on a wide monitor it looked like a slot between two
     halls; as a full third it would have wasted the space the lists want. */
  grid-template-columns: minmax(0, 1fr) clamp(330px, 20%, 430px) minmax(0, 1fr);
  gap: 16px;
  align-items: start;
}
.trade > .panel { margin-bottom: 0; min-width: 0; }
.mid { display: flex; flex-direction: column; gap: 16px; }
.mid > .card { margin-bottom: 0; }

/* Below this the three columns become one, in reading order: what you have,
   then the till, then the shop. The middle going second is deliberate — on a
   phone the bank and the selected item belong between the two lists, not
   pushed to the bottom where you would never scroll to them. */
@media (max-width: 1180px) {
  .trade { grid-template-columns: 1fr; }
}

.panel-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-bottom: 12px;
}
.panel-head h2 { margin: 0; }
.panel-sub { color: var(--muted); font-size: 12px; }

/* The lists are the tall part of the page and the only part worth scrolling
   independently. Capping them keeps the till in view while you work down a
   column, which is the whole point of it being beside the lists. */
.panel > div[id$="Wrap"] { max-height: 68vh; overflow-y: auto; overflow-x: hidden; }
.hist  #histWrap        { max-height: 30vh; overflow-y: auto; overflow-x: hidden; }
/* Fixed layout, with every column but the name stating its width in the head
   definitions. Auto layout sizes columns to their content, which in a column
   half the page wide meant the longest item name decided where the Buy button
   went — and it went off the edge. Fixed layout plus a <colgroup> makes the
   name column the one that gives, which is what the ellipsis on it is for. */
.panel table, .hist table { width: 100%; table-layout: fixed; }

/* --------------------------------------------------- the selected item */
.detail { padding: 15px 16px; }
.detail.empty { color: var(--muted); }
.detail.empty p { margin: 0; font-size: 13px; }

.detail-head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.detail-ico {
  width: 56px; height: 56px; flex: 0 0 56px; object-fit: contain;
  filter: drop-shadow(0 1px 2px rgba(16,60,80,.3));
}
.detail-title { min-width: 0; }
.detail-name { font-weight: 700; font-size: 15px; line-height: 1.25; }
.detail-sub {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  color: var(--muted); font-size: 11.5px; margin-top: 3px;
}
.detail-sub .grade { margin: 0; }

.detail-stats { display: flex; flex-direction: column; gap: 1px; }
.stat-line {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  padding: 6px 0; border-bottom: 1px solid var(--line-2);
}
.stat-line:last-child { border-bottom: 0; }
.stat-k {
  color: var(--muted); font-size: 10px; text-transform: uppercase;
  letter-spacing: .9px;
}
.stat-v { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat-v.gold { color: var(--gold); }
.stat-v.warn { color: var(--warn); font-size: 13px; }

.detail-key {
  margin-top: 10px; color: var(--muted); font-size: 10px;
  font-family: ui-monospace, Menlo, Consolas, monospace; word-break: break-all;
}

/* The selected row, on whichever side it was clicked. A left edge rather than
   a fill, so it reads as a marker and does not fight the grade tints — which
   already own the row background and the left edge is the only channel left. */
tbody tr.is-sel { background: rgba(14,147,179,.10); }
tbody tr.is-sel td:first-child { box-shadow: inset 3px 0 0 var(--accent); }

.hist-table { font-size: 12.5px; }
.hist-table tbody td { height: auto; padding: 7px 6px; vertical-align: top; }
.h-line { display: flex; align-items: center; gap: 6px; min-width: 0; }
.h-what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.h-meta {
  display: flex; align-items: center; gap: 7px; margin-top: 3px;
  font-size: 10.5px; flex-wrap: wrap;
}

/* -------------------------------------------------------- store, legacy */
.store-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 14px; flex-wrap: wrap; margin-bottom: 14px;
}
.store-hint { color: var(--muted); font-size: 12.5px; }

/* Angled tabs, as on the game's own menu bar. clip-path rather than a
   transform so the text stays upright and selectable. */
.tabs { display: flex; gap: 4px; }
.tab {
  background: rgba(255,255,255,.5); color: var(--muted);
  border: 1px solid var(--line); border-bottom: 0;
  padding: 9px 26px 9px 20px; font: inherit; font-weight: 700; font-size: 13px;
  letter-spacing: .5px; text-transform: uppercase; cursor: pointer;
  clip-path: polygon(0 0, 100% 0, calc(100% - 12px) 100%, 0 100%);
  margin-right: -10px;
}
.tab:hover { background: rgba(255,255,255,.8); color: var(--text); }
.tab.is-on {
  background: linear-gradient(180deg, var(--accent-l), var(--accent));
  color: #fff; border-color: var(--accent-d);
}

/* Category chips. The count is the useful part — it tells you whether a
   filter is worth pressing before you press it. */
.cats { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 14px; }
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  background: rgba(255,255,255,.62); color: var(--muted);
  border: 1px solid var(--line); border-radius: 999px;
  padding: 4px 11px; font: inherit; font-size: 12px; cursor: pointer;
}
.chip:hover { background: #fff; color: var(--text); }
.chip.is-on {
  background: var(--accent); border-color: var(--accent-d); color: #fff;
}
.chip-n {
  font-size: 10px; font-variant-numeric: tabular-nums;
  background: rgba(16,60,80,.10); border-radius: 999px; padding: 1px 6px;
}
.chip.is-on .chip-n { background: rgba(255,255,255,.25); }

/* The second rank, scoped to the selected category. Indented and quieter than
   the first so the hierarchy reads at a glance rather than looking like the
   top row wrapped onto two lines. Empty means no rule fires at all — the
   :empty guard keeps the margin from opening a gap under the chips when
   there is nothing to choose between. */
.subcats { margin: -8px 0 14px 14px; }
.subcats:empty { margin: 0; }
.subcats .chip {
  background: rgba(255,255,255,.42); font-size: 11.5px; padding: 3px 10px;
  border-style: dashed;
}
.subcats .chip:hover { background: #fff; border-style: solid; }
.subcats .chip.is-on {
  background: var(--accent-d); border-color: var(--accent-d);
  border-style: solid; color: #fff;
}

/* Grade chips carry their own grade's colour, so the strip reads as the ladder
   it is without anyone having to learn which word is worth more. Selected
   fills with that colour rather than the accent — picking "Legendary" should
   look legendary, not look like every other selected chip on the page. */
.grades { margin: -6px 0 14px; }
.grades:empty { margin: 0; }
.grades .chip { font-size: 11.5px; padding: 3px 10px; font-weight: 700; }
.grades .chip.g1 { color: #2f7d3f; border-color: rgba(47,125,63,.45); }
.grades .chip.g2 { color: #1668a8; border-color: rgba(22,104,168,.45); }
.grades .chip.g3 { color: #7b34c2; border-color: rgba(123,52,194,.45); }
.grades .chip.g4 { color: #a86a06; border-color: rgba(168,106,6,.5); }
.grades .chip.g1.is-on { background: #2f7d3f; border-color: #2f7d3f; color: #fff; }
.grades .chip.g2.is-on { background: #1668a8; border-color: #1668a8; color: #fff; }
.grades .chip.g3.is-on { background: #7b34c2; border-color: #7b34c2; color: #fff; }
.grades .chip.g4.is-on { background: #a86a06; border-color: #a86a06; color: #fff; }
.grades .chip.is-on .chip-n { background: rgba(255,255,255,.25); }

/* Search and the availability toggle share a line: the toggle is a filter on
   the same list, so putting it anywhere else invites people to hunt for it. */
.search-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.search-row .search { flex: 1 1 320px; }
.avail {
  display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
  font-size: 13px; color: var(--muted); cursor: pointer; user-select: none;
  border: 1px solid var(--line); border-radius: 999px; padding: 8px 14px;
  background: var(--panel-2);
}
.avail:hover { color: var(--text); border-color: var(--accent-l); }
.avail input { accent-color: var(--accent); margin: 0; cursor: pointer; }
.avail.is-on { color: var(--accent-d); border-color: var(--accent); font-weight: 600; }

/* ---------------------------------------------------------------- tables */
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th {
  text-align: left; color: var(--muted); font-weight: 700; font-size: 11px;
  text-transform: uppercase; letter-spacing: .7px;
  border-bottom: 1px solid var(--line); padding: 9px 10px;
}
/* A sortable header is a button wearing the header's clothes, so it keeps
   keyboard focus and screen-reader semantics without looking like a control
   bolted onto the table. The th drops its padding and the button takes it
   back, so the click target fills the whole cell rather than just the words.
   Flagged with a class rather than :has(), which is newer than it needs to
   be for something this simple. */
th.sortable { padding: 0; }
.sortbtn {
  font: inherit; color: inherit; letter-spacing: inherit;
  text-transform: inherit; background: none; border: 0; cursor: pointer;
  width: 100%; padding: 9px 10px; text-align: inherit;
  display: flex; align-items: center; gap: 5px;
  border-radius: 6px 6px 0 0;
}
.num .sortbtn { justify-content: flex-end; }
.sortbtn:hover  { color: var(--text); background: rgba(14,147,179,.07); }
.sortbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sortbtn.is-on  { color: var(--accent); }
.sortbtn .arrow { font-size: 12px; line-height: 1; }

td { padding: 9px 10px; border-bottom: 1px solid var(--line-2); }
/* Every row the same height, whether or not it carries controls.
   A buyable row holds a number field and a button (~32px); a locked row holds
   one line of text (~19px). Left alone that is a 13px difference per row, and
   at 600 rows the list visibly breathes as you scroll past gated items. `height`
   on a table cell is a MINIMUM, not a cap, so this floors every row at the same
   size and still lets a long name wrap to two lines when it has to.

   The number is the BORDER box, not the content box — `* { box-sizing:
   border-box }` at the top of this file covers table cells too, so 38 here
   silently meant 20 and did nothing. 54 is what a row with the 35px icon and
   the buy controls comes to naturally; this brings the bare rows up to it. */
tbody td { height: 54px; }
tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: rgba(14,147,179,.055); }

.num { text-align: right; font-variant-numeric: tabular-nums; }

/* Unit price and line total, ranked. The total is the number you act on —
   selling 76 Dog Coin at 3 each is a 228 decision, not a 3 decision — so it
   carries the gold and the weight, and the per-item price steps back to being
   the footnote it actually is. */
td.unit  { color: var(--muted); font-size: 13px; }
td.total { font-weight: 700; }
/* The name cell is a flex row rather than inline text with a floated image.
   Inline alignment needed a hand-tuned `vertical-align` that only held at one
   icon size, and the dot fallback sat on a different baseline from the art it
   replaced. Flex centres both against the text at any size. */
.name {
  display: flex; align-items: center; gap: 8px;
  font-weight: 600; min-width: 0;
}
/* The name truncates rather than wrapping. Each list is half the width it used
   to be, and a three-line name drags the whole row's height with it — which is
   the thing the fixed row height exists to prevent. The full name stays on the
   row's title attribute for anyone who needs it. */
/* `0 1 auto`, not `1 1 auto`: the name should shrink when it must but never
   GROW, or on a wide monitor it stretches and shoves the grade badge to the
   far side of the column, reading as a separate column rather than a label on
   the item it belongs to. */
.name .nm { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The grade word is a SECOND carrier for something the row tint already says
   in colour, so in a narrow column it gives ground before the name does —
   but it never disappears, because colour must never be the only channel. */
.panel .grade { max-width: 74px; overflow: hidden; text-overflow: clip; }
.panel .qty { width: 56px; }

/* Item grades, tinting the WHOLE ROW, the way the game plates them.
 *
 * Colouring just the name was the first attempt and it read as a typography
 * quirk rather than a property of the item — you had to already know the
 * scheme to see it. A tinted band with a coloured left edge is what the game
 * does, and it survives being scanned at speed: the eye picks out the purple
 * row without reading a word of it.
 *
 * The tints are weak on purpose. At 1,022 rows a saturated fill becomes a
 * stripey mess and the gold prices stop being the thing you notice. The left
 * edge carries the colour at full strength instead — a 3px bar reads clearly
 * at any tint level, so the fill can stay almost subliminal.
 *
 * Common is deliberately absent: it is the default, and tinting 601 rows to
 * say "ordinary" would leave nothing untinted to contrast against. */
tbody tr.r1 { background: rgba(47,125,63,.070);  box-shadow: inset 3px 0 0 #2f7d3f; }
tbody tr.r2 { background: rgba(22,104,168,.070); box-shadow: inset 3px 0 0 #1668a8; }
tbody tr.r3 { background: rgba(123,52,194,.075); box-shadow: inset 3px 0 0 #7b34c2; }
tbody tr.r4 { background: rgba(168,106,6,.085);  box-shadow: inset 3px 0 0 #a86a06; }

/* Hover has to beat the tint or the row stops responding under the cursor,
   and the plain `tbody tr:hover` rule below cannot — same specificity, and it
   loses on order. So each grade gets its own, slightly stronger. */
tbody tr.r1:hover { background: rgba(47,125,63,.135); }
tbody tr.r2:hover { background: rgba(22,104,168,.135); }
tbody tr.r3:hover { background: rgba(123,52,194,.145); }
tbody tr.r4:hover { background: rgba(168,106,6,.160); }

/* The grade word, in its own colour, so the row is readable to anyone who
   cannot separate the tints — colour is never the only carrier. */
.grade {
  flex: 0 0 auto; margin-left: -2px; font-size: 10px; font-weight: 700;
  letter-spacing: .6px; text-transform: uppercase; opacity: .9;
}
.grade.r1 { color: #2f7d3f; }
.grade.r2 { color: #1668a8; }
.grade.r3 { color: #7b34c2; }
.grade.r4 { color: #a86a06; }
.key  { color: var(--muted); font-size: 11px; font-family: ui-monospace, Menlo, Consolas, monospace; }
.gold { color: var(--gold); font-weight: 700; }
/* Gold leaving the account. Deliberately not red — spending is not an error,
   and the history is full of it — but clearly the other direction from the
   gold that came in. */
.spent { color: var(--muted); font-weight: 700; }

/* The real item icon, pulled from the game and keyed by item_key.
   35px rather than 22 — the art is detailed enough that at the old size a
   Musket and a Crossbow were the same grey smear, which defeats the point of
   having it. The source webps are 64px, so this is still a downscale.
   `flex: 0 0` fixes the slot width, which is what keeps every name in the
   column starting at the same x whether the row got art or a dot. */
.ico {
  width: 35px; height: 35px; flex: 0 0 35px;
  object-fit: contain; image-rendering: auto;
  filter: drop-shadow(0 1px 1px rgba(16,60,80,.25));
}

/* Category dot — the fallback for the ~1600 keys with no extracted art.
   Padded out with side margins to occupy the same 35px the art would, so a
   fallback swap doesn't nudge the name sideways mid-load. */
.dot {
  display: inline-block; width: 13px; height: 13px; border-radius: 50%;
  margin: 0 11px; background: var(--muted);
  box-shadow: 0 0 0 2px rgba(255,255,255,.7);
}
.dot.material { background: #7c8ea0; }
.dot.food     { background: #4da35c; }
.dot.ammo     { background: #c9821f; }
.dot.sphere   { background: #2f86c9; }
.dot.medicine { background: #c4535c; }
.dot.seed     { background: #86a832; }
.dot.boost    { background: #9a5fc2; }
.dot.currency { background: var(--gold); }
.dot.armor,
.dot.blueprint,
.dot.accessory { background: #5c7182; }

/* -------------------------------------------------------------- controls */
.btn {
  display: inline-block;
  background: linear-gradient(180deg, var(--accent-l), var(--accent));
  color: #fff; border: 1px solid var(--accent-d); border-radius: 6px;
  padding: 8px 15px; font: inherit; font-weight: 700; font-size: 13px;
  cursor: pointer; text-decoration: none;
  box-shadow: 0 1px 0 rgba(255,255,255,.4) inset, 0 1px 2px rgba(16,60,80,.18);
}
.btn:hover { filter: brightness(1.07); }
.btn:disabled { background: #c6d5dd; border-color: #b3c4ce; color: #7c929e; cursor: not-allowed; box-shadow: none; }
.btn.small { padding: 6px 11px; font-size: 12px; }
/* Secondary action: same shape, less pull. Withdrawing is the rarer move. */
.btn.ghost {
  background: rgba(255,255,255,.8); color: var(--accent-d);
  border-color: var(--accent); box-shadow: none;
}
.btn.ghost:hover { background: #fff; }
.btn.ghost:disabled { background: #eef3f6; color: #9db0bb; border-color: #cfdde4; }

.qty {
  width: 68px; background: rgba(255,255,255,.9); color: var(--text);
  border: 1px solid var(--line); border-radius: 6px; padding: 6px 8px;
  font: inherit; font-size: 13px; text-align: right;
}
.qty:focus, .search:focus { outline: 2px solid var(--accent-l); outline-offset: 1px; }

.search {
  width: 100%; background: rgba(255,255,255,.9); color: var(--text);
  border: 1px solid var(--line); border-radius: 7px;
  padding: 10px 13px; font: inherit; margin-bottom: 14px;
}

.pill {
  display: inline-block; padding: 3px 9px; border-radius: 3px;
  font-size: 11px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase;
  border: 1px solid transparent;
}
.pill.pending    { background: rgba(185,118,13,.12);  color: var(--warn);   border-color: rgba(185,118,13,.35); }
.pill.taking,
.pill.dispensing { background: rgba(14,147,179,.12);  color: var(--accent-d); border-color: rgba(14,147,179,.35); }
.pill.filled,
.pill.claimed    { background: rgba(47,138,82,.12);   color: var(--good);   border-color: rgba(47,138,82,.35); }
.pill.failed,
.pill.expired    { background: rgba(196,68,77,.12);   color: var(--bad);    border-color: rgba(196,68,77,.35); }

/* A locked row stays legible — dimmed, not hidden. You are meant to read it
   and want it. */
tr.is-locked { opacity: .62; }
/* Dimming washes the edge bar out along with everything else, so a tinted
   locked row keeps a little more of itself. Listed explicitly rather than with
   [class*="r"], which would also match any future class containing an r. */
tbody tr.is-locked.r1,
tbody tr.is-locked.r2,
tbody tr.is-locked.r3,
tbody tr.is-locked.r4 { opacity: .72; }
tr.is-locked .name { font-weight: 500; }
.lock {
  display: inline-flex; align-items: center; gap: 5px;
  color: var(--muted); font-size: 12px; white-space: nowrap;
}
.lock-i { font-size: 11px; opacity: .8; }

/* Verb chip in the history list. Colour carries the direction at a glance:
   money in is green, money out is amber, moves are neutral. */
.hkind {
  display: inline-block; min-width: 62px; margin-right: 8px;
  font-size: 10px; font-weight: 700; letter-spacing: .6px; text-transform: uppercase;
  padding: 2px 7px; border-radius: 3px; border: 1px solid transparent;
  vertical-align: 1px;
}
.hkind.sold      { background: rgba(47,138,82,.12);  color: var(--good);   border-color: rgba(47,138,82,.3); }
.hkind.bought    { background: rgba(196,68,77,.12);  color: var(--bad);    border-color: rgba(196,68,77,.3); }
.hkind.deposited { background: rgba(14,147,179,.12); color: var(--accent-d); border-color: rgba(14,147,179,.3); }
.hkind.withdrew  { background: rgba(185,118,13,.12); color: var(--warn);   border-color: rgba(185,118,13,.3); }

/* ----------------------------------------------------------------- toast */
.toast {
  position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(20px);
  background: rgba(255,255,255,.97); color: var(--text);
  border: 1px solid var(--accent); border-left: 4px solid var(--accent);
  border-radius: 7px; padding: 11px 18px;
  opacity: 0; pointer-events: none; transition: opacity .18s, transform .18s;
  max-width: 90vw; box-shadow: 0 10px 30px rgba(16,60,80,.22);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast.bad  { border-color: var(--bad); }

.foot { color: var(--muted); font-size: 12.5px; padding: 4px 0 44px; }
.foot a { color: var(--accent-d); }

/* ============================================================ MERCHANT LEDGER
   The /shop screen. Shares the hero, cards, buttons and toast with the trade
   screen deliberately — it is the same shop, one door further in, and a
   second visual language would make it feel like a different site. */

.shopnav { margin: 0 0 14px; }

/* THE PAGE HEADER: TITLE IN THE MIDDLE, CONTROLS EITHER SIDE.
 *
 * Every screen used to open with a back button on one line and a title flush
 * left under it, which wastes the widest, most-looked-at strip of the page on
 * two short things hugging the same edge. The title is the one element every
 * page has and the one a person uses to know where they are, so it goes in
 * the centre with everything else flanking it.
 *
 * A THREE-COLUMN GRID, NOT flex with space-between. With flex the title is
 * only centred when the things on either side happen to be the same width,
 * which they never are — "Back to the market" against an empty right-hand
 * side would push the title visibly off-centre. `1fr auto 1fr` centres the
 * middle column against the CONTAINER regardless of what the sides contain,
 * which is the only version that stays true when a page adds a control later.
 *
 * The side columns collapse below 720px, where three columns of anything is
 * one column of nothing. */
.pagehead {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: start;
  gap: 10px 16px;
  margin: 0 0 18px;
}
.pagehead .ph-left  { grid-column: 1; justify-self: start; }
.pagehead .ph-mid   { grid-column: 2; text-align: center; min-width: 0; }
.pagehead .ph-right { grid-column: 3; justify-self: end; }
.pagehead .page-title { margin: 0; }
/* The standfirst under a title, where a page has one. Capped in width because
   a centred paragraph running the full width of a 2560 monitor is a line
   nobody can track back to the start of. */
.page-sub {
  margin: 8px auto 0; max-width: 620px;
  font-size: 13.5px; line-height: 1.65;
}
@media (max-width: 720px) {
  .pagehead { grid-template-columns: 1fr; }
  .pagehead .ph-left, .pagehead .ph-mid, .pagehead .ph-right {
    grid-column: 1; justify-self: start; text-align: left;
  }
  .page-sub { margin-left: 0; }
}

/* All three currencies at the same weight. Showing the scarce ones smaller
   would say the opposite of what the ledger means. */
/* `.wallet`, NOT `.purses`.

   This started life as `.purses` and inherited the bank card's rule up at line
   231 — `display:grid; grid-template-columns: 1fr 1fr` — which made this card's
   single row one item in a two-column grid, so the three currencies filled
   exactly half the width and the right half sat empty. Reusing a layout class
   for a different layout is how you get a bug that looks like a styling
   accident and is actually an inheritance. */
.wallet .purse-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 18px;
}
.wallet .purse .sub {
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.45;
  opacity: .62;
}

.lines {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 18px;
  margin: 18px 0;
}
/* Same reason as .intro .card: the grid's gap is the spacing, so the cards in
   it must not also bring their own or a wrapped second row gets 36px. */
.lines .card { margin-bottom: 0; }

.line-head h2 { margin: 0; }
.line-what { font-size: 13px; opacity: .66; margin-top: 2px; }

/* The current bonus is the number someone came to this page to read, so it is
   the only thing on the card set in display size. */
.line-now { display: flex; align-items: baseline; gap: 10px; margin: 14px 0 8px; }
.line-now .big { font-size: 40px; font-weight: 700; line-height: 1; }
.line-now .cap { font-size: 13px; opacity: .55; }

.bar {
  height: 8px;
  border-radius: 999px;
  background: rgba(255, 255, 255, .10);
  overflow: hidden;
}
.bar .fill {
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, #7ec8ff, #4aa3ff);
  transition: width .35s ease;
}
.steps { font-size: 12px; opacity: .6; margin: 6px 0 14px; }

.buy-row { display: flex; flex-wrap: wrap; gap: 8px; }
.buy-row .btn { flex: 1 1 auto; }

.next-note { font-size: 12px; opacity: .62; margin-top: 10px; line-height: 1.5; }

/* Said plainly rather than by greying a button and leaving people to guess how
   far off they are. */
.short { font-size: 12px; margin-top: 8px; opacity: .8; }


.tier-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
  margin-top: 14px;
}
.tier { padding: 12px 14px; border-radius: 10px; background: rgba(255, 255, 255, .05); }
.tier-range { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; opacity: .5; }
.tier-cur { font-size: 16px; font-weight: 600; margin: 2px 0 6px; }
.tier .sub { font-size: 12px; line-height: 1.45; opacity: .6; }

/* No top margin. This card was a page FOOTER when that margin was written,
   and it is a grid item now — so the margin pushed it 22px below the card
   beside it and took 22px off its height, which is the whole of the "uneven
   gaps" everyone kept seeing. A rule that outlives the layout it was written
   for is worse than no rule: it looks intentional. */

/* THE DOOR TO THE LEDGER — and it was too quiet.

   The first version was a 12px centred link at 72% opacity, which is what you
   write when you are worried about competing with Deposit all. The result was
   a whole feature nobody could see. A destination still has to LOOK like
   something you can go to.

   So: full width, its own border, an arrow, and a faint accent tint that marks
   it as a different KIND of thing to the two buttons above it rather than a
   quieter version of them. It still does not shout — it just stops hiding. */
.ledger-link {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-top: 12px;
  padding: 10px 12px;
  border-radius: 10px;
  /* LEGENDARY. Same #a86a06 the tables plate a rarity-4 item in, because the
     shop already taught everyone what that colour means: the good one, the one
     worth stopping for. Borrowing an established cue beats inventing a new
     accent nobody has been trained on. */
  border: 1px solid rgba(168, 106, 6, .48);
  background: linear-gradient(180deg, rgba(168, 106, 6, .155), rgba(168, 106, 6, .065));
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: .01em;
  text-align: center;
  text-decoration: none;
  color: #a86a06;
  transition: border-color .15s ease, background .15s ease, transform .15s ease;
}
.ledger-link::after {
  content: "\2192";
  opacity: .75;
  transition: transform .15s ease;
}
.ledger-link:hover {
  border-color: rgba(168, 106, 6, .85);
  background: linear-gradient(180deg, rgba(168, 106, 6, .26), rgba(168, 106, 6, .12));
  transform: translateY(-1px);
}
.ledger-link:hover::after { transform: translateX(3px); }

/* THE .vault-door VARIANT IS GONE with the element it dressed. It was a
   neutral-coloured twin of .ledger-link sitting in the till column, and both
   full-width doors there were removed as duplicates of the header nav — see
   the note where they used to be in index.html.

   `.ledger-link` itself stays: shop.js still builds one for "Open your vault"
   on the perks screen, which is a link to somewhere else rather than a second
   copy of a link already on screen. */

/* The same door, in the header, for anyone who never scrolls to the till. */
.hero-nav { display: flex; align-items: center; gap: 10px; }

/* THE ACCOUNT CHIP IS A BUTTON NOW, in both states. Signed out it was a blank
   space where a sign-in link should be; signed in it was plain text on a
   photograph, which read as decoration rather than as somewhere to go. */
.who-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 13px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, .28);
  background: #16242e;
  color: #e8f1f7;
  font-size: 13px;
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
  transition: border-color .15s ease, background .15s ease;
}
.who-link:has(.av) { padding-left: 6px; }
.who-link:hover { border-color: rgba(255, 255, 255, .55); background: #22343f; }

/* THE FACE IN THE CHIP.
   Same avatar the account carries on LetsRollTonight and RaceReadyCheck: the
   Steam picture if there is one, otherwise the emoji-on-colour preset. 22px so
   it sits inside the pill's existing 7px padding without making the header row
   taller than it already is — the chip has to line up with the perks button
   beside it, and a bigger avatar would have moved both. */
.who-link .av {
  width: 22px; height: 22px; border-radius: 50%;
  flex: 0 0 auto; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px; line-height: 1; color: #fff;
  background: #3a1260;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .45);
}
.who-link .av img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Signed out, it is the primary thing on the bar and should look it. */
.who-link.signin {
  border-color: rgba(120, 220, 160, .6);
  background: #10321f;
  color: #9ff0bf;
}
.who-link.signin:hover { border-color: #7fe0a8; background: #16452b; color: #d6ffe8; }
/* The header sits on the dark key art, where #a86a06 reads as brown rather
   than as gold. Same hue, lifted until it carries — the cue is the colour
   family, and a legendary badge nobody can read is not a cue. */
.hero-link {
  padding: 7px 13px;
  border-radius: 999px;
  /* OPAQUE, NOT TINTED. These sit on Steam key art — a busy, high-contrast
     photograph — and a translucent pill takes whatever is behind it as its
     background. Over a bright patch the gold text vanished entirely. A solid
     fill means the label reads the same wherever the art happens to be light
     or dark, which is the whole job of a button. */
  border: 1px solid rgba(226, 160, 40, .75);
  background: #2a1d06;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .35);
  font-size: 13px;
  font-weight: 700;
  text-decoration: none;
  color: #f0b845;
  white-space: nowrap;
  transition: border-color .15s ease, background .15s ease, color .15s ease;
}
.hero-link:hover { border-color: #e2a028; background: #3c2a09; color: #ffd98a; }

/* THE GLYPHS ON THE TWO HEADER DOORS — a coin on the perks pill, a chest on
   the vault pill.
 *
 * Inline SVG drawn on `currentColor` rather than an image file, which is what
 * makes them free: each one inherits its pill's colour, so the coin is gold
 * and the chest is the quiet blue without either being drawn twice, and the
 * hover states already defined above tint the icon along with the label with
 * no extra rules at all. The item art in /assets/items/ has no chest — only
 * chest KEYS — so a coin from there and a chest from somewhere else would have
 * been two different drawing styles sitting 8px apart.
 *
 * 15px, and the pill keeps its height: the glyph is shorter than the 13px bold
 * label's line box, so nothing here moves the header row. */
.hero-link { display: inline-flex; align-items: center; gap: 7px; }
.hl-ico {
  width: 15px; height: 15px; flex: 0 0 auto;
  display: block; overflow: visible;
  opacity: .92;
}
.hero-link:hover .hl-ico { opacity: 1; }


/* The second door in the header — the vault — deliberately not gold.
   The gold pill means "the good one, worth stopping for", and putting two of
   them side by side would spend that meaning on a link that does not need it.
   Same shape, neutral colour: still obviously a place you can go. */
.hero-link.quiet {
  border-color: rgba(255, 255, 255, .28);
  background: #16242e;               /* opaque, for the same reason */
  color: #e8f1f7;
  font-weight: 600;
}
.hero-link.quiet:hover {
  border-color: rgba(255, 255, 255, .55);
  background: #22343f;
  color: #fff;
}

@media (max-width: 640px) {
  .hero-link { display: none; }   /* the bank card still carries it */
}

/* A buy button carries two things: what it costs, and what it does. The second
   is the reason anyone presses it, so it gets its own line rather than being
   squeezed into the label. */
.buy-row .btn { display: block; text-align: left; line-height: 1.35; padding: 10px 14px; }
.b-cost { display: block; font-weight: 600; }
.b-preview { display: block; margin-top: 3px; font-size: 12px; opacity: .78; font-weight: 400; }
.b-now   { text-decoration: line-through; opacity: .7; }
.b-arrow { margin: 0 5px; opacity: .7; }
.b-next  { font-weight: 600; }
.b-unit  { opacity: .8; }

/* Said out loud rather than left as a button that does nothing. */
.notlive {
  margin: 4px 0 14px;
  padding: 9px 11px;
  border-radius: 8px;
  font-size: 12px;
  line-height: 1.5;
  background: rgba(255, 200, 90, .10);
  border: 1px solid rgba(255, 200, 90, .28);
}

.ledger-note p { margin: 0 0 10px; line-height: 1.6; }
.ledger-note p:last-child { margin-bottom: 0; }

/* The page needs to say what it is. It had a browser-tab title and a header
   wordmark and nothing on the page itself. */
.page-title {
  margin: 4px 0 16px;
  font-size: 30px;
  line-height: 1.1;
  letter-spacing: -.01em;
}

/* The rules and the currency bands, side by side. They are one explanation —
   "here is how it works" and "here is what it costs as you climb" — and the
   bands spent their first version at the very bottom of the page, below every
   number they exist to make sense of. */
.intro {
  display: grid;
  /* Not 1fr each: the left card is two short paragraphs and the right is three
     labelled bands, so an even split starves the one with more in it. */
  grid-template-columns: minmax(320px, 5fr) minmax(340px, 7fr);
  gap: 18px;
  align-items: stretch;
  margin-bottom: 18px;      /* the same rhythm every other card sets */
}
@media (max-width: 900px) { .intro { grid-template-columns: 1fr; } }
/* EQUAL HEIGHTS, EARNED RATHER THAN FORCED.

   Two attempts got this wrong in opposite directions: stretching the short
   card left a void inside it, and letting each size naturally left a gap
   beside it. Both are the same problem in different clothes — the two cards
   held very different amounts of content.

   So the CONTENT moved. A sentence that belonged on the left had been on the
   right, and the right opened with a paragraph restating the left. They are
   close enough now that matching heights costs a few pixels of padding rather
   than a hole. */
.intro .card {
  height: 100%;
  /* AND NO BOTTOM MARGIN. `.card` carries 18px, which with `height: 100%`
     makes the box 100% + 18px — so it overflows the grid row by 18px and eats
     the gap underneath instead of leaving one. The GRID owns the spacing
     between rows; its items should own none. */
  margin-bottom: 0;
}
.intro h2 { margin: 0 0 10px; font-size: 15px; text-transform: uppercase;
            letter-spacing: .07em; opacity: .72; }

/* Inside a half-width card the three bands read better stacked than squeezed
   into three columns that each fit four words. */
.intro .tier-row { grid-template-columns: 1fr; gap: 6px; margin-top: 10px; }
.intro .tier { padding: 9px 12px; }
.intro .tier { display: grid; grid-template-columns: 96px 1fr; gap: 4px 12px; align-items: baseline; }
.intro .tier-range { grid-row: 1; }
.intro .tier-cur   { grid-row: 1; margin: 0; font-size: 15px; }
.intro .tier .sub  { grid-column: 2; }

/* A ROW WITH A SALE IN FLIGHT.

   Dimmed, and struck through with a barber-pole so the state reads at a glance
   from anywhere in the list rather than only where the word "Pending" is. The
   stripes are the "in transit" cue people already know from progress bars, and
   they sit UNDER the text so nothing becomes harder to read. */
tbody tr.is-pending {
  opacity: .55;
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(127, 127, 127, .00) 0 8px,
    rgba(127, 127, 127, .14) 8px 16px
  );
}
tbody tr.is-pending:hover { opacity: .78; }
tbody tr.is-pending .name .nm { text-decoration: line-through; text-decoration-thickness: 1px; }

.pending-chip {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  border: 1px dashed currentColor;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .03em;
  white-space: nowrap;
  opacity: .95;
}

/* A PURCHASE IN FLIGHT — the same stripes, none of the dimming.

   A pending SALE is finished business: the pile is spoken for and there is
   nothing useful left to press, so the row steps back. A pending PURCHASE is
   not — wanting a second one is a real thing to want — so this row keeps its
   full contrast and its button, and the stripes say only "something is already
   happening here". Greying a control that still works would be a lie about it. */
tbody tr.is-awaiting {
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(126, 200, 255, .00) 0 8px,
    rgba(126, 200, 255, .10) 8px 16px
  );
}
tbody tr.is-awaiting .pending-chip { margin-right: 6px; opacity: .85; }

/* Banking a currency out of the bag, on the purse it lands in. */
.wallet .purse .btn.wide { margin-top: 8px; }
.wallet .banking {
  margin-top: 8px;
  padding: 5px 9px;
  border-radius: 999px;
  border: 1px dashed currentColor;
  font-size: 11px;
  font-weight: 600;
  text-align: center;
  opacity: .8;
}

/* Whether the income tick is actually running. A payout nobody can see is
   indistinguishable from one that never happened. */
.tickstate {
  margin: 2px 0 12px;
  padding: 8px 10px;
  border-radius: 8px;
  background: rgba(127, 127, 127, .10);
  font-size: 12px;
  line-height: 1.5;
}
.tick-live { color: #2f7d3f; font-weight: 700; }
.tick-wait { opacity: .6; font-weight: 700; }

/* ==================================================================== VAULT
   The /vault screen. Same hero, cards, tables and row states as the trade
   screen, for the same reason the ledger shares them: it is the same shop and
   the same gesture — items leaving your bag — against a different
   destination. A second visual language for it would teach a difference that
   is not there. */

/* Bag left, storage right, items moving rightwards. Even columns here, unlike
   the trade screen's three, because neither side is the reference the other is
   read against — they are two lists of the same kind of thing. */
.vault-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 18px;
  align-items: start;
  margin-bottom: 18px;
}
/* The grid owns the spacing between its items; its items own none, or a
   wrapped row gets 18px of gap plus 18px of margin. Third time this exact
   rule has been needed — see .intro .card and .lines .card. */
.vault-grid .card { margin-bottom: 0; }

/* THE RULES HAVE MOVED TWICE AND THIS IS THE THIRD PLACE.
   First a full card at the FOOT of the page — below every control it
   explained, which is the one place an instruction cannot do its job. Then a
   caption flexed to the right of the title, which was free height and read
   correctly at 1280 and stranded a slab of grey type against the far bezel at
   2560. Now a card sharing a row with the meter; see .vault-top below.
   The title keeps the row to itself. */
.vault-head { margin-bottom: 16px; }
.vault-head .page-title { margin: 0; }

/* THE METER AND THE RULES SIDE BY SIDE.
 *
 * The rules were a caption flexed to the right of the page title, ragged edge
 * inward, flush against the page edge. At 1280 that reads as a caption. At
 * 2560 it is a slab of grey type stranded against the right-hand bezel with a
 * screen's width of empty blue between it and the heading it belongs to, which
 * is what Matt was looking at when he said it should be a card.
 *
 * Two equal columns rather than a narrow sidebar: neither of these is
 * subordinate to the other. The meter is what you can do, the card is what
 * happens when you do it. They collapse to one column before the text measure
 * gets uncomfortable. */
.vault-top {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 18px;
  align-items: start;
  margin-bottom: 18px;
}
/* AND THE ITEMS OWN NONE OF THE SPACING. Fourth time — see .vault-grid .card
   directly above, .intro .card, and .lines .card. Leaving .card's own 18px
   margin-bottom in place under a grid that already has a gap adds the two
   together, so this row sat 34px above the panels while its two cards were
   18px apart, and the gap that was supposed to be square was not. One number,
   18, in all four directions on this page. */
.vault-top .card { margin-bottom: 0; }
@media (max-width: 900px) { .vault-top { grid-template-columns: minmax(0, 1fr); } }

.vault-how-card h2 { margin: 0 0 8px; font-size: 15px; }
.vault-how-card p {
  margin: 0 0 8px;
  font-size: 12.5px;
  line-height: 1.6;
  opacity: .78;
}
.vault-how-card p:last-child { margin-bottom: 0; }

.vault-meter { padding-bottom: 14px; }
.vault-count { display: flex; align-items: baseline; gap: 10px; }
.vault-count .big { font-size: 34px; font-weight: 700; line-height: 1; }
.vault-count .cap { font-size: 13px; opacity: .55; }
.vault-meter .bar { margin: 12px 0 8px; }
/* Full is a state worth seeing before you press a button that will refuse.
   The same warn amber the pending pill uses, so it reads as "attention",
   not as "error". */
.vault-meter .bar .fill.full { background: var(--warn); }
.vault-note { font-size: 12.5px; line-height: 1.55; opacity: .72; }
.inline-link { color: #a86a06; font-weight: 700; text-decoration: none; }
.inline-link:hover { text-decoration: underline; }

/* FOUR COLUMNS: item, what you have, how much to move, the button.
   The item name is the only one that should absorb slack — the other three
   are a number, a 68px box and a button, and letting them stretch just moves
   the button away from the box it acts on. */
.vault-grid table { table-layout: fixed; }
.vault-grid th:nth-child(2), .vault-grid td:nth-child(2) { width: 74px; }
.vault-grid th:nth-child(3), .vault-grid td:nth-child(3) { width: 86px; }
.vault-grid th:nth-child(4), .vault-grid td:nth-child(4) { width: 92px; }
/* The box is right-aligned with the button under it, and full-width inside
   its own column so the two edges line up down the list. */
.vault-grid td:nth-child(3) .qty { width: 100%; }
/* A disabled number box should read as unavailable rather than as empty. */
.vault-grid .qty:disabled { opacity: .45; cursor: not-allowed; }

/* A DEPOSIT THAT HAS NOT LANDED YET.
   It is not in the stash — it is a sell order on its way — so it cannot be a
   table row without inventing one. Listed under the table instead, faintly,
   because "it worked, wait a minute" and "where did my items go" are the same
   sixty seconds and only one of them is true. */
.arriving {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
  padding: 7px 9px;
  border-radius: 8px;
  background: rgba(126, 200, 255, .08);
  font-size: 12px;
  line-height: 1.45;
  opacity: .85;
}

/* ================================================== THE BUILD BANNER
   Shown when the page discovers the shop has been updated underneath it.

   NOT the toast. A toast fades after five seconds, and this is the explanation
   for the page being about to reload — it has to still be on screen when the
   reload happens, or the reload is just the site glitching. So it is a fixed
   bar at the top, it does not dismiss, and it uses the warn amber rather than
   the error red: nothing has gone wrong, something has changed. */
.build-banner {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 60;                       /* over the hero, under nothing */
  padding: 11px 18px;
  background: var(--warn);
  color: #fff;
  font-size: 13.5px;
  font-weight: 600;
  text-align: center;
  letter-spacing: .01em;
  box-shadow: 0 2px 12px rgba(16, 60, 80, .22);
  transform: translateY(-100%);
  transition: transform .25s ease;
}
.build-banner.show { transform: translateY(0); }

/* ============================================== THE BLACK MARKET DOOR
   At the very bottom of the page, in normal flow, small and unlabelled.

   The first version was `position: fixed` in the corner of the viewport. That
   was wrong twice over: it followed the reader around while they were trying
   to compare prices, and being permanently visible turned a secret into
   furniture — the kind of thing you stop seeing after a day. A door you have
   to scroll past the whole shop to find is worth more the once than a badge in
   the corner is worth a hundred times.

   `image-rendering: pixelated` matters: the asset is 384px wide and drawn at a
   quarter of that, and a browser's default smoothing turns hand-drawn pixel
   art into mush at any non-integer scale. */
.bm-door-wrap {
  display: flex;
  justify-content: flex-end;
  max-width: 1400px;
  margin: -8px auto 0;
  padding: 0 22px 10px;
  line-height: 0;
}
.bm-door-link { display: inline-block; line-height: 0; border-radius: 4px; }
.bm-door {
  display: block;
  width: 92px;                  /* small. it is a rumour, not a sign. */
  opacity: .82;
  image-rendering: pixelated;
  transition: opacity .2s ease, transform .2s ease;
}
.bm-door-link:hover .bm-door { opacity: 1; transform: translateY(-3px); }
.bm-door-link:focus-visible { outline: 3px solid #e2a028; outline-offset: 4px; }

@media (max-width: 900px) { .bm-door { width: 74px; } .bm-door-wrap { padding: 0 12px 10px; } }
/* Someone who has asked their system not to animate things should not be given
   a blinking animal at the foot of the page. */
@media (prefers-reduced-motion: reduce) { .bm-door-wrap { display: none; } }

/* THE ONE-TIME NOTE. Shown on the market page the first time a player has a
   reason to care that the cave exists — a Standing step bought, or a token in
   hand — and never again after they read it or go down there. */
.bm-hint {
  position: relative;
  margin: 0 0 18px;
  padding: 14px 44px 14px 16px;
  border-radius: 12px;
  border: 1px solid rgba(168, 106, 6, .45);
  background: linear-gradient(180deg, rgba(168,106,6,.16), rgba(168,106,6,.07));
  font-size: 13.5px;
  line-height: 1.6;
  color: #7a5405;
}
.bm-hint b { color: #a86a06; }
.bm-hint .x {
  position: absolute; top: 8px; right: 10px;
  background: none; border: 0; cursor: pointer;
  font-size: 18px; line-height: 1; color: #a86a06; opacity: .6; padding: 4px 6px;
}
.bm-hint .x:hover { opacity: 1; }

/* ============================================ FOLDED FILTERS
   Both stores had every chip open at once — eleven categories, six grades and
   a second rank of shelves whenever one was picked. That is a wall of buttons
   in front of a shop, and almost nobody changes them twice in a session.

   `<details>` rather than a hand-rolled popover: one element, keyboard
   accessible for free, works before the JavaScript loads, and impossible to
   position wrongly. */
.filters { margin: 0 0 12px; }
.filters > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, .62);
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
  user-select: none;
}
.filters > summary::-webkit-details-marker { display: none; }
/* The caret rotates, so open/shut is legible without reading the words. */
.filters > summary::after {
  content: "\25BE";
  font-size: 11px;
  opacity: .7;
  transition: transform .15s ease;
}
.filters[open] > summary::after { transform: rotate(180deg); }
.filters > summary:hover { background: #fff; color: var(--text); }
.filters > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Something IS filtered — the shut state has to say so, or "why is this list
   empty" becomes a support question. */
.filters.is-on > summary {
  border-color: var(--accent);
  color: var(--accent-d);
  background: rgba(14, 147, 179, .09);
}
.filters .fbody { padding-top: 10px; }
.filters .fbody .cats:last-child { margin-bottom: 0; }

/* The cave's own copy, on a photograph rather than on paper. */
body.bm .filters > summary {
  border-color: rgba(150, 190, 220, .28);
  background: rgba(6, 12, 22, .6);
  color: #b8c8d8;
}
body.bm .filters > summary:hover { background: rgba(6, 12, 22, .85); color: #eaf2f8; }
body.bm .filters.is-on > summary {
  border-color: rgba(226, 160, 40, .6);
  background: rgba(168, 106, 6, .22);
  color: #ffd98a;
}
body.bm .cats .chip { border-color: rgba(150, 190, 220, .25); color: #c3d3e2;
                      background: rgba(255, 255, 255, .05); }
body.bm .cats .chip.is-on { border-color: rgba(226, 160, 40, .7);
                            background: rgba(168, 106, 6, .26); color: #ffd98a; }

/* A text button that is plainly an action but must not compete with Buy or
   Sell — used for "cancel" on a pending row, where the primary control has
   deliberately been taken away. */
.linkbtn {
  background: none; border: 0; padding: 2px 4px; margin-left: 8px;
  font: inherit; font-size: 11.5px; color: var(--muted);
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
}
.linkbtn:hover { color: var(--bad); }
.linkbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

/* ------------------------------------------------- the token browser
 *
 * The shelf at the top of the cave, and the dialog that opens when a gated
 * row is bought. Both exist for the same rule: anywhere a token can be spent,
 * the player picks WHICH — and can see what each one would do first.
 *
 * The chips carry three lines because two of them are the point. A name and a
 * quantity is a receipt; "opens 14 below" is the thing that makes someone
 * click. They were unstyled until the browser was built, which is its own
 * small answer to how much use they were getting. */
.tok-row {
  display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 10px;
}
.tok-chip {
  display: flex; flex-direction: column; gap: 3px;
  min-width: 190px; max-width: 260px; padding: 9px 11px;
  text-align: left; font: inherit; cursor: pointer;
  border: 1px solid rgba(226, 160, 40, .30); border-radius: 10px;
  background: rgba(168, 106, 6, .14); color: #e9dcc4;
  transition: border-color .12s, background .12s;
}
.tok-chip:hover { border-color: rgba(226, 160, 40, .60);
                  background: rgba(168, 106, 6, .24); }
.tok-chip.is-on { border-color: rgba(226, 160, 40, .85);
                  background: rgba(168, 106, 6, .34); color: #ffd98a; }
.tok-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tk-top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.tk-n   { font-weight: 650; font-size: 13px; }
.tk-q   { font-size: 11.5px; opacity: .85; white-space: nowrap; }
.tk-what  { font-size: 11.5px; line-height: 1.35; opacity: .78; }
.tk-opens { font-size: 11.5px; font-weight: 600; color: #ffd98a; opacity: .95; }

/* THE TOKEN, ON THE COIN IT WAS STRUCK ON.
 *
 * The chip used to be a name, a count and a sentence in a gold-tinted box —
 * accurate, and completely unlike the object the player just watched slide
 * under the marker to win it. The crate room and the cave now draw the same
 * medallion the odds dialog does, at thumbnail size, so a Free Rein is
 * recognisably the same gold thing everywhere it appears.
 *
 * The grade comes from the server, derived from the drop table — see
 * token_grades() — so nothing here has to guess a rarity from a key name. */
.tok-chip { flex-direction: row; align-items: center; gap: 11px; max-width: 320px; }
.tk-coin { width: 54px; height: 54px; flex: 0 0 54px; object-fit: contain;
           filter: drop-shadow(0 2px 5px rgba(0,0,0,.5)); }
.tk-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }

/* The dialog. Deliberately plain — it is a decision, not a display, and it
   sits over a photograph that is already doing a lot of work. */
.modal-back {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center; padding: 20px;
  background: rgba(6, 10, 16, .68);
}
.modal {
  width: min(460px, 100%); max-height: min(80vh, 640px); overflow: auto;
  padding: 18px 18px 14px; border-radius: 14px;
  border: 1px solid rgba(226, 160, 40, .35);
  background: #141a22;
  /* LIGHT TYPE, EXPLICITLY. It was `color: var(--text)`, which is correct for
     everything on a page whose surfaces are light — and this surface is not.
     The heading came out navy on near-black. A dark panel has to declare its
     own foreground rather than inherit one chosen for the opposite background,
     and .muted has to be re-pointed for the same reason. */
  color: #e8f1f7;
  box-shadow: 0 18px 50px rgba(0, 0, 0, .55);
}
.modal h3 { margin: 0 0 4px; font-size: 16px; color: #fff; }
.modal .muted { color: rgba(232, 241, 247, .62); }
.modal .sm { font-size: 12px; margin: 0 0 12px; }
/* The chips are a light-theme control sitting on a dark panel; same treatment
   as the Black Market's, which solved this once already. */
.modal .chip {
  border-color: rgba(255, 255, 255, .18);
  background: rgba(255, 255, 255, .06);
  color: rgba(232, 241, 247, .82);
}
.modal .chip:hover { border-color: rgba(255, 255, 255, .40); }
.modal .chip.is-on {
  border-color: var(--accent);
  background: rgba(14, 147, 179, .30);
  color: #fff;
}
.modal-acts { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }

.tokpick-list { display: flex; flex-direction: column; gap: 6px; }
.tokpick-row {
  display: flex; align-items: flex-start; gap: 10px; padding: 9px 11px;
  border: 1px solid rgba(255, 255, 255, .10); border-radius: 10px;
  background: rgba(255, 255, 255, .03); cursor: pointer;
}
.tokpick-row:hover { border-color: rgba(226, 160, 40, .45); }
.tokpick-row.is-best { border-color: rgba(226, 160, 40, .45);
                       background: rgba(168, 106, 6, .16); }
.tokpick-row input { margin-top: 3px; flex: none; }
.tp-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.tp-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.tp-n    { font-weight: 650; font-size: 13px; }
.tp-q    { font-size: 11.5px; color: var(--muted); }
.tp-best {
  font-size: 10.5px; font-weight: 700; letter-spacing: .3px; text-transform: uppercase;
  padding: 1px 6px; border-radius: 999px; color: #ffd98a;
  background: rgba(168, 106, 6, .34);
}
.tp-what { font-size: 11.5px; color: var(--muted); line-height: 1.35; }

.tokpick-blocked {
  margin-top: 12px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, .08);
}
.tp-blocked-head {
  font-size: 11px; text-transform: uppercase; letter-spacing: .4px;
  color: var(--muted); margin-bottom: 6px;
}
.tp-blocked-row {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 3px 0; font-size: 12px; color: var(--muted); opacity: .8;
}
.tp-why { text-align: right; font-style: italic; }

/* ================================================================ crates
 *
 * The reel is a CS:GO-style horizontal strip that decelerates onto the
 * winner. Worth being explicit about what the CSS is and is not doing: it
 * animates a transform to a position JavaScript computed from a result the
 * SERVER decided. There is no randomness in this stylesheet and there must
 * never be — see the header of crates.js.
 *
 * --reel-pitch is card width plus its gap, and crates.js reads this exact
 * property to work out where to stop. Change it here and the landing follows;
 * hard-code it in two places and the winner drifts off the marker. */
:root { --reel-pitch: 132px; }

.crate-stage { padding-bottom: 16px; }
.crate-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 18px; flex-wrap: wrap; margin-bottom: 14px;
}
.crate-have { display: flex; align-items: baseline; gap: 10px; }
.crate-have .big { font-size: 34px; font-weight: 700; line-height: 1; }
.crate-have .cap { font-size: 13px; opacity: .55; }
.crate-buy { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.crate-price { font-size: 12px; color: var(--muted); width: 100%; text-align: right; }

/* --- the reel ---------------------------------------------------------- */
.reel-wrap {
  position: relative; overflow: hidden;
  height: 96px; border-radius: 12px;
  border: 1px solid var(--line);
  /* SOLID, and the fades below reuse the same custom property. They were a
     hard-coded #16242e against a translucent background, which on the light
     card this sits in rendered as two black bars rather than as a fade. A
     gradient to a colour that is not the background behind it is not a fade,
     it is a rectangle. */
  --reel-bg: #1d2c37;
  background: var(--reel-bg);
}
.reel {
  display: flex; align-items: center; height: 100%;
  will-change: transform;                 /* promoted once, not per frame */
}
/* The marker is the whole point of the geometry: the card centred under this
   line is the one you won. It sits above the cards and ignores the pointer. */
.reel-marker {
  position: absolute; left: 50%; top: 0; bottom: 0; width: 2px;
  margin-left: -1px; z-index: 3; pointer-events: none;
  background: linear-gradient(180deg, transparent, var(--accent) 12%,
                              var(--accent) 88%, transparent);
  box-shadow: 0 0 10px rgba(14, 147, 179, .75);
}
/* Fades at both ends so cards enter and leave rather than popping at a hard
   edge — the single cheapest thing that makes a strip read as motion. */
.reel-fade {
  position: absolute; top: 0; bottom: 0; width: 84px; z-index: 2;
  pointer-events: none;
}
.reel-fade.left  { left: 0;  background: linear-gradient(90deg,  var(--reel-bg), transparent); }
.reel-fade.right { right: 0; background: linear-gradient(270deg, var(--reel-bg), transparent); }

.reel-card {
  color: #e8f1f7;                  /* the reel is dark whatever the page is */
  flex: 0 0 120px; width: 120px; margin-right: 12px;   /* 120 + 12 = --reel-pitch */
  height: 74px; border-radius: 9px;
  display: flex; flex-direction: column; justify-content: center; gap: 4px;
  padding: 8px 10px; box-sizing: border-box;
  border: 1px solid rgba(255, 255, 255, .10);
  background: rgba(255, 255, 255, .04);
  /* The tier colour reads as a bar at the foot of the card rather than as a
     border, so a legendary is obvious in peripheral vision at speed. */
  border-bottom-width: 3px;
}
.rc-tier {
  font-size: 9.5px; text-transform: uppercase; letter-spacing: .6px;
  opacity: .7; font-weight: 700;
}
.rc-name { font-size: 12px; font-weight: 650; line-height: 1.25; }

/* TIER COLOURS, one definition, used by the reel, the results, the odds table
   and the pull feed. Grey, blue, purple, magenta, gold — the ladder people
   already know from every game that has one, which is worth more here than
   being original. */
/* TIER METAL, DEFINED ONCE AND UNSCOPED.
   The reel cards, the coins in the odds dialog, the result rows, the odds
   band headers and the pull feed all read these three. They used to be
   declared twice — a border colour here and a --tok-a set on .tok-sq four
   hundred lines down — which is two palettes that agree until one of them is
   edited. */
.t-common    { --tok-a: #9aa7b3; --tok-b: #59656f; --tok-hi: #d3dbe2; }
.t-uncommon  { --tok-a: #58a0e6; --tok-b: #1f4d80; --tok-hi: #a9d2f7; }
.t-rare      { --tok-a: #9a77dd; --tok-b: #4a3179; --tok-hi: #cfb6f5; }
.t-epic      { --tok-a: #dd6ad1; --tok-b: #79216f; --tok-hi: #f4b0ec; }
.t-legendary { --tok-a: #f3ba3f; --tok-b: #8a5a05; --tok-hi: #ffe6a3; }

/* THE WHOLE CARD IS THE TIER, not a stripe along its edge.
 *
 * It was a 3px bar at the foot of an otherwise identical grey card, and Matt's
 * note was the right one: the moment a gold card slides under the marker has
 * to be legible from across the room, and a coloured hairline is not. At speed
 * you cannot read a card's name — you can only read its colour — so the colour
 * has to be the card.
 *
 * Which also makes the reel do half the work of teaching the tiers. Fifty
 * cards stream past every spin and almost all of them are grey; the first time
 * a purple one goes by, its rarity has been explained without a word. */
.reel-card {
  background: linear-gradient(160deg,
              color-mix(in srgb, var(--tok-a) 88%, white) 0%,
              var(--tok-a) 42%,
              var(--tok-b) 100%);
  border-color: color-mix(in srgb, var(--tok-a) 60%, black);
  border-bottom-color: color-mix(in srgb, var(--tok-b) 70%, black);
}
.rc-tier { opacity: .85; color: color-mix(in srgb, var(--tok-hi) 80%, white); }
.rc-name { color: #fff; text-shadow: 0 1px 2px rgba(0, 0, 0, .55); }

/* Legendary gets a glow the others do not. Every tier being merely a different
   colour makes the top one just another colour; this is the card you want
   somebody to shout at. */
.reel-card.t-legendary {
  box-shadow: 0 0 16px rgba(243, 186, 63, .55), inset 0 0 12px rgba(255, 230, 160, .35);
}
.reel-card.t-epic {
  box-shadow: 0 0 12px rgba(221, 106, 209, .40);
}
.reel-card.is-win { box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .55); }
.reel-card.is-win.t-legendary {
  box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .75),
              0 0 24px rgba(243, 186, 63, .8);
}

.crate-acts { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
.crate-note { font-size: 12px; color: var(--muted); }

/* --- results ----------------------------------------------------------- */
.crate-results { margin-top: 14px; }
.res-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; }
.res-row {
  padding: 10px 12px; border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, .10); border-left-width: 3px;
  background: rgba(255, 255, 255, .04);
}
.res-row { border-left-color: var(--tok-a); }
.res-row.t-legendary { background: rgba(243, 186, 63, .12); }
.pull-row { border-left-color: var(--tok-a); }
.odds-band { border-left-color: var(--tok-a); }
.odds-band.t-legendary { color: #f3ba3f; }
.res-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.res-tier { font-size: 9.5px; text-transform: uppercase; letter-spacing: .6px; opacity: .65; font-weight: 700; }
.res-name { font-size: 13.5px; font-weight: 700; }
.res-detail { font-size: 12.5px; color: var(--muted); margin-top: 3px; line-height: 1.4; }
.res-practice { font-size: 11px; font-style: italic; opacity: .6; margin-top: 4px; }

/* --- the carry / pulls pair ------------------------------------------- */
.crate-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 18px; align-items: start; margin-bottom: 18px;
}
.crate-grid .card { margin-bottom: 0; }   /* the grid owns the spacing */
.carry-block { margin-bottom: 14px; }
.carry-block:last-child { margin-bottom: 0; }
.carry-block h3 {
  font-size: 11px; text-transform: uppercase; letter-spacing: .5px;
  color: var(--muted); margin: 0 0 7px;
}
.eff-row {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 5px 0; font-size: 12.5px;
  border-bottom: 1px solid rgba(255, 255, 255, .06);
}
.eff-when { color: var(--muted); font-size: 11.5px; white-space: nowrap; }
.pull-list { display: flex; flex-direction: column; }
.pull-row {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 5px 0 5px 9px; font-size: 12.5px;
  border-left: 3px solid transparent;
  border-bottom: 1px solid rgba(255, 255, 255, .06);
}
.pl-detail { color: var(--muted); text-align: right; }

/* --- the odds table ---------------------------------------------------- */
.odds-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.odds-sub { font-size: 12px; color: var(--muted); margin-bottom: 10px; }
.odds-band {
  display: flex; justify-content: space-between; align-items: baseline;
  margin: 14px 0 4px; padding-left: 9px;
  border-left: 3px solid transparent;
  font-size: 11px; text-transform: uppercase; letter-spacing: .6px; font-weight: 700;
}
.ob-pct { opacity: .7; }
.odds-table, .tokref-table { width: 100%; border-collapse: collapse; }
.odds-table td, .tokref-table td, .tokref-table th {
  padding: 6px 8px; border-bottom: 1px solid rgba(255, 255, 255, .06);
  font-size: 12.5px; vertical-align: top;
}
.tokref-table th {
  font-size: 10.5px; text-transform: uppercase; letter-spacing: .5px;
  color: var(--muted); text-align: left; font-weight: 700;
}
.od-name { font-weight: 650; }
.od-what { font-size: 11.5px; color: var(--muted); line-height: 1.35; margin-top: 2px; }
.od-pct  { white-space: nowrap; font-weight: 650; }
.od-one  { white-space: nowrap; color: var(--muted); font-size: 11.5px; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ------------------------------------------------- the payout tokens
 *
 * Matt: "make it feel like you earned a TOKEN", with a struck gold coin as the
 * reference — milled rim, ring band with a legend curved around it, recessed
 * face in the middle.
 *
 * The coin is SVG (see coinSvg() in crates.js) because two of those three
 * things are not available to CSS: knurling wants a dashed stroke on a circle,
 * and curved type wants a textPath. What lives here is only the METAL — two
 * colours per tier, read by every layer of the coin — so adding a tier stays
 * two lines rather than a new sprite.
 */
.tok-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(184px, 1fr));
  gap: 14px;
  margin: 8px 0 18px;
}

.tok-sq {
  position: relative;
  aspect-ratio: 1 / 1;
  display: flex; align-items: center; justify-content: center;
  /* No card behind the coin. The coin IS the object — a rounded rectangle
     around it would read as a coin in a box, which is a different feeling and
     not the one asked for. */
  transition: transform .16s ease, filter .16s ease;
  filter: drop-shadow(0 4px 10px rgba(0, 0, 0, .45));
}
.tok-sq:hover { transform: translateY(-3px) scale(1.02); filter: drop-shadow(0 8px 16px rgba(0,0,0,.55)); }

.ts-coin { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* The metal itself is declared once, up with the reel — see the .t-* block
   there. A second copy here was the bug waiting to happen: two palettes that
   agree right up until somebody retunes one of them. */

/* --- the coin's layers ------------------------------------------------- */
.cn-edge  { fill: var(--tok-b); }
/* Knurling. A dashed stroke around the rim IS milling, and it costs one
   attribute where sixty little rectangles would have cost sixty nodes. */
.cn-mill  { fill: none; stroke: var(--tok-a); stroke-width: 4.4; opacity: .95; }
.cn-ring  { fill: var(--tok-a); }
/* The light, off the upper left. Without it the whole thing reads as a flat
   disc of colour rather than as something struck. */
.cn-shine { fill: var(--tok-hi); opacity: .30; }
/* A dark groove between ring and face, so the middle looks recessed rather
   than painted on. */
.cn-groove { fill: color-mix(in srgb, var(--tok-b) 70%, black); }
.cn-face   { fill: color-mix(in srgb, var(--tok-b) 82%, black); }

.cn-legend {
  fill: color-mix(in srgb, var(--tok-b) 55%, black);
  font-size: 7.4px; font-weight: 800; letter-spacing: 1.1px;
  text-transform: uppercase;
  font-family: inherit;
}
.cn-legend-b { font-size: 6.6px; letter-spacing: .9px; }

/* --- what is stamped on the face -------------------------------------- */
.ts-face {
  position: relative; z-index: 1;
  width: 60%;                       /* inside the r=32 recess, with margin */
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; text-align: center;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
}
.ts-name {
  font-size: 12.5px; font-weight: 800; line-height: 1.12;
  color: #fff; text-wrap: balance;
}
.ts-what {
  font-size: 9.5px; line-height: 1.25;
  color: rgba(255, 255, 255, .80);
  /* Clamped so one long description cannot make its coin taller than the rest
     of the row — every coin in the grid is the same object. */
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
.ts-pct {
  font-size: 11.5px; font-weight: 800; margin-top: 1px;
  color: color-mix(in srgb, var(--tok-hi) 85%, white);
}

/* The dialog is wider than the token picker's — it holds a grid, not a list. */
.odds-modal { width: min(920px, 100%); }
.om-head h3 { margin: 0 0 4px; }
.om-head .sm { margin: 0 0 6px; }
.odds-open { margin-bottom: 8px; }
.odds-modal .odds-band { margin: 16px 0 2px; }

/* The filter chips inside the odds dialog. Same chip vocabulary as the two
   markets' category filters, so the control is one someone has already used
   rather than a new one to learn. */
.om-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 10px; }
.om-note { margin: 0 0 4px; }
.odds-btns { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }

/* The SSO explanation, in the signed-out card. Amber rather than red: the
   sign-in did not break, it did not happen, and the difference matters to
   whoever is reading it. */
.sso-note {
  margin: 0 0 12px; padding: 10px 12px;
  border-radius: 9px; border: 1px solid rgba(226, 160, 40, .45);
  background: rgba(226, 160, 40, .12);
  font-size: 13px; line-height: 1.5;
}


/* =========================================================================
   FILTER DROPDOWNS  (assets/js/filterselect.js)

   Rows of chips grew until they wrapped three deep and pushed the results
   below the fold. A chip row costs screen space in proportion to how many
   options EXIST; a dropdown costs it in proportion to how many you have
   CHOSEN, which is the number that should cost something. One button per
   filter, wearing its own count, with its own Clear inside.

   Every colour here is a custom property, so the market's paper, the vault's
   teal, the hatchery's green and the cave's amber all re-tint it without
   knowing this component exists.
   ========================================================================= */
.fsel-bar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }

.fsel {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 12px; border-radius: 9px; cursor: pointer;
  border: 1px solid var(--line); background: var(--panel-2, rgba(255,255,255,.62));
  color: var(--text); font: inherit; font-size: 13px;
  transition: border-color .12s ease, background .12s ease;
}
.fsel:hover, .fsel.is-open { border-color: var(--accent); }
.fsel.is-on { border-color: var(--accent); background: rgba(42,168,126,.16); font-weight: 600; }
.fsel-l { white-space: nowrap; }
/* The count is the whole point of the collapsed state: it says how much this
   filter is doing without opening it. */
.fsel-n { min-width: 18px; height: 18px; line-height: 18px; padding: 0 6px;
  border-radius: 999px; background: var(--accent); color: #fff;
  font-size: 11px; font-weight: 700; text-align: center; }
.fsel-v { font-size: 11.5px; font-weight: 600; color: var(--accent-l, var(--accent));
  white-space: nowrap; }
.fsel-c { font-size: 9px; opacity: .6; }
.fsel:hover .fsel-c { opacity: 1; }

.fsel-panel {
  position: fixed; z-index: 98; max-width: 340px;
  background: var(--panel, #fff); border: 1px solid var(--line);
  border-radius: 11px; box-shadow: 0 18px 44px rgba(0,0,0,.55);
  overflow: hidden;
  /* Three of the four pages set --panel to something translucent so cards sit
     ON the painting. That is right for a card and wrong for a menu: the page
     title bleeding through a list of checkboxes makes both harder to read.
     Blurring what is behind keeps the panel of a piece with the rest of the
     page while still giving the text an opaque-enough bed to sit on. */
  backdrop-filter: blur(14px) saturate(1.1);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
}
.fsel-search { padding: 8px 8px 6px; border-bottom: 1px solid var(--line-2); }
.fsel-search input { width: 100%; box-sizing: border-box; padding: 6px 9px;
  border: 1px solid var(--line); border-radius: 7px; font: inherit; font-size: 12.5px;
  background: var(--panel-2, #fff); color: var(--text); margin: 0; }
.fsel-list { max-height: 300px; overflow-y: auto; padding: 5px; }
.fsel-row { display: flex; align-items: center; gap: 8px; padding: 6px 9px;
  border-radius: 7px; cursor: pointer; font-size: 12.5px; color: var(--text); }
.fsel-row:hover { background: rgba(127,127,127,.14); }
.fsel-row.is-on { background: rgba(42,168,126,.18); font-weight: 600; }
.fsel-row.is-dis { opacity: .4; cursor: default; }
.fsel-row input[type=checkbox] { flex: 0 0 auto; margin: 0; }
.fsel-t { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.fsel-ico { width: 15px; height: 15px; object-fit: contain; flex: 0 0 auto; }
.fsel-row .eli { flex: 0 0 auto; margin: 0; vertical-align: 0; }
.fsel-note { flex: 0 0 auto; font-size: 9.5px; text-transform: uppercase;
  letter-spacing: .06em; color: var(--muted); }
/* The second control a row may carry — the work-level stepper. Sits inside
   the label, so its own clicks are stopped from toggling the checkbox. */
.fsel-lv { flex: 0 0 auto; padding: 1px 4px; border-radius: 5px; font: inherit;
  font-size: 11.5px; font-weight: 700; border: 1px solid var(--line);
  background: var(--panel-2, #fff); color: var(--text); }
.fsel-none { margin: 10px; font-size: 12px; color: var(--muted); }
.fsel-foot { border-top: 1px solid var(--line-2); padding: 6px; }
.fsel-clear { width: 100%; padding: 6px; border: 0; border-radius: 7px;
  background: none; color: var(--muted); font: inherit; font-size: 12px; cursor: pointer; }
.fsel-clear:hover { background: rgba(200,60,60,.14); color: var(--bad, #c0392b); }

/* THE OVERALL CLEAR. Every filter has its own; this empties the lot. Disabled
   rather than hidden when nothing is set, so its position is learnable — a
   control that appears and disappears has to be found again each time. */
.fclear { margin-top: 10px; padding: 6px 14px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line); background: none; color: var(--muted);
  font: inherit; font-size: 12px; }
.fclear:hover:not(:disabled) { border-color: var(--bad, #c0392b); color: var(--bad, #c0392b);
  background: rgba(200,60,60,.10); }
.fclear:disabled { opacity: .4; cursor: default; }

/* Availability and the dropdowns are the same kind of control — both hide
   rows — so they share a line. The checkbox used to live in the search row,
   which put two filters on two lines and made the search box look like it
   owned one of them. */
/* flex-start, not center: when the dropdowns wrap to a second row the
   checkbox would otherwise float to the middle of both, level with nothing. */
.filterline { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px;
  margin-bottom: 12px; }
 /* `1 1 0`, not `1 1 auto`: with auto the bar claims its full content width,
    finds it does not fit beside the checkbox, and wraps the WHOLE bar to a
    second line — which is the layout this change exists to remove. Allowed to
    shrink, it stays on the line and lets its own buttons wrap instead. */
.filterline .fsel-bar { margin-bottom: 0; flex: 1 1 0; min-width: 0; }
.filterline .avail { flex: 0 0 auto; margin: 0; }
