/* Steam Tags Explorer — gamified storefront for Steam tag rarity.
   No build step, no framework: this file and the ES modules under public/js are it. */

:root {
  --ink: #0a0d14;
  --ink-2: #111624;
  --ink-3: #171d2d;
  --line: #26304a;
  --line-soft: #1d2437;
  --text: #e8ecf5;
  --text-dim: #97a2b8;
  --text-faint: #6b7691;
  --gold: #f7ce46;
  --plum: #e457b1;
  --violet: #a763ff;
  --blue: #4b7bff;
  --sky: #4fc3f7;
  --mint: #64f5c8;
  --red: #eb4b4b;
  --common: #aab2c0;
  --tier-color: var(--sky);
  --tier-glow: rgba(79, 195, 247, 0.4);
  --radius: 14px;
  --radius-sm: 9px;
  --shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
  /* Width of the saved panel, and the height it starts below — vault.js keeps the
     height in step with the top bar as the window narrows and it wraps. */
  --vault-w: 380px;
  --topbar-h: 92px;
  /* The page column: an open panel moves it aside, it never makes it wider. */
  --page-max: 1180px;
  font-family: "Inter", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif;
}

* { box-sizing: border-box; }

/* One column, one scroll direction: nothing here is meant to be read by dragging sideways.
   `clip` rather than `hidden`: `hidden` would make the root a scroll container and the sticky
   header would stop sticking. The real overflow sources — game rows, the update strip, the
   header's library block — are fixed below as well, so nothing has to be clipped to fit. */
html, body { margin: 0; padding: 0; overflow-x: clip; }

body {
  background:
    radial-gradient(1200px 700px at 12% -10%, rgba(75, 123, 255, 0.22), transparent 60%),
    radial-gradient(900px 600px at 100% 0%, rgba(228, 87, 177, 0.16), transparent 60%),
    var(--ink);
  color: var(--text);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}

#confetti {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 40;
}

.flash {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 39;
  background: radial-gradient(circle at 50% 45%, var(--tier-glow), transparent 65%);
  opacity: 0;
  transition: opacity 140ms ease-out;
}
.flash.is-on { opacity: 1; }

/* --------------------------------- top bar -------------------------------- */

/* The header is a grid of named areas, not a wrapping flex row. A flex row with
   `margin-left: auto` on the library redrew itself at every width — the library sliding out
   from under the tabs, the row reordering, the tabs stretching to fill what was left — which
   is what dragging a window from landscape to portrait looked like. The areas answer the same
   question at every width: brand and library on the top row, the tabs on one centred row
   under them, and the tabs back up beside the brand as soon as there is room. Nothing moves
   between the two shapes except the tabs, and `vault.js trackTopbar` re-measures the height,
   so the saved panel keeps clearing the header whatever shape it takes. */
.topbar {
  display: grid;
  /* `auto` for the wordmark, `1fr` for the library — not the other way round. A track free to fall
     to 0 hands its space to the block beside it, and with the old crawler row (~427px of buttons)
     that starved the brand to 125px of a 600px header, where the wordmark overflowed its track and
     sat on top of the select beside it. That row is gone — the block is 268px now — but the order
     stays: the brand takes what its name needs and the library wraps its own controls instead. */
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas:
    "brand library"
    "tabs tabs";
  align-items: center;
  justify-items: stretch;
  gap: 12px 24px;
  padding: 14px 26px;
  border-bottom: 1px solid var(--line-soft);
  background: linear-gradient(180deg, rgba(17, 22, 36, 0.92), rgba(10, 13, 20, 0.72));
  position: sticky;
  top: 0;
  z-index: 30;
  backdrop-filter: blur(14px);
}

/* Once there is room for four tabs *and* both blocks beside them, the header is one row again
   — the same three areas, rearranged by the grid instead of by a reflow. The number comes out of
   the row's own arithmetic at 1240: brand 345, the tab strip as the 1fr track (527 there, its four
   labels need 401 and it caps at 620), the library block 268 — its contents 186, being the Code
   link 85, a 10px gap and Sound 90 — plus two 24px gaps and 26px of padding either side. That adds
   up to ~1,114, so a single row fits from there; 1240 keeps a margin rather than seating the row at
   its minimum, where one long tagline or one more button would push the tabs past their labels. */
@media (min-width: 1240px) {
  .topbar {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "brand tabs library";
  }
}

.brand {
  grid-area: brand;
  display: flex;
  align-items: center;
  gap: 12px;
  /* min-content, not 0: the library block on the same row has a wide max-content (its numbers
     and its buttons), and a freely shrinkable brand let that row hand the wordmark 85px of a
     560px header and spill it over the block beside it. The brand holds its own width; the
     numbers and buttons below are what wrap. */
  min-width: min-content;
}
.brand-mark {
  font-size: 26px;
  filter: drop-shadow(0 6px 14px rgba(247, 206, 70, 0.35));
}
.brand-text { display: flex; flex-direction: column; }
.brand-name {
  /* One size that scales with the viewport instead of a breakpoint that swaps it, so the
     wordmark neither dominates a phone nor shrinks into the tagline. */
  font-size: clamp(18px, 2.6vw, 21px);
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  background: linear-gradient(90deg, var(--gold), var(--plum), var(--violet));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.brand-sub { font-size: 12px; color: var(--text-faint); letter-spacing: 0.02em; }

/* The mode switch is a control, not a ruler. It takes the width its four labels need — never
   more than 620px — and centres itself in whatever row the header gives it, rather than being
   pulled across the whole header and leaving four small pills lost in a wide band. Four labels
   are ~401px at their full size, so 620px is room to read, not empty space. A definite `width`
   rather than a `max-width` is what lets the two-rows-of-two layout below 380px resolve its
   percentage flex bases against a box that actually has one. */
.tabs {
  grid-area: tabs;
  justify-self: center;
  width: min(100%, 620px);
  display: flex;
  gap: 6px;
  padding: 5px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgba(10, 13, 20, 0.65);
}
.tab {
  border: 0;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  padding: 9px 18px;
  border-radius: 999px;
  cursor: pointer;
  text-align: center;
  flex: 1 1 auto;
  transition: color 140ms ease, background 140ms ease;
}
.tab:hover { color: var(--text); }
.tab.is-active {
  color: #0b0f18;
  background: linear-gradient(120deg, var(--gold), #ffe08a);
  box-shadow: 0 8px 22px rgba(247, 206, 70, 0.28);
}

/* Two rows of its own, right aligned: the numbers, then the controls under them. On a single
   row the label sitting above the select made the buttons look like they were floating.
   It lives in the grid's own area now rather than pushing itself right with `margin-left:
   auto`, and both its tracks can shrink, so this block is never what makes the page wider
   than the phone showing it. */
.library {
  grid-area: library;
  justify-self: end;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  min-width: 0;
}
/* nowrap while the header is one row, because "updated 7 days ago" breaking in the middle reads
   badly; the two-row header below gives it back so the block can wrap instead of pushing the
   wordmark out of the way. */
.library-meta { margin: 0; font-size: 12.5px; color: var(--text-dim); white-space: nowrap; overflow-wrap: anywhere; }
.library-meta b { color: var(--text); font-weight: 700; }
.library-controls { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
/* One height for everything in that row, so the link and the button share a bottom line. */
.library-controls .btn { height: 36px; }

/* --------------------------------- buttons -------------------------------- */

.btn {
  border: 1px solid var(--line);
  background: var(--ink-3);
  color: var(--text);
  font: inherit;
  font-weight: 650;
  font-size: 14px;
  padding: 11px 18px;
  border-radius: 11px;
  cursor: pointer;
  transition: transform 120ms ease, border-color 120ms ease, background 120ms ease;
}
.btn:hover { border-color: #38466b; transform: translateY(-1px); }
.btn:disabled { opacity: 0.45; cursor: progress; transform: none; }
/* While a panel is working, `setBusy` (ui.js) puts `is-busy` on its section and disables everything
   in it that could start a second job. A button already looks that way; the fields, the dealt tags
   and the paging pills follow it, so the panel greys as one block instead of one control at a time,
   and the wait cursor says why. */
.console.is-busy { cursor: progress; }
.console.is-busy select:disabled,
.console.is-busy input:disabled,
.console.is-busy .chip:disabled,
.console.is-busy .mini-btn:disabled,
.console.is-busy .games-more:disabled { opacity: 0.45; cursor: progress; }
.btn-primary {
  border-color: transparent;
  background: linear-gradient(120deg, #4b7bff, #7d5cff);
  box-shadow: 0 10px 26px rgba(75, 123, 255, 0.35);
}
.btn-ghost { background: transparent; color: var(--text-dim); }
.btn-ghost:hover { color: var(--text); }
.library-controls .btn { padding: 0 14px; font-size: 13px; }
/* The repository link is an anchor in that same row, so it shares the row's height, colour and
   wrapping instead of adding header furniture of its own. */
.repo-link { display: inline-flex; align-items: center; gap: 7px; text-decoration: none; }
.repo-mark { width: 16px; height: 16px; flex: 0 0 auto; }
.btn-mini { padding: 7px 12px; font-size: 12.5px; }
.btn-crate {
  border-color: transparent;
  background: linear-gradient(120deg, var(--gold), #ff9f43);
  color: #1a1405;
  box-shadow: 0 12px 30px rgba(247, 206, 70, 0.35);
  font-weight: 800;
  letter-spacing: 0.03em;
}

/* Any display rule below would otherwise beat the hidden attribute. */
[hidden] { display: none !important; }

/* ---------------------------------- layout -------------------------------- */

main { padding: 24px 26px 40px; max-width: var(--page-max); margin: 0 auto; }
.view[hidden] { display: none; }

/* ---------------------------------- boot ----------------------------------
 * While the catalog is downloading and being indexed, the panels are not merely disabled — they are
 * not shown at all. Every one of them answers out of the index built from that one file in this
 * browser, so a control that looks usable would answer nothing, and the only hint that the page was
 * still starting was one line of small text in the top bar. The tab strip stays in place but stops
 * taking clicks, and this block carries the status instead. main.js removes `is-booting` from <html>
 * when the index is ready; index.html writes the class so the first painted frame is already gated. */
html.is-booting .view, html.is-booting .vault { display: none; }
html.is-booting .tabs { opacity: 0.42; pointer-events: none; }
html:not(.is-booting) .boot { display: none; }

.boot {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: 14px;
  min-height: min(58vh, 460px);
  padding: clamp(26px, 6vh, 64px) 20px;
  text-align: center;
}

.boot-title {
  margin: 0;
  font-size: clamp(19px, 2.4vw, 25px);
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--gold);
}

/* Tabular figures, because the byte count changes in place and a proportional 1 makes the line
   jitter as the download runs. */
.boot-status {
  margin: 0;
  color: var(--text);
  font-size: 15px;
  font-variant-numeric: tabular-nums;
}

.boot-bar {
  width: min(460px, 100%);
  height: 7px;
  border-radius: 999px;
  background: rgba(38, 48, 74, 0.85);
  border: 1px solid var(--line-soft);
  overflow: hidden;
}

.boot-fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--plum), var(--gold));
  transition: width 0.2s ease-out;
}

/* No percentage to show — the host did not send a length, or the bytes are in and the CPU has the
   file now. A bar with no end to it says "working" better than a frozen one at 100%. */
.boot.is-busy .boot-fill {
  width: 38% !important;
  animation: boot-sweep 1.15s ease-in-out infinite;
}

@keyframes boot-sweep {
  from { transform: translateX(-105%); }
  to { transform: translateX(300%); }
}

@media (prefers-reduced-motion: reduce) {
  .boot.is-busy .boot-fill { animation: none; width: 100% !important; }
}

.boot-note {
  margin: 0;
  max-width: 52ch;
  color: var(--text-faint);
  font-size: 13px;
  line-height: 1.55;
}

.boot.is-failed .boot-status { color: var(--red); }
.boot.is-failed .boot-bar { display: none; }

.console {
  background: linear-gradient(160deg, rgba(23, 29, 45, 0.92), rgba(14, 18, 29, 0.86));
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  padding: 20px 22px;
  box-shadow: var(--shadow);
}
.console + .results, .console + .radar { margin-top: 20px; }
.console-head { margin-bottom: 16px; min-width: 0; }
.console-title { margin: 0 0 6px; font-size: 20px; letter-spacing: 0.02em; }
.hint { margin: 0; color: var(--text-dim); font-size: 13.5px; line-height: 1.55; max-width: 78ch; overflow-wrap: anywhere; }
.hint em { color: var(--text); font-style: normal; font-weight: 600; }
.hint strong { color: var(--text); font-weight: 700; }

/* Every panel is the same two columns: the settings on the left, the rarity block on the
   right, at the same place in all four. The left column is one order every time — settings,
   buttons, status line — and both columns can shrink to zero, which is what stops a long word
   in a status line from pushing the block out of the page. Below 1000px the block drops under
   the settings rather than squeezing them. */
.console-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(215px, 24vw, 300px);
  gap: 22px 26px;
  align-items: start;
}
.console-controls { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.console-legends { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
.crate-stage { margin-top: 22px; }

.control-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
/* A field takes its width from the row, not from the option currently showing in it: the
   Rarity list swaps between "Guarantee very common" and "Any rarity", and a field sized by
   its text would shuffle everything next to it every time. */
.field { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: var(--text-faint); flex: 1 1 145px; min-width: 0; max-width: 250px; }
.field-wide { flex: 2 1 235px; min-width: 215px; max-width: 330px; }
.field select, .field input {
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  padding: 9px 11px;
  min-width: 128px;
}
.field input[type="number"] { min-width: 92px; }
.field select:focus, .field input:focus, #tagInput:focus { outline: 2px solid rgba(75, 123, 255, 0.55); outline-offset: 1px; }

.action-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
/* The status line is a slot, not a sentence that comes and goes. It always holds two lines of
   height, so the buttons above it and everything below it stay exactly where they are while a
   search runs, when a number gains a comma, and when a sentence turns into a longer one. */
.stat-line { margin: 0; font-size: 12.5px; color: var(--text-faint); }
.console-controls .stat-line,
.console-controls .draw-note { margin: 0; line-height: 1.35; overflow-wrap: anywhere; }
/* Three lines where a mode writes long sentences — the Finder and Missed opportunities say how
   many combinations, from how many candidates, among how many tags, in how many
   milliseconds — and two where it writes one short line. Enough for the longest thing the panel
   says, so the board below it starts at the same height every time. */
.console-controls .stat-line { min-height: 4.05em; }
.console-crate .stat-line,
.console-controls .draw-note { min-height: 2.7em; }
.console-controls .chips { margin: 0; }
.console-controls .tag-search { max-width: 100%; }

/* The two reference blocks: every rarity band and the exact count it means, in the column
   beside the control that uses it, and every crate and the pool it draws from, above the crate
   field that lists them. A row is one grid of dot, name and number, so a band name takes the
   room it needs and the range keeps a column of its own. Both blocks carry the console's own
   panel styling, so they read as part of the console. */
.legend-host { min-width: 0; }
/* The crate block belongs to one field, and it sits where the choice is made: under the mode
   description, above the settings. It folds, so on a phone it never pushes the button away. */
.legend-host-cases { margin: 0 0 16px; }
.legend-block {
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.025);
  padding: 10px 12px 11px;
  min-width: 0;
}
.legend-title {
  margin: 0;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.legend-note { margin: 2px 0 8px; font-size: 11px; line-height: 1.4; color: var(--text-faint); }
.legend-rows { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* One row is one grid: the dot takes only what a dot takes, the name takes what is left, the
   number keeps a right-aligned column of tabular figures. `minmax(0, 1fr)` on the name is the
   part that stops a band name being squeezed out of existence at a width nobody tested. */
.legend-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
  padding: 2px 0;
  font-size: 12px;
}
.legend-key { color: var(--text); overflow-wrap: anywhere; }
.legend-value {
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}
.legend-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  align-self: center;
}
/* The mode lights the rows of the bands it covers — one row for a guarantee, the half it names
   for a prioritize entry — name and range together, so each mark reads as "this row", not as
   "this word". */
.legend-row.is-on .legend-key,
.legend-row.is-on .legend-value { color: var(--gold); }
.legend-row.is-on .legend-dot { box-shadow: 0 0 0 2px rgba(247, 206, 70, 0.45); }

/* Seven crates read faster as a short grid than as one long column: the columns come out of
   the width on offer, and a row is its glyph and name on one line with the one-line blurb
   under it, indented to the name rather than to the glyph. */
.legend-rows-cases {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr));
  gap: 9px 20px;
}
.legend-row-case { grid-template-columns: auto minmax(0, 1fr); row-gap: 1px; align-items: center; }
.legend-glyph { font-size: 13px; }
.legend-blurb {
  grid-column: 2;
  text-align: left;
  white-space: normal;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-faint);
  overflow-wrap: anywhere;
}

/* A fold for the crate block: the heading is the control, so the block takes no more room
   than the heading plus a chevron once the reader is done comparing crates. The chevron
   points down while the block is open and turns to point left once it is folded away. */
details.legend-block > summary { cursor: pointer; list-style: none; display: flex; align-items: center; gap: 10px; }
details.legend-block > summary::-webkit-details-marker { display: none; }
details.legend-block > summary::after {
  content: "▾";
  margin-left: auto;
  font-size: 11px;
  color: var(--text-faint);
  transition: transform 160ms ease;
}
details.legend-block:not([open]) > summary::after { transform: rotate(90deg); }
details.legend-block > summary:hover { color: var(--text-dim); }
details.legend-block[open] > summary { margin-bottom: 2px; }

/* -------------------------------- tag entry ------------------------------- */

.tag-search { position: relative; max-width: 420px; }
#tagInput {
  width: 100%;
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  font-size: 14.5px;
  padding: 12px 14px;
}
.suggest {
  list-style: none;
  margin: 6px 0 0;
  padding: 4px;
  position: absolute;
  z-index: 20;
  width: 100%;
  background: var(--ink-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  max-height: 260px;
  overflow: auto;
}
.suggest li { padding: 8px 10px; border-radius: 7px; cursor: pointer; font-size: 13.5px; display: flex; justify-content: space-between; gap: 10px; }
.suggest li:hover, .suggest li.is-active { background: rgba(75, 123, 255, 0.18); }
.suggest .count { color: var(--text-faint); font-size: 12px; }

.chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0 4px; min-height: 34px; }
.chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(75, 123, 255, 0.14);
  font-size: 13px;
  font-weight: 600;
}
.chip .num { color: var(--text-faint); font-size: 11.5px; font-weight: 500; }
.chip button {
  border: 0;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
  padding: 0;
}
.chip button:hover { color: var(--red); }
.chip-ghost {
  cursor: pointer;
  background: rgba(255, 255, 255, 0.06);
  border-color: var(--line-soft);
}
/* Hover says “clickable”, picked says “in the tag box”, and the two must never look alike. A touch
   screen puts the tapped element into :hover and keeps it there until the next tap somewhere else,
   so a hover that paints the chip gold leaves the chip you have just taken out of the tag box
   looking picked under your finger — which is why hover styling lives behind (hover: hover) and why
   what it does is an edge and a faint fill, never the picked text colour. */
@media (hover: hover) {
  .chip-ghost:hover { border-color: rgba(247, 206, 70, 0.55); background: rgba(247, 206, 70, 0.07); }
  .chip-ghost.is-picked:hover { background: rgba(247, 206, 70, 0.22); }
}
/* A tag on a card that is already part of the search: clicking it takes it out. */
.chip-ghost.is-picked {
  background: rgba(247, 206, 70, 0.14);
  border-color: rgba(247, 206, 70, 0.55);
  color: var(--gold);
}
.chip-ghost.is-picked .num { color: rgba(247, 206, 70, 0.7); }
.chips-empty { color: var(--text-faint); font-size: 13px; }

/* --------------------------------- results -------------------------------- */

/* The board is one column that cannot be widened by anything inside it — a card with a long
   game title in it included. */
.results { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; }
.results > * { min-width: 0; }
/* The places the page scrolls to when an answer arrives land under the sticky top bar, not behind
   it — `fx.js scrollToInterest` uses `block: 'start'` and this reserves the room. */
.results, .crate-stage, .reveal { scroll-margin-top: calc(var(--topbar-h) + 14px); }
.empty-state { color: var(--text-faint); font-size: 14px; line-height: 1.6; border: 1px dashed var(--line-soft); border-radius: var(--radius); padding: 18px; }
.error-state { color: #ff9b9b; border-color: rgba(235, 75, 75, 0.4); }

.card {
  position: relative;
  background: linear-gradient(150deg, rgba(23, 29, 45, 0.95), rgba(13, 17, 27, 0.9));
  border: 1px solid var(--line-soft);
  border-left: 4px solid var(--tier-color);
  border-radius: var(--radius);
  padding: 16px 18px;
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35);
  animation: cardIn 320ms ease both;
}
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius);
  pointer-events: none;
  box-shadow: inset 0 0 46px var(--tier-glow);
  opacity: 0.55;
}
.card.card-hero { border-width: 1px; border-left-width: 6px; }
@keyframes cardIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

.card-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.tier-badge {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 12px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #0a0d14;
  background: var(--tier-color);
}
.tier-blurb { color: var(--text-dim); font-size: 12.5px; }
.card-count { margin-left: auto; text-align: right; }
.card-count b { font-size: 22px; letter-spacing: 0.01em; }
.card-count span { display: block; font-size: 11.5px; color: var(--text-faint); }

.tagline { display: flex; flex-wrap: wrap; gap: 7px; margin: 12px 0 0; }

/* The card's two values, drawn as bars rather than as two bare numbers: a filled length is
   read at a glance, and two cards side by side compare without anyone doing arithmetic. The
   groove is a bordered channel with quarter marks behind the fill, so a short bar is still
   measured against something instead of being a smudge on a dark card. */
.meters { display: flex; flex-wrap: wrap; gap: 14px 18px; margin-top: 14px; }
.meter { flex: 1 1 190px; min-width: 0; }
.meter-label { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; font-size: 11px; color: var(--text-faint); letter-spacing: 0.05em; text-transform: uppercase; }
.meter-label b { color: var(--text); font-size: 13px; letter-spacing: 0; font-variant-numeric: tabular-nums; }
.meter-track {
  height: 10px;
  margin-top: 6px;
  border: 1px solid var(--line-soft);
  border-radius: 999px;
  background-color: rgba(255, 255, 255, 0.06);
  background-image: repeating-linear-gradient(90deg, transparent 0 calc(25% - 1px), rgba(255, 255, 255, 0.16) calc(25% - 1px) 25%);
  overflow: hidden;
}
.meter-fill {
  display: block;
  height: 100%;
  /* A bar of 0 still shows 3px: the measurement exists, it is just tiny. */
  min-width: 3px;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--tier-color), var(--tier-glow));
  box-shadow: 0 0 12px var(--tier-glow), inset 0 1px 0 rgba(255, 255, 255, 0.35);
  transition: width 320ms ease;
}

.stattrak {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--gold); border: 1px solid rgba(247, 206, 70, 0.45); border-radius: 999px; padding: 4px 10px;
}

.card-actions { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
.mini-btn {
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-dim);
  font: inherit;
  font-size: 12.5px;
  padding: 7px 12px;
  border-radius: 8px;
  cursor: pointer;
}
.mini-btn:hover { color: var(--gold); border-color: rgba(247, 206, 70, 0.5); }

/* `minmax(0, 1fr)` is the point: a grid column asked for with the default `auto` track grows
   to whatever its widest child wants, and a game title on one line is very wide. */
.games { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; margin-top: 14px; }
.game {
  display: flex; align-items: center; gap: 12px;
  padding: 8px 10px;
  border: 1px solid var(--line-soft);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.03);
  text-decoration: none;
  color: inherit;
  transition: border-color 120ms ease, background 120ms ease;
}
.game:hover { border-color: var(--blue); background: rgba(75, 123, 255, 0.1); }
.game img {
  width: 122px; height: 46px; object-fit: cover; border-radius: 6px; flex: none;
  background: linear-gradient(135deg, #2b3552, #1a2237);
}
.game-body { min-width: 0; flex: 1 1 auto; }
.game-name { font-size: 14px; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* A name that has to be read beats a name on one line: the ellipsis is for a desktop where
   there is room to see the rest, and below that the title simply wraps. */
.game-open { flex: none; }
.game-meta { display: flex; gap: 10px; flex-wrap: wrap; font-size: 11.5px; color: var(--text-faint); margin-top: 3px; }
.game-meta .good { color: #8ee0b6; }
.game-flag { border: 1px solid var(--line-soft); border-radius: 999px; padding: 2px 8px; font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase; }
.game-flag.free { color: var(--mint); border-color: rgba(100, 245, 200, 0.4); }
.game-flag.ea { color: var(--gold); border-color: rgba(247, 206, 70, 0.4); }
.game-open { margin-left: auto; font-size: 12px; color: var(--text-dim); white-space: nowrap; }
.no-games { color: var(--text-faint); font-size: 13px; border: 1px dashed var(--line-soft); border-radius: 10px; padding: 12px; }
/* The one control that extends a list — a card's games, a board of combinations in either
   searching mode — and only when entries are being held back. Quieter than the card actions on
   purpose: it stretches a list already open rather than opening a new view. One rule set, because
   `ui.js paintMoreControl` builds all of them and they must not look different from each other. */
.games-more {
  justify-self: start;
  background: transparent;
  border: 1px dashed var(--line-soft);
  border-radius: 999px;
  color: var(--text-faint);
  font: inherit;
  font-size: 12px;
  padding: 5px 12px;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}
.games-more:hover { color: var(--gold); border-color: rgba(247, 206, 70, 0.5); }
.games-more:disabled { cursor: default; opacity: 0.7; }

/* The note above the combinations that sit next to an empty one. It names what the row is;
   nothing has to say that the entries are clickable, because everything on a card is. */
.empty-note { font-size: 12.5px; color: var(--mint); margin-top: 10px; }
.neighbors { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
/* Neighbours are buttons now: clicking one hands that combination's tags to the Finder. The
   styling follows the card's own small buttons, and the band colour on hover says it is part
   of this card, not a link out of it. */
.neighbor {
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--line-soft);
  border-radius: 8px;
  padding: 6px 10px;
  font: inherit;
  font-size: 12px;
  color: var(--text-dim);
  cursor: pointer;
  max-width: 100%;
  overflow-wrap: anywhere;
  transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
}
.neighbor:hover { color: var(--text); border-color: var(--tier-color); background: rgba(255, 255, 255, 0.06); }
.neighbor:focus-visible { outline: 2px solid var(--tier-color); outline-offset: 1px; }
.neighbor b { color: var(--text); }

.variants { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; align-items: center; }

/* ---------------------------------- crate --------------------------------- */

.console-crate { overflow: hidden; }

.crate-stage { position: relative; }
.reel {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: linear-gradient(180deg, #0d1120, #141a2c);
  height: 132px;
}
.reel-strip { display: flex; gap: 10px; padding: 14px 12px; position: relative; z-index: 2; will-change: transform; }
/* Blank travel slots after the last card: same size as a card, nothing drawn. */
.reel-pad { flex: none; width: 168px; height: 100px; border: 0; background: none; }
.reel-card {
  flex: none;
  width: 168px; height: 100px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: linear-gradient(160deg, #1b2236, #121725);
  display: flex; flex-direction: column; justify-content: center; gap: 6px;
  padding: 12px;
  font-size: 13px; font-weight: 650;
  color: var(--text-dim);
  overflow: hidden;
}
.reel-card .reel-count { font-size: 11px; color: var(--text-faint); }
/* What the cards say stays unreadable while the strip runs, and clears on the schedule the
   lid lifts (--unblur-ms, set by reel.js), so the crate opening and the reveal of the tags
   read as one movement. The counts under the names stay readable: they never tell you which
   tags won. */
.reel-card > span:first-child { transition: filter var(--unblur-ms, 260ms) ease-out; }
.reel-strip.is-blurred .reel-card > span:first-child { filter: blur(7px); }
.reel-card.is-win { border-color: var(--tier-color); color: var(--text); box-shadow: inset 0 0 30px var(--tier-glow); }
.reel-needle {
  position: absolute;
  top: 0; bottom: 0;
  left: 50%;
  width: 3px;
  transform: translateX(-50%);
  background: linear-gradient(180deg, var(--gold), transparent);
  box-shadow: 0 0 18px rgba(247, 206, 70, 0.6);
  z-index: 4;
}
/* The edge fades paint the same vertical gradient as the reel, masked sideways,
   so the fade never leaves a colour step or a visible straight edge. */
.reel::before, .reel::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0;
  width: 120px;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(180deg, #0d1120, #141a2c);
}
.reel::before {
  left: 0;
  -webkit-mask-image: linear-gradient(90deg, #000 25%, transparent);
  mask-image: linear-gradient(90deg, #000 25%, transparent);
}
.reel::after {
  right: 0;
  -webkit-mask-image: linear-gradient(270deg, #000 25%, transparent);
  mask-image: linear-gradient(270deg, #000 25%, transparent);
}

/* The crate graphic sits inside the reel window. It is closed and under the cards while
   the reel is empty, sits over the cards while they scroll, and its lid, box and glow
   finish opening exactly when the reel lands, so nothing is left over the revealed tags. */
.crate-art {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  pointer-events: none;
}
.crate-art.is-spinning, .crate-art.is-open { z-index: 3; }
.crate-art .crate-box, .crate-art .crate-lid, .crate-art .crate-glow { position: absolute; }
.crate-art .crate-box {
  left: 50%; bottom: 16px; transform: translateX(-50%);
  width: 116px; height: 62px;
  border: 2px solid var(--gold);
  border-radius: 6px;
  background:
    linear-gradient(90deg,
      transparent 21%, rgba(247, 206, 70, 0.30) 21% 26%,
      transparent 26% 74%, rgba(247, 206, 70, 0.30) 74% 79%, transparent 79%),
    linear-gradient(180deg,
      transparent 47%, rgba(247, 206, 70, 0.18) 48% 52%, transparent 53%),
    linear-gradient(180deg, #2f2715, #16120a);
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(247, 206, 70, 0.25);
}
.crate-art .crate-lid {
  left: 50%; bottom: 74px; transform: translateX(-50%);
  width: 124px; height: 14px;
  border: 2px solid var(--gold);
  border-radius: 5px;
  background: linear-gradient(180deg, #3f3617, #211a0c);
}
.crate-art .crate-lid::after {
  content: "";
  position: absolute;
  left: 50%; top: 100%; transform: translateX(-50%);
  width: 14px; height: 7px;
  border: 2px solid var(--gold);
  border-top: 0;
  border-radius: 0 0 4px 4px;
  background: #1b1608;
}
.crate-art .crate-glow {
  left: 50%; bottom: 4px; transform: translateX(-50%);
  width: 188px; height: 88px;
  background: radial-gradient(ellipse at 50% 62%, rgba(247, 206, 70, 0.24), transparent 70%);
}
.crate-art.is-nudging .crate-box, .crate-art.is-nudging .crate-lid { animation: crateNudge 360ms ease-in-out 2; }
/* The opening is timed in JS: --open-ms spans the last stretch of the spin, and the lid,
   box and glow animations all end when the reel lands on the winning tags. */
.crate-art.is-open .crate-lid { animation: lidUp var(--open-ms, 620ms) cubic-bezier(0.18, 0.9, 0.3, 1) forwards; }
.crate-art.is-open .crate-box { animation: crateFade var(--box-fade-ms, 340ms) ease-out var(--box-fade-delay, 240ms) forwards; }
.crate-art.is-open .crate-glow { animation: crateBurst var(--open-ms, 620ms) ease-out forwards; }
@keyframes crateNudge {
  0%, 100% { transform: translateX(-50%); }
  30% { transform: translateX(-50%) translateY(-3px); }
  65% { transform: translateX(-50%) translateY(2px); }
}
@keyframes lidUp {
  45% { transform: translateX(-50%) translateY(-4px) rotate(-2deg); }
  75% { transform: translateX(-50%) translateY(-22px) rotate(-8deg); opacity: 1; }
  to { transform: translateX(-50%) translateY(-44px) rotate(-15deg); opacity: 0; }
}
@keyframes crateFade { to { opacity: 0; } }
@keyframes crateBurst {
  55% { opacity: 1; transform: translateX(-50%) scale(1.05); }
  to { opacity: 0; transform: translateX(-50%) scale(1.7); }
}

.draw-note { margin-top: 12px; font-size: 13px; color: var(--text-faint); }

.reveal {
  margin-top: 18px;
  border-radius: var(--radius);
  border: 1px solid var(--tier-color);
  background: linear-gradient(160deg, rgba(23, 29, 45, 0.96), rgba(11, 15, 24, 0.92));
  box-shadow: 0 0 60px var(--tier-glow);
  padding: 20px 22px;
  animation: revealPop 480ms cubic-bezier(0.18, 1.3, 0.4, 1) both;
}
@keyframes revealPop { from { opacity: 0; transform: scale(0.9) translateY(14px); } to { opacity: 1; transform: none; } }
.reveal-tier { font-size: 30px; font-weight: 900; letter-spacing: 0.06em; text-transform: uppercase; color: var(--tier-color); text-shadow: 0 0 34px var(--tier-glow); margin-bottom: 16px; }
.reveal .tagline { margin-top: 16px; }
.reveal .chip { background: rgba(255, 255, 255, 0.08); }

.crate-side { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; margin-top: 18px; }
.streak { font-size: 13px; color: var(--text-dim); }
.streak b { color: var(--gold); }
/* Each list under the reel says what it is, in the same voice as the reference blocks: one
   heading style, so a heading means “this is a list of …” wherever the reader meets one. The
   block is the unit, because a heading with nothing under it is worse than no heading. */
.side-block { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.side-title {
  margin: 0;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.side-empty {
  margin: 0;
  padding: 8px 10px;
  border: 1px dashed var(--line-soft);
  border-radius: 10px;
  font-size: 12px;
  color: var(--text-faint);
}
.ladder { display: flex; flex-wrap: wrap; gap: 8px; }
.history { display: grid; gap: 8px; }
/* A draw already made is a combination like any other, so its row carries the same affordance
   the ladder rungs and the saved rows do: one click takes that mix to the Finder and searches
   it. Full width, so the count keeps its place on the right of every row. */
.history-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 12px;
  border: 1px solid var(--line-soft);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.03);
  font: inherit;
  font-size: 12.5px;
  color: var(--text-dim);
  text-align: left;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}
.history-item:hover { color: var(--gold); border-color: rgba(247, 206, 70, 0.5); }
.history-item .dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }

/* --------------------------------- draft ---------------------------------- */

/* Each tag in the pool is a button: click it to disable that tag for this draft. */
.chip-pool {
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: inherit;
  cursor: pointer;
  text-align: left;
}
.chip-pool:hover { border-color: var(--gold); }
.chip-pool .chip-state { font-size: 11px; font-weight: 500; color: var(--text-faint); }
.chip-pool.is-off {
  background: rgba(235, 75, 75, 0.1);
  border-color: rgba(235, 75, 75, 0.4);
  color: var(--text-faint);
}
.chip-pool.is-off > span:first-child { text-decoration: line-through; }
.chip-pool.is-off .chip-state { color: var(--red); }

/* --------------------------------- uncharted ------------------------------ */

.radar { position: relative; height: 0; overflow: hidden; transition: height 420ms ease; }
.radar.is-scanning { height: 210px; border: 1px solid var(--line-soft); border-radius: var(--radius); background: radial-gradient(circle at 50% 50%, rgba(100, 245, 200, 0.08), rgba(10, 13, 20, 0.9) 70%); }
.radar-ring { position: absolute; border: 1px solid rgba(100, 245, 200, 0.25); border-radius: 50%; left: 50%; top: 50%; transform: translate(-50%, -50%); }
.r1 { width: 90px; height: 90px; }
.r2 { width: 160px; height: 160px; }
.r3 { width: 230px; height: 230px; }
.radar-sweep {
  position: absolute; left: 50%; top: 50%; width: 230px; height: 230px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: conic-gradient(from 0deg, rgba(100, 245, 200, 0.45), transparent 28%);
  animation: sweep 1.6s linear infinite;
}
@keyframes sweep { to { transform: translate(-50%, -50%) rotate(360deg); } }
.radar-blip { position: absolute; width: 8px; height: 8px; border-radius: 50%; background: var(--mint); box-shadow: 0 0 12px var(--mint); animation: blip 2.2s ease-in-out infinite; }
.b1 { left: 38%; top: 40%; }
.b2 { left: 60%; top: 58%; animation-delay: 0.7s; }
.b3 { left: 52%; top: 32%; animation-delay: 1.3s; }
@keyframes blip { 0%, 100% { opacity: 0; transform: scale(0.4); } 40% { opacity: 1; transform: scale(1); } 70% { opacity: 0.2; transform: scale(1.2); } }

.saved-when { margin-left: auto; font-size: 11.5px; color: var(--text-faint); white-space: nowrap; }

/* -------------------------------- saved vault ------------------------------ */
/* The one place saved combinations go, whichever mode saved them. Collapsed it is a
   handle on the right edge; open it is a panel that starts under the sticky top bar. */

.vault {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: var(--vault-w);
  z-index: 30;
  pointer-events: none;
}

.vault-rail {
  position: fixed;
  right: 0; top: 50%;
  transform: translateY(-50%);
  pointer-events: auto;
  display: flex; flex-direction: column; align-items: center; gap: 9px;
  padding: 16px 8px;
  border: 1px solid var(--line);
  border-right: 0;
  border-radius: 12px 0 0 12px;
  background: linear-gradient(180deg, #1b2236, #10141f);
  color: var(--text-dim);
  font: inherit; font-size: 11px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase;
  cursor: pointer;
  box-shadow: -8px 0 26px rgba(0, 0, 0, 0.42);
  transition: right 240ms ease, color 160ms ease, border-color 160ms ease;
}
.vault-rail:hover { color: var(--gold); border-color: rgba(247, 206, 70, 0.5); }
.vault-rail-arrow { font-size: 17px; line-height: 1; transition: transform 200ms ease; }
.vault-rail-label { writing-mode: vertical-rl; }
.vault-rail-icon { font-size: 14px; }
.vault-rail-count {
  min-width: 24px;
  padding: 2px 6px;
  border: 1px solid var(--line-soft);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.07);
  color: var(--text);
  font-size: 11px; letter-spacing: 0;
}
.vault.is-open .vault-rail { right: var(--vault-w); }
.vault:not(.is-open) .vault-rail-arrow { transform: rotate(180deg); }

.vault-rail-count.is-bump { animation: vaultBump 720ms ease; }
@keyframes vaultBump {
  0% { transform: scale(1); }
  30% { transform: scale(1.5); background: var(--tier-color); color: #0a0d14; border-color: var(--tier-color); }
  100% { transform: scale(1); }
}
.vault-rail.is-pulse { animation: vaultPulse 950ms ease; }
@keyframes vaultPulse {
  0% { box-shadow: -8px 0 26px rgba(0, 0, 0, 0.42), 0 0 0 0 var(--tier-glow); }
  35% { box-shadow: -8px 0 26px rgba(0, 0, 0, 0.42), 0 0 0 10px var(--tier-glow); border-color: var(--tier-color); color: var(--text); }
  100% { box-shadow: -8px 0 26px rgba(0, 0, 0, 0.42), 0 0 0 24px rgba(0, 0, 0, 0); }
}

/* The words that point at the handle while the saved mix is flying into it. */
.vault-flag {
  position: fixed;
  right: 64px; top: 50%;
  transform: translateY(-50%);
  margin: 0;
  pointer-events: none;
  opacity: 0;
  white-space: nowrap;
  font-size: 11px; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--tier-color);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.85);
  transition: right 240ms ease;
}
.vault-flag.is-on { animation: flagIn 1500ms ease; }
@keyframes flagIn {
  0% { opacity: 0; transform: translateY(-50%) translateX(14px); }
  20% { opacity: 1; transform: translateY(-50%) translateX(0); }
  72% { opacity: 1; }
  100% { opacity: 0; transform: translateY(-50%) translateX(-6px); }
}

.vault-panel {
  position: absolute;
  top: var(--topbar-h); bottom: 10px; right: 0;
  width: var(--vault-w);
  pointer-events: auto;
  display: flex; flex-direction: column;
  border: 1px solid var(--line);
  border-right: 0;
  border-radius: 14px 0 0 14px;
  background: linear-gradient(180deg, rgba(19, 24, 38, 0.98), rgba(11, 14, 22, 0.98));
  box-shadow: -18px 0 44px rgba(0, 0, 0, 0.45);
  animation: vaultIn 240ms ease both;
}
.vault-panel.is-closing { animation: vaultOut 200ms ease both; }
@keyframes vaultIn { from { transform: translateX(100%); opacity: 0.5; } to { transform: none; opacity: 1; } }
@keyframes vaultOut { from { transform: none; opacity: 1; } to { transform: translateX(100%); opacity: 0; } }

.vault-head { display: flex; align-items: center; gap: 10px; padding: 13px 16px 10px; border-bottom: 1px solid var(--line-soft); }
.vault-head h3 { margin: 0; font-size: 13px; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; }
.vault-sub { font-size: 11.5px; color: var(--text-faint); }
.vault-head .btn-ghost { margin-left: auto; }
.vault-tip { margin: 0; padding: 10px 16px 4px; font-size: 12px; line-height: 1.55; color: var(--text-faint); }

.vault-list {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: grid; align-content: start; gap: 10px;
  padding: 10px 14px 18px;
  scrollbar-width: thin;
}
.vault-list:focus-visible { outline: 1px solid var(--blue); outline-offset: -4px; }

.vault-item {
  border: 1px solid var(--line-soft);
  border-left: 3px solid var(--tier-color, var(--line));
  border-radius: 10px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, 0.03);
}
.vault-item.is-flash { animation: vaultFlash 1300ms ease; }
@keyframes vaultFlash {
  0% { background: rgba(255, 255, 255, 0.03); }
  22% { background: rgba(255, 255, 255, 0.09); box-shadow: 0 0 0 1px var(--tier-color), 0 0 22px var(--tier-glow); }
  100% { background: rgba(255, 255, 255, 0.03); }
}
.vault-item.is-going { animation: vaultRowOut 240ms ease both; }
@keyframes vaultRowOut { to { opacity: 0; transform: translateX(26px); } }

.vault-line { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.vault-line .tagline { margin: 0; }
.vault-badge {
  margin-left: auto;
  padding: 3px 9px;
  border: 1px solid var(--line-soft);
  border-radius: 999px;
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--tier-color, var(--text-faint));
  white-space: nowrap;
}
.vault-badge.is-waiting { color: var(--text-faint); }
.vault-meta { margin: 8px 0 0; font-size: 11.5px; line-height: 1.45; color: var(--text-faint); }
.vault-unresolved { color: #ff9b9b; }

.vault-games { display: grid; gap: 6px; margin-top: 10px; }
.vault-games .game { gap: 9px; padding: 5px 7px; }
.vault-games .game img { width: 78px; height: 30px; }
.vault-games .game-name { font-size: 12.5px; }
.vault-games .game-meta { gap: 7px; margin-top: 2px; font-size: 10.5px; }
.vault-games .game-open { display: none; }
.vault-loading { margin: 0; font-size: 11.5px; line-height: 1.5; color: var(--text-faint); }
.vault-none {
  margin: 0;
  padding: 8px 10px;
  border: 1px dashed rgba(100, 245, 200, 0.35);
  border-radius: 8px;
  font-size: 11.5px; line-height: 1.5;
  color: var(--mint);
}

.vault-actions { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 10px; }
.vault-actions .mini-btn { padding: 5px 9px; font-size: 11.5px; }
.vault-actions .vault-del:hover { color: #ff9b9b; border-color: rgba(235, 75, 75, 0.5); }
.vault-empty {
  margin: 4px 0;
  padding: 12px;
  border: 1px dashed var(--line-soft);
  border-radius: 10px;
  font-size: 12.5px; line-height: 1.6;
  color: var(--text-faint);
}

/* The mix itself, on its way to the handle. Above the confetti and the tier flash. */
.vault-fly {
  position: fixed;
  z-index: 45;
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  padding: 8px 10px;
  border: 1px solid var(--tier-color);
  border-radius: 10px;
  background: linear-gradient(150deg, rgba(23, 29, 45, 0.96), rgba(13, 17, 27, 0.94));
  box-shadow: 0 14px 30px rgba(0, 0, 0, 0.5), inset 0 0 26px var(--tier-glow);
  pointer-events: none;
}
.vault-fly-lead {
  font-size: 10px; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--tier-color);
}
.vault-fly-tag {
  padding: 2px 8px;
  border: 1px solid var(--line-soft);
  border-radius: 999px;
  font-size: 11.5px;
  color: var(--text-dim);
}

/* ---------------------------------- footer -------------------------------- */

.foot { border-top: 1px solid var(--line-soft); padding: 20px 26px 34px; max-width: var(--page-max); margin: 0 auto; }
.foot p { margin: 0 0 8px; color: var(--text-faint); font-size: 12.5px; line-height: 1.6; max-width: 90ch; }
.foot a { color: var(--text-dim); }

/* ------------------------------- animations off ---------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
  .radar.is-scanning { height: 60px; }
}

/* ------------------------------ narrow screens ----------------------------
   One ladder, climbing down from the two things that decide when a width is no longer wide
   enough: the reference block beside the settings, and the four tab labels in one row.
   Nothing below changes what a control says or what it looks like — only where it sits. */

/* Under 1000px the rarity block drops below the settings instead of standing beside them, and
   the header keeps its second shape: brand and library on top, the tabs on their own centred
   row underneath. */
@media (max-width: 1000px) {
  .topbar { padding: 12px 16px; gap: 10px 16px; }
  /* The brand keeps its wordmark and the numbers give their line breaks back: in this shape the
     library shares a row with the brand, so wrapping is cheaper than overflowing. */
  .library-meta { white-space: normal; text-align: right; }
  .console-body { grid-template-columns: minmax(0, 1fr); gap: 18px; }
  .console-legends { max-width: 460px; }
  /* With the whole panel width to use, eight bands read better as two columns of four than as
     one long column: the rows are grids already, so only the wrapping changes. The crate block
     has its own, wider columns and is left alone. */
  .legend-block-rarity .legend-rows {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
    gap: 3px 20px;
  }
  main { padding: 18px 14px 34px; }
  .game img { width: 84px; height: 36px; }
  .reel-card { width: 140px; }
}

@media (max-width: 760px) {
  .brand-sub { display: none; }
  .console { padding: 16px 15px; }
  /* The two buttons in a header row share the width they are given instead of each taking its
     own word's worth and overflowing it. */
  .action-row .btn { flex: 1 1 auto; }
}

/* Under 640px two measured things stop fitting at about the same width: four tab labels in one
   pill row (their natural width is ~566px, and the row is 32px narrower than the viewport), and
   the library block beside the wordmark: its row of buttons is ~427px, and by the time the row
   has made room for the wordmark and the gap there is nothing left for the buttons to sit in
   without stacking into three lines of their own. Below this the header becomes three full-width
   rows, which is the same height with nothing overlapping. */
@media (max-width: 640px) {
  .topbar {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "brand"
      "library"
      "tabs";
  }
}

@media (max-width: 640px) {
  /* The pills lose their generous side padding before they lose a row: four labels want ~401px
     at full size, ~322px here, and a phone-width row is worth the second one. */
  .tab { padding: 9px 10px; font-size: 13px; }
}

@media (max-width: 560px) {
  .field, .field-wide { flex: 1 1 100%; max-width: none; }
  .card-head { gap: 6px 10px; }
  .card-count { margin-left: 0; text-align: left; }
  .game img { width: 70px; height: 29px; }
  /* On a phone the image keeps its 2:1 plate, the "store page" hint goes (the whole row is the
     link) and the title wraps, so no row can push the page wider than the viewport. */
  .game-open { display: none; }
  .game-name { white-space: normal; }
  /* The crate block needs nothing here: its columns come from the width on offer, so at this
     width it is simply one crate per row, glyph and name on the first line, blurb under it. */
  .reveal-tier { font-size: 25px; }
  .foot { font-size: 12px; }
}

/* On a wide page an open panel slides the page column aside instead of covering it: the
   column keeps the width it had and centres itself in what is left over, and it only
   shrinks once what is left over is narrower than it. Narrower than this the panel
   overlays, because there is nothing left to squeeze. */
@media (min-width: 1320px) {
  main, .foot { transition: margin 240ms ease; }

  body.vault-open {
    --page-shift: max(26px, calc((100vw - var(--vault-w) - var(--page-max)) / 2));
  }

  body.vault-open main,
  body.vault-open .foot {
    max-width: min(var(--page-max), calc(100vw - var(--vault-w) - 52px));
    margin-left: var(--page-shift);
    margin-right: calc(var(--vault-w) + var(--page-shift));
  }
}

body.vault-open .vault-flag { right: calc(var(--vault-w) + 62px); }

@media (max-width: 1000px) {
  :root { --vault-w: min(88vw, 372px); }
  .vault-rail { padding: 12px 7px; }
  .vault-flag { right: 58px; }
  /* The words that point at the handle: on a phone the open panel is 88vw wide, so the offset
     that puts them beside the handle would put them off the left edge of the screen. They are
     decorative (aria-hidden, no pointer events) and the handle is behind the panel here, so they
     simply do not appear while the panel is open. */
  body.vault-open .vault-flag { display: none; }
}

@media (max-width: 380px) {
  /* The four labels at their narrow size want ~325px inside a row that is the viewport minus the
     header's padding and the pill strip's own padding, so one row stops fitting just under 380px.
     Two rows of two at the same text size, rather than a fifth font size nobody asked for. */
  .tabs { flex-wrap: wrap; border-radius: 16px; }
  .tab { flex: 1 1 calc(50% - 6px); }
}
