/* Pokéwatch - a dealer's terminal.
   Dark ground so the single actionable state carries all the visual weight.
   Foil gradient appears only on the mark and the buy-now edge; everywhere else
   is flat, dense and quiet. Triage first, decoration last.

   Type   11 / 12 / 13 / 15 / 17 / 20 / 22 / 26    weights 400 500 600 700
   Space  4 8 12 16 24 40           radius 8 / 14 / 999          */
:root {
  /* Designed in OKLCH so the steps are perceptually even, then written as hex
     for reach. Ground is cool and faintly blue - research is consistent that
     warm darks read brown and cool darks carry layered depth.

     Gold is the brand and the only call to action. Blue means "later", red
     means "broken", grey means "gone", and plain ink means "fine". Nothing
     needs green, so there is none: it was doing eight jobs and meaning none. */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 40px;
  --r1: 8px;
  --r2: 14px;
  --ground: #0a0c11;
  /* oklch(.155 .010 268) */
  --surface: #15171d;
  /* oklch(.205 .012 268) */
  --raised: #20232a;
  /* oklch(.255 .014 268) */
  --line: #2b2e35;
  /* oklch(.300 .014 268) */
  --line-bright: #43464f;
  /* oklch(.395 .016 268) */
  --ink: #ebedf0;
  /* 15.3:1 on surface */
  --ink-dim: #a6a9b0;
  /*  7.6:1 */
  --ink-faint: #878a92;
  /*  4.6:1 on --raised, 5.2 on --surface, 5.7 on --ground.
     Measured against all three, not just --surface: at #7d8088 this was
     4.53 on --surface and 3.98 on --raised, and --raised is what a table
     row turns when you hover it - so the faint columns in the offers
     table failed the small-text floor exactly while being pointed at. */
  --gold: #e5b147;
  /* brand + the one action     9.1:1 */
  --gold-deep: #ca932e;
  --gold-ink: #1a1405;
  /* text on a gold button      9.4:1 */
  --gold-bg: #1e1a10;
  --gold-line: #4a3d18;
  --blue: #67a4dd;
  /* pre-order / upcoming       6.8:1 */
  --blue-bg: #121a24;
  --blue-line: #2b3f55;
  --red: #df5644;
  /* a check that failed        5.0:1 */
  --red-bg: #241413;
  --red-line: #55291f;
  --gone: #7e8792;
  /* sold out - deliberately neutral */
  --gone-bg: #181b21;
  --gone-line: #2f333b;
  --focus: #e5b147;
  --art-shadow: 0 6px 14px rgb(0 0 0 / 55%);
  --font:
    ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
/* The light theme, warm rather than neutral: Solarized's parchment surfaces with
   a Claude-ish coral doing the job gold does elsewhere. Every token is
   redefined, so nothing inherits a cool value and turns muddy - and each was
   checked against both surfaces (lowest is 5.2:1, well over the 4.5 floor). */
:root[data-theme="warm"] {
  --ground: #f3efe4;
  --surface: #fbf7ec;
  --raised: #ece5d4;
  --line: #ded5c0;
  --line-bright: #bfb49a;
  --ink: #1f1c17;
  /* 15.9:1 on surface */
  --ink-dim: #514b40;
  /*  8.1:1 */
  --ink-faint: #6b6455;
  /*  5.5:1 */
  /* The accent keeps the --gold names so no component has to know the theme. */
  --gold: #a8461f;
  /*  5.5:1 - and 5.9:1 for white on it */
  --gold-deep: #8a3a19;
  --gold-ink: #fffaf3;
  --gold-bg: #f8ece1;
  --gold-line: #e0c3ab;
  --blue: #1f6a93;
  /*  5.5:1 */
  --blue-bg: #e8f1f6;
  --blue-line: #b6d2e1;
  --red: #a32b1d;
  /*  6.7:1 */
  --red-bg: #fbeae7;
  --red-line: #e9b9b1;
  --gone: #6e685c;
  /*  5.2:1 */
  --gone-bg: #f0ece1;
  --gone-line: #ddd6c6;
  --focus: #a8461f;
  --art-shadow: 0 4px 12px rgb(63 51 32 / 22%);
}
* {
  box-sizing: border-box;
}
/* The UA rule for [hidden] is `display: none` at the lowest possible
   specificity, so ANY author `display` beats it and the element stays on
   screen with its `hidden` attribute set. That has shipped visibly broken four
   times here - the search clear button, the region row, the "Available now"
   panel and the chart tooltip. Fix the class of bug, not each instance. */
[hidden] {
  display: none !important;
}
body {
  margin: 0;
  padding: 0 var(--s4) var(--s6);
  background: var(--ground);
  color: var(--ink);
  font: 400 13px/1.5 var(--font);
  -webkit-font-smoothing: antialiased;
}
main {
  max-width: 1180px;
  margin: 0 auto;
}
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r1);
}
a {
  color: inherit;
}
/* ------------------------------------------------------------- masthead */
/* ================================================================== v3 ===
   Sticky header, legible small text, filters that look like filters, and
   table headers that read as a header rather than grey drift. */
/* The bar spans the window; its contents stay on the same 1180px column as
   everything below it. That is why the header sits outside <main> rather than
   fighting its way out with negative viewport margins, which overflow by the
   width of a scrollbar. */
/* Cancel the body's side padding so the bar reaches both window edges; the
     inner column re-applies it, so the contents stay on the same grid as the
     page below. */
.masthead {
  align-items: center;
  gap: var(--s4);
  margin-bottom: var(--s4);
  display: block;
  position: sticky;
  top: 0;
  z-index: 30;
  padding: var(--s3) 0 0;
  margin: 0 calc(-1 * var(--s4)) var(--s4);
  background: var(--ground);
  border-bottom: 1px solid var(--line);
}
.brand {
  display: flex;
  align-items: center;
  gap: var(--s3);
  text-decoration: none;
  color: inherit;
  flex: none;
}
/* No crop and no hairline. Both were there to contain a saturated illustration
   that vibrated against a near-black bar; the mark is now a flat shape on
   transparency and needs neither. Losing the circle is also what lets it read
   larger in the same box - a round crop was cutting the corners off a
   rectangle. */
.mark {
  width: 32px;
  height: 32px;
  flex: none;
}
/* The logo carries the colour; the wordmark stays ink and lets it. Heavier
   and tighter than body text so a five-syllable name still reads as one mark
   at 18px, and the accent keeps its own optical size.
 *
 * Two tones, and which half gets the darker one is the whole idea. "Poké" is
 * borrowed - it is the reason the footer carries a disclaimer - and "watch" is
 * what this thing actually does. Setting the borrowed half a step back makes
 * the eye land on the half that is ours, so the name reads as *watch* rather
 * than as a fan page for somebody else's trademark. It is also the only
 * styling here that a system font stack can do well, which is the constraint:
 * no webfont is loaded, from anywhere, and the CSP would refuse one.
 *
 * 800 rather than 700 because at 18px the extra weight is what separates a
 * wordmark from a bolded sentence, and the tighter tracking closes the gaps
 * that weight opens. */
.wordmark {
  font-size: 18px;
  font-weight: 800;
  letter-spacing: -0.03em;
  color: var(--ink);
  /* The lining figures and tabular widths of the UI stack are wrong for a
     name; this asks for the proportional, text-height forms where they exist
     and is ignored where they do not. */
  font-variant-numeric: normal;
}
.wordmark-borrowed {
  color: var(--ink-dim);
  font-weight: 700;
}
/* The headline is the answer to "why did I open this", so it gets the weight
   that a status badge used to waste, and clicking it filters to those rows. */
.headline {
  display: flex;
  gap: var(--s2);
  flex-wrap: wrap;
}
.stat {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  color: var(--ink-dim);
  font: 500 13px var(--font);
}
.stat strong {
  color: var(--ink);
  display: block;
  margin-top: 2px;
  font: 600 17px var(--mono);
  font-variant-numeric: tabular-nums;
}
.stat.good strong {
  color: var(--gold);
}
.stat.bad strong {
  color: var(--red);
}
/* Pre-order is actionable too, but it is not the same act as buying - a second
   hue keeps the two counts apart without a second row of controls. */
.stat.wait strong {
  color: var(--blue);
}
.stat:hover {
  border-color: var(--line-bright);
}
.stat[aria-pressed="true"] {
  background: var(--raised);
  border-color: var(--line-bright);
}
.utility {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-left: auto;
}
.offline {
  padding: var(--s1) var(--s2);
  border-radius: var(--r1);
  background: var(--blue-bg);
  border: 1px solid var(--blue-line);
  color: var(--blue);
  font: 500 12px var(--font);
}
.icon-btn {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r1);
  color: var(--ink-dim);
  cursor: pointer;
}
.icon-btn svg {
  width: 16px;
  height: 16px;
}
.icon-btn:hover {
  color: var(--ink);
  border-color: var(--line-bright);
}
/* The one control in this row whose payload is letters rather than a glyph.
   Two characters at the icons' own weight, tracked out slightly so "SV" reads
   as a language tag and not as a word that got cut off. Tabular figures are
   not the point; even width between the two codes is, so the row does not
   shift by a pixel when the language changes. */
/* The language pair, sized to sit level with the icon buttons beside it: the
   same 34px height and the same border, so the header reads as one row of
   controls rather than as three unrelated things. */
.lang-switch {
  display: flex;
  align-items: center;
  height: 34px;
  padding: 3px;
  gap: 2px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r1);
}
.lang-opt {
  height: 100%;
  padding: 0 8px;
  border: 0;
  border-radius: calc(var(--r1) - 3px);
  background: transparent;
  color: var(--ink-faint);
  font: 600 11px / 1 var(--font);
  letter-spacing: 0.06em;
  cursor: pointer;
}
.lang-opt:hover {
  color: var(--ink-dim);
}
.lang-opt[aria-pressed="true"] {
  background: var(--raised);
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--line-bright);
  cursor: default;
}
/* The country row sat almost on top of the breadcrumb; it is a control, and a
   control needs room away from the thing that says where you are. */
.filters.secondary {
  margin: 0 0 var(--s6);
}
/* A floor under the content so switching between a six-row table and a
   two-card grid does not collapse the page and yank the scroll position. */
#content {
  min-height: 60vh;
}
.hero-text {
  min-width: 0;
}
.stat span {
  white-space: nowrap;
  display: block;
  color: var(--ink-faint);
  font-size: 12px;
}
/* Under 760px the brand and the tools share the first row and the two
   headline counts drop to a row of their own. The rule used to target
   .masthead, which stopped being the flex row when the header grew its
   two-row structure, so on a phone the counts sat 560px to the right and the
   whole page scrolled sideways to reach them. */
@media (max-width: 760px) {
  .mast-top {
    flex-wrap: wrap;
    gap: var(--s3);
  }
  .mast-top > .headline {
    order: 3;
    width: 100%;
  }
  .mast-top > .utility {
    margin-left: auto;
    gap: 6px;
  }
  /* Brand plus six controls is 364px at desktop sizes and a 390px phone has
     358 to give, so the controls dropped to a row of their own and the header
     was three rows deep before the search box. Two pixels off each control
     and two off each gap is the difference between two rows and three. */
  .utility .icon-btn {
    width: 32px;
    height: 32px;
  }
  .utility .ghost {
    padding-inline: var(--s2);
  }
}
/* The name stays on a phone.
   It used to be dropped below 480px, on the arithmetic that brand plus six
   controls came to 434px against a 390px screen. That arithmetic was true
   when it was written and is not any more: the count chips moved to their own
   row, so the top row is the brand and two buttons. Measured with the
   wordmark shown - 390, 360 and even 320px all keep the same two rows with
   16px to spare.

   It matters more than the space it costs. The logo was the only way back to
   the front page on a phone, and a picture with no word next to it is a
   guess: you have to already know it is a home link to try it. */
/* --------------------------------------------------- available-now rail */
.now {
  margin-bottom: var(--s5);
  padding: var(--s4);
  background: var(--surface);
  border-radius: var(--r2);
  border: 1px solid var(--line);
  position: relative;
  overflow: hidden;
}
.now::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  background: linear-gradient(135deg, var(--gold) 0%, var(--gold-deep) 100%);
}
.now h2 {
  margin: 0 0 var(--s3);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
/* The heading is the control. A bar-wide hit target beats a small chevron,
   and the count stays legible when the cards are folded away. */
.now-head {
  display: flex;
  align-items: center;
  gap: var(--s2);
  width: 100%;
  margin: 0 0 var(--s3);
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-dim);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.now.collapsed .now-head {
  margin-bottom: 0;
}
.now-title {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.now-count {
  margin-left: auto;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
}
.now-count-word {
  letter-spacing: 0.02em;
}
.now-chev {
  width: 16px;
  height: 16px;
  flex: none;
  color: var(--ink-faint);
  transition: transform 0.18s ease;
}
.now.collapsed .now-chev {
  transform: rotate(-90deg);
}
.now-head:hover {
  color: var(--ink);
}
.now-head:hover .now-chev {
  color: var(--gold);
}
.now-head:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: var(--r1);
}
@media (prefers-reduced-motion: reduce) {
  .now-chev {
    transition: none;
  }
}
/* Last alerts sent: a slim strip, because it reports what already happened
   and must never compete with what you can buy right now. */
.recent {
  margin-bottom: var(--s5);
  padding: var(--s4);
  background: var(--surface);
  border-radius: var(--r2);
  border: 1px solid var(--line);
}
.recent-head {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  margin-bottom: var(--s3);
}
.recent-head h2 {
  margin: 0;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.recent-all {
  margin-left: auto;
  font-size: 12px;
  color: var(--ink-faint);
  text-decoration: none;
}
.recent-all:hover {
  color: var(--gold);
}
.recent-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--s3);
}
.recent-card {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) auto;
  gap: var(--s3);
  align-items: center;
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  background: var(--raised);
  color: inherit;
  text-decoration: none;
}
.recent-card:hover {
  border-color: var(--gold-line);
}
.recent-art {
  width: 40px;
  height: 40px;
  object-fit: contain;
  border-radius: var(--r1);
}
.recent-art.noimg {
  display: grid;
  place-items: center;
  color: var(--ink-faint);
  background: var(--surface);
}
.recent-body {
  min-width: 0;
}
.recent-what,
.recent-meta {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.recent-what {
  font-size: 13px;
  color: var(--ink);
}
.recent-meta {
  font-size: 12px;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.recent-when {
  font-size: 12px;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.now-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(228px, 1fr));
  gap: var(--s3);
}
/* A buy card is two controls, not one: the body opens our own comparison for
   that product, the corner button leaves for the shop. Reading first is the
   default because the card only ever shows the cheapest listing, and cheapest
   before postage is not the same as best. */
/* The box keeps the chrome; the grid moves onto the link inside it, which is
   the part that used to be the card. */
.buy-card {
  grid-template-columns: 60px minmax(0, 1fr);
  gap: var(--s3);
  align-items: center;
  background: var(--gold-bg);
  border: 1px solid var(--gold-line);
  border-radius: var(--r1);
  text-decoration: none;
  position: relative;
  padding: 0;
  display: block;
}
.buy-card:hover {
  border-color: var(--gold);
}
.buy-card img,
.buy-card .noimg {
  width: 60px;
  height: 60px;
  flex: none;
  object-fit: contain;
  filter: drop-shadow(0 4px 8px rgb(0 0 0 / 45%));
}
.buy-card .who {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-weight: 600;
  font-size: 13px;
  order: 2;
  margin-top: 2px;
  min-width: 0;
}
/* Two lines, not one: the product type is the word that tells the cards
     apart, and a single line ellipsised it away on every card. */
/* The title is a single truncated line, and the button sits over its right
   end. Reserving the gutter permanently -- rather than only while hovering --
   keeps the ellipsis where it is instead of the text reflowing under the
   cursor the moment the button fades in. */
.buy-card .what {
  margin-top: 1px;
  order: 0;
  /* Two lines' worth whether it needs one or two - the grid row above sizes
     the track, this keeps a one-line title from centring inside it. */
  align-self: start;
  font-size: 12px;
  line-height: 1.3;
  color: var(--ink-dim);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  padding-right: 24px;
}
/* Available-now card: price is the headline, shop is the caption, and the row
   never wraps into the jumble it became once logos and a second currency
   arrived. */
.buy-card .cost {
  margin-top: var(--s1);
  order: 1;
  margin: 0;
  font: 700 17px/1.1 var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.now.quiet::before {
  background: var(--line-bright);
}
.now .nothing {
  color: var(--ink-faint);
  font-size: 13px;
}
/* --------------------------------------------------------------- filters */
/* ------------------------------------------------------ breathing room ---
   The search row, the breadcrumb and the country row were stacked with no
   rhythm, so they read as one undifferentiated block of chrome. */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-bottom: var(--s5);
}
.filters input[type="search"] {
  flex: 1 1 220px;
  min-width: 0;
  padding: var(--s2) var(--s3);
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  font: inherit;
}
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  min-height: 32px;
  background: var(--surface);
  color: var(--ink-dim);
  border: 1px solid var(--line);
  border-radius: 999px;
  font: 500 12px var(--font);
  cursor: pointer;
  white-space: nowrap;
}
.chip[aria-pressed="true"] {
  background: var(--ink);
  color: var(--ground);
  border-color: var(--ink);
}
.chip .n {
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
}
.chip[aria-pressed="true"] .n {
  color: var(--ground);
  opacity: 0.7;
}
.chip .live {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--gold);
}
/* ---------------------------------------------------------------- series */
.series {
  margin-bottom: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  /* `clip` rounds the corners exactly as `hidden` did, but does not turn the
     section into a scroll container - which is what stopped the sticky column
     headers from ever sticking to the viewport. */
  overflow: clip;
  background: var(--surface);
}
.series > summary {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  cursor: pointer;
  list-style: none;
  background: var(--raised);
}
.series > summary::-webkit-details-marker {
  display: none;
}
.series > summary::after {
  content: "";
  margin-left: auto;
  width: 8px;
  height: 8px;
  flex: none;
  border-right: 2px solid var(--ink-faint);
  border-bottom: 2px solid var(--ink-faint);
  transform: rotate(-45deg);
  transition: transform 140ms ease;
}
.series[open] > summary::after {
  transform: rotate(45deg);
}
.series h2 {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.series .meta {
  color: var(--ink-faint);
  font-size: 12px;
}
.series .kind {
  border-top: 1px solid var(--line-bright);
}
.kind + .kind {
  margin-top: var(--s2);
}
.limits {
  display: flex;
  gap: var(--s3);
  margin-left: auto;
}
.limit {
  display: flex;
  align-items: center;
  gap: var(--s2);
}
.limit label {
  color: var(--ink-faint);
  font-size: 12px;
}
.limit input {
  width: 84px;
  padding: var(--s1) var(--s2);
  text-align: right;
  background: var(--ground);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  font: 500 13px var(--mono);
  font-variant-numeric: tabular-nums;
}
.limit input:focus {
  border-color: var(--focus);
}
/* ----------------------------------------------------------------- rows */
table {
  width: 100%;
  border-collapse: collapse;
  /* Fixed layout so a long store name can never shift the numbers, and so the
     async sparklines land in a column that already has its final width. */
  table-layout: fixed;
}
/* The last column was being clipped by the card edge: the fixed widths summed
   past the container once the action column arrived. */
.c-state {
  width: 118px;
}
.c-price {
  width: 104px;
}
.c-spark {
  width: 168px;
}
/* Wide enough for the word, not for the value.
   The cells hold "8m" and "2h", so 70px was generous - but the heading is
   CHECKED plus a sort arrow, which wants 85, and a th spills into its
   neighbour rather than wrapping. The neighbour's background is painted after
   it, so the spill was hidden and the column read as "CHECKE". A heading that
   cannot be read is not a heading. */
.c-age {
  width: 88px;
}
.c-watch {
  width: 62px;
}
/* Table headers: a header should announce the columns, not whisper. */
/* The header has to stay put: fifteen shops is more rows than fit, and a price
   column with no heading above it is a column of anonymous numbers. */
thead th {
  padding: var(--s1) var(--s4) var(--s2);
  text-align: left;
  color: var(--ink-faint);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.04em;
  white-space: nowrap;
  border-bottom: 1px solid var(--line);
}
th.num,
td.num {
  text-align: right;
}
tbody td {
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
tbody tr:last-child td {
  border-bottom: 0;
}
tbody tr:hover {
  background: var(--raised);
}
tbody tr.off {
  opacity: 0.45;
}
.thumb,
.noimg {
  width: 60px;
  height: 60px;
  object-fit: contain;
  display: block;
  filter: drop-shadow(0 4px 8px rgb(0 0 0 / 45%));
}
/* Size and the cancelled shadow only; the plate itself comes from .art-none,
   shared with the release calendar and the ready-to-buy cards. It used to be
   a grey square with a literal "-" in it. */
.noimg {
  filter: none;
  border-radius: var(--r1);
}
.noimg svg {
  width: 22px;
  height: 22px;
}
.store {
  font-weight: 600;
  font-size: 13px;
}
.store a {
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.store a:hover {
  border-bottom-color: var(--gold);
  color: var(--gold);
}
.state {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: 5px 10px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  background: var(--gone-bg);
  color: var(--gone);
  border: 1px solid var(--gone-line);
}
.state::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: currentColor;
}
.state.buy {
  background: var(--gold-bg);
  color: var(--gold);
  border-color: var(--gold-line);
}
.state.pre {
  background: var(--blue-bg);
  color: var(--blue);
  border-color: var(--blue-line);
}
.state.fail {
  background: var(--red-bg);
  color: var(--red);
  border-color: var(--red-line);
}
td.num {
  font: 600 15px/1.2 var(--mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.amount {
  display: block;
}
.cheap {
  color: var(--gold);
  font-weight: 600;
}
.dear {
  color: var(--ink-faint);
}
.age {
  color: var(--ink-faint);
  font-size: 12px;
  white-space: nowrap;
}
.spark {
  display: block;
  width: 100%;
  height: 36px;
}
.spark path {
  fill: none;
  stroke-width: 1.8;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.spark path.live,
.spark circle.live {
  fill: none;
  stroke: var(--ink-dim);
}
.spark circle.live {
  fill: var(--ink-dim);
}
.spark path.dead,
.spark circle.dead {
  stroke: var(--ink-faint);
}
.spark circle.dead {
  fill: var(--ink-faint);
}
.spark .flat {
  stroke: var(--line-bright);
  stroke-width: 1.5;
  stroke-dasharray: 3 4;
}
.switch {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  cursor: pointer;
}
.switch input {
  width: 18px;
  height: 18px;
  accent-color: var(--gold);
}
.nothing-found {
  padding: var(--s6);
  text-align: center;
  color: var(--ink-faint);
}
/* Shown when a deep link outranks the filter chips, so the page explains
   itself rather than quietly disagreeing with the controls above it. */
.filter-relaxed {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--s2);
  margin: 0 0 var(--s3);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--raised);
  color: var(--ink-dim);
  font-size: 0.85rem;
}
.filter-relaxed .clear-filters {
  border: 1px solid var(--line);
  border-radius: var(--r1);
  padding: 0.25rem 0.6rem;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
}
.filter-relaxed .clear-filters:hover {
  border-color: var(--gold-line);
  color: var(--gold);
}
@media (max-width: 760px) {
  thead {
    display: none;
  }
  tbody td {
    display: inline-block;
    border: 0;
    padding: var(--s1) var(--s4);
  }
  tbody td[data-col="store"] {
    display: block;
    padding-top: var(--s3);
  }
  tbody td[data-col="ship"] {
    display: block;
  }
  tbody td[data-col="toggle"] {
    display: block;
    padding-bottom: var(--s3);
    border-bottom: 1px solid var(--line);
  }
  td.num {
    text-align: left;
  }
  .limits {
    margin-left: 0;
    width: 100%;
  }
  .verdict {
    width: 100%;
    margin-left: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  * {
    transition: none !important;
    animation: none !important;
  }
}
/* ------------------------------------------------------------- typeahead */
.finder {
  position: relative;
  flex: 1 1 260px;
  min-width: 0;
  display: flex;
  align-items: center;
}
.finder-icon {
  position: absolute;
  left: 11px;
  width: 15px;
  height: 15px;
  color: var(--ink-faint);
  pointer-events: none;
}
.finder input {
  width: 100%;
  padding: var(--s2) 34px var(--s2) 34px;
  min-height: 34px;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  font: inherit;
}
.finder input:focus {
  border-color: var(--focus);
}
/* No `outline: none` here.
   The border colour alone was the entire focus signal for the search field,
   which is one of the two controls somebody tabbing hits first - and a
   1px colour change on a dark bar is not a focus indicator. Everything else
   on the page uses :focus-visible with a real ring; so does this now. */
.finder input:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Off-screen until it is focused, then the first thing on the page. */
.skip-link {
  position: absolute;
  left: var(--s4);
  top: -100px;
  z-index: 100;
  padding: var(--s2) var(--s3);
  border-radius: var(--r1);
  background: var(--gold);
  color: var(--gold-ink);
  font-weight: 600;
  text-decoration: none;
  transition: top 0.12s ease;
}
/* :focus, not :focus-visible - a skip link exists for whatever gave it focus,
   and a browser that decides the focus was not "visible" would leave it
   off-screen while it holds the keyboard. */
.skip-link:focus {
  top: var(--s2);
}
@media (prefers-reduced-motion: reduce) {
  .skip-link {
    transition: none;
  }
}

/* The masthead is sticky, so an anchor jump or a focus move used to land the
   target underneath it. Enough margin to clear the bar at its tallest. */
:target,
[id] {
  scroll-margin-top: 96px;
}
.finder-clear {
  position: absolute;
  right: 8px;
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  background: transparent;
  color: var(--ink-faint);
  border: 0;
  border-radius: 999px;
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}
.finder-clear:hover {
  color: var(--ink);
  background: var(--raised);
}
.suggestions {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 40;
  max-height: 60vh;
  overflow-y: auto;
  padding: var(--s2);
  background: var(--surface);
  border: 1px solid var(--line-bright);
  border-radius: var(--r2);
  box-shadow: 0 12px 40px rgb(0 0 0 / 45%);
}
.sugg-head {
  margin: var(--s2) 0 var(--s1);
  padding: 0 var(--s2);
  color: var(--ink-faint);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.sugg {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2);
  border-radius: var(--r1);
  cursor: pointer;
}
.sugg.on,
.sugg:hover {
  background: var(--raised);
}
.sugg img,
.sugg-blank {
  width: 44px;
  height: 44px;
  flex: none;
  object-fit: contain;
  filter: drop-shadow(0 3px 6px rgb(0 0 0 / 40%));
}
.sugg-blank {
  background: var(--raised);
  border: 1px solid var(--line);
}
.sugg-body {
  min-width: 0;
  flex: 1;
}
.sugg-title {
  display: block;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sugg-note {
  display: block;
  color: var(--ink-dim);
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sugg-right {
  text-align: right;
  flex: none;
}
.sugg-price {
  display: block;
  color: var(--gold);
  font: 600 13px var(--mono);
  font-variant-numeric: tabular-nums;
}
.sugg-sub {
  display: block;
  color: var(--ink-faint);
  font-size: 12px;
}
.no-hits {
  margin: 0;
  padding: var(--s3);
  color: var(--ink-faint);
}
/* --------------------------------------------------------- history detail */
.spark-open {
  display: block;
  width: 100%;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: var(--r1);
  cursor: pointer;
}
.spark-open:hover {
  background: var(--raised);
}
dialog.detail {
  width: min(720px, 94vw);
  padding: 0;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-bright);
  border-radius: var(--r2);
}
dialog.detail::backdrop {
  background: rgb(0 0 0 / 62%);
}
.detail-head {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s4);
  border-bottom: 1px solid var(--line);
}
.detail-head h2 {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
}
.detail-head p {
  margin: 2px 0 0;
  color: var(--ink-dim);
  font-size: 12px;
}
.detail-close {
  margin-left: auto;
  padding: var(--s1) var(--s3);
  background: transparent;
  color: var(--ink-dim);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  cursor: pointer;
  font: 500 12px var(--font);
}
.detail-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
  gap: var(--s3);
  padding: var(--s4);
  border-bottom: 1px solid var(--line);
}
.detail-chart {
  padding: var(--s4);
}
/* ------------------------------------------------------- detail chart v2 */
.detail-chart svg {
  width: 100%;
  display: block;
  height: 230px;
}
.detail-chart .line {
  fill: none;
  stroke: var(--gold);
  stroke-width: 2;
}
.detail-chart .band {
  fill: var(--gold);
  opacity: 0.5;
}
.detail-chart .grid {
  stroke: var(--line);
  stroke-width: 1;
}
.detail-chart .tick {
  fill: var(--ink-faint);
  font-size: 11px;
}
.detail-note {
  margin: 0;
  padding: 0 var(--s4) var(--s4);
  color: var(--ink-faint);
  font-size: 12px;
}
.ship {
  display: inline-block;
  color: var(--ink-dim);
  font-size: 13px;
  white-space: nowrap;
}
.ship.free {
  color: var(--ink);
  font-weight: 600;
}
.ship.none {
  color: var(--ink-faint);
}
/* ----------------------------------------------------------- breadcrumbs */
.crumbs {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  /* Holds its line whatever it contains: picking a filter adds a crumb, and a
     row that grows from nothing shoves the whole page down. */
  min-height: 28px;
  min-height: 22px;
  margin: 0 0 var(--s4);
  font-size: 13px;
}
.crumbs[hidden] {
  display: none;
}
.crumb {
  padding: 0;
  background: none;
  border: 0;
  color: var(--ink-dim);
  font: 500 13px var(--font);
  cursor: pointer;
}
.crumb:hover {
  color: var(--ink);
  text-decoration: underline;
}
.crumb.current {
  color: var(--ink);
  cursor: default;
}
.crumb-sep {
  color: var(--ink-faint);
}
/* A setting currently applied, with the way to take it off attached to it.
   Used both in the breadcrumb trail and beside the browse row, so the two
   read as the same kind of thing. */
.drop {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: 3px 8px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--raised);
  color: var(--ink);
}
.drop span {
  color: var(--ink-faint);
  font-size: 14px;
  line-height: 1;
}
.drop:hover {
  border-color: var(--gold-line);
  color: var(--gold);
  text-decoration: none;
}
.drop:hover span {
  color: var(--gold);
}
.drop {
  cursor: pointer;
  font: 500 12px var(--font);
  white-space: nowrap;
}
/* Sits at the end of the row, and only when there is more than one thing to
   put back - one chip is its own reset. */
.drop-all {
  border-style: dashed;
  color: var(--ink-dim);
}
.sort-row .drop {
  margin-left: 0;
}
.premium {
  display: block;
  margin-top: 2px;
  min-height: 15px;
  color: var(--blue);
  font: 500 12px var(--font);
  font-variant-numeric: tabular-nums;
}
.premium.empty {
  visibility: hidden;
}
.premium.heavy {
  color: var(--red);
}
/* inline-flex, not flex: as a block it filled the store column, which is the
   widest one on the page, so the gold underline on .store a:hover appeared
   while the pointer was still a hundred pixels to the right of the shop's
   name. A line under a name you are not pointing at reads as the page
   twitching. The phone rules already used inline-flex for the tap target;
   this makes the desktop agree with them.

   max-width so the ellipsis still has something to clip against. */
.store-line {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
  max-width: 100%;
}
.store-line a {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.store-mark {
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: 6px;
  object-fit: contain;
  background: #fff;
  padding: 2px;
}
.store-mark.blank {
  display: grid;
  place-items: center;
  background: var(--raised);
  color: var(--ink-dim);
  font: 600 13px var(--font);
  padding: 0;
  border: 1px solid var(--line);
}
.detail-chart .series {
  fill: none;
  stroke-width: 2.2;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.detail-chart .series.live {
  stroke: var(--ink-dim);
}
.detail-chart .series.dead {
  stroke: var(--ink-faint);
}
.detail-chart .limit {
  stroke: var(--blue);
  stroke-width: 1.5;
  stroke-dasharray: 6 4;
}
.detail-chart .limit-label {
  fill: var(--blue);
  font-size: 11px;
  font-weight: 600;
}
.detail-chart .low-mark {
  fill: var(--gold);
  stroke: var(--surface);
  stroke-width: 2;
}
.detail-chart .rail {
  fill: var(--gold);
  opacity: 0.55;
}
.detail-chart .rail-label {
  fill: var(--ink-faint);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
/* Four rows, whether or not there is something to put in each.
   As a flex column the rows stacked from the top, so a title that wrapped to
   two lines pushed its price and shop name down while its neighbour's stayed
   up - and a card with no native-currency line ended shorter than the one
   beside it. Across a grid that reads as cards of different sizes rather than
   as titles of different lengths. Explicit track sizes hold the shape even
   when the last row is empty, which is what an empty grid row is for. */
.buy-body {
  min-width: 0;
  display: grid;
  grid-template-rows: 2.6em auto auto 1.25em;
  align-content: start;
  gap: 1px;
}
.buy-card .cost small {
  margin-left: var(--s2);
  color: var(--ink-faint);
  font: 400 12px var(--font);
}
.buy-card .who-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.buy-card .who small {
  flex: none;
  margin-left: var(--s1);
  padding: 1px var(--s1);
  border-radius: 4px;
  background: var(--surface);
  color: var(--ink-faint);
  font: 500 11px var(--font);
}
.buy-card .who .store-mark {
  width: 20px;
  height: 20px;
}
.buy-card .sek {
  order: 3;
}
.cur {
  margin-left: 3px;
  font-size: 0.7em;
  font-weight: 500;
  color: var(--ink-faint);
  letter-spacing: 0.03em;
}
.sek {
  display: block;
  margin-top: 1px;
  color: var(--ink-faint);
  font: 400 11px var(--font);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* The set wordmark identifies a section faster than its name does, but the
   name stays for search, screen readers and any set without artwork. */
.set-mark {
  height: 40px;
  width: auto;
  max-width: 150px;
  flex: none;
  object-fit: contain;
  filter: drop-shadow(0 2px 4px rgb(0 0 0 / 45%));
}
/* ------------------------------------------------- table, second pass ----
   Taken from a price-comparison table that does this well: a large round shop
   mark, the price as the loudest thing in the row, availability as a status
   line rather than a capsule, and one unmistakable action per row. */
.c-buy {
  width: 86px;
}
tbody td {
  padding: var(--s3) var(--s4);
}
tbody tr {
  transition: background-color 120ms ease;
}
.store-mark {
  width: 34px;
  height: 34px;
  border-radius: 999px;
  background: #fff;
  padding: 2px;
}
.store-mark.blank {
  background: var(--raised);
  font-size: 13px;
}
.store-line {
  gap: var(--s3);
}
.store-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Country mark: a flag people recognise, plus the code, never colour alone. */
.flag {
  color: var(--ink-dim);
  font: 600 11px var(--font);
  letter-spacing: 0.04em;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: none;
  padding: 2px 6px 2px 3px;
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: 999px;
}
td.num .amount {
  display: block;
  font-size: 17px;
  color: var(--ink);
}
/* Reserved status colour: only a price you could actually act on. Never
   colour alone - these rows also carry the tick in the availability column. */
td.num .amount.actionable {
  color: var(--gold);
}
td.num .amount.muted {
  color: var(--ink-faint);
}
/* Magnitude mark: one neutral hue, anchored at the group's cheapest. */
/* The price cell opens that listing's price history. It was reachable only
   through the sparkline button, which does not exist on a phone. */
.listings td[data-col="price"] {
  cursor: pointer;
}
.pbar {
  display: block;
  width: 62px;
  height: 4px;
  margin: 6px 0 0 auto;
  border-radius: 2px;
  background: var(--line-bright);
  overflow: hidden;
}
.pbar > span {
  display: block;
  width: var(--w, 0%);
  height: 100%;
  border-radius: 2px;
  background: var(--ink-dim);
}
.over {
  display: block;
  margin-top: 3px;
  color: var(--ink-faint);
  font: 500 11px var(--font);
  font-variant-numeric: tabular-nums;
}
.best {
  display: block;
  margin-top: 3px;
  font: 600 11px var(--font);
  letter-spacing: 0.02em;
  color: var(--ink-dim);
}
.over.muted {
  font-weight: 400;
  color: var(--ink-faint);
  opacity: 1;
}
.avail {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  color: var(--gone);
}
.avail svg {
  width: 14px;
  height: 14px;
  flex: none;
}
/* ============================================================ accent diet ==
   Gold had crept onto eight things per screen - the price, the tick, the
   word "Buy now", the button, the sparkline dot, the header count, the mark
   and the avatar. One hue repeated over a grey field is what a colourblind
   simulation looks like, which is exactly how it read.

   In stock is the *normal* state and wears no colour. Gold now appears once
   per row, on the single thing worth clicking. */
/* The accent diet was right that gold on eight unrelated things means nothing.
   It was wrong that "in stock is the normal state": in a stock watcher it is
   the exceptional one - five rows of seventeen here, usually fewer - and it is
   the only reason to open the page. So the accent comes back, but only here,
   and it never carries the meaning alone: the badge has its own shape and
   ground, so it still reads when the hue does not. */
/* The deep step, not the base one: base gold on the tinted ground is 4.2:1
     in the light theme, under the 4.5 floor. Deep clears it in all three. */
.avail.buy {
  padding: 3px 9px 3px 7px;
  border-radius: 999px;
  background: var(--gold-bg);
  border: 1px solid var(--gold-line);
  color: var(--gold-deep);
  font-weight: 600;
}
.avail.pre {
  color: var(--blue);
}
.avail.fail {
  color: var(--red);
}
.buy-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  width: 100%;
  min-height: 34px;
  padding: 0 var(--s3);
  border-radius: var(--r1);
  background: var(--raised);
  border: 1px solid var(--line-bright);
  color: var(--ink-dim);
  font: 600 13px var(--font);
  text-decoration: none;
}
/* A table with a cart shortcut in it gives the action column room for two
   controls; every other table keeps the single-button width. */
.drop tr.drop-buy td, .drop tr.drop-pre td {
  background: color-mix(in srgb, var(--gold) 12%, transparent);
}
.has-cart .c-buy {
  width: 150px;
}
/* On a shared row the word is the button; the outward-arrow glyph is what
   pushed "View" out of its own box. */
.buy-group .buy-btn svg {
  display: none;
}
/* Buy and the cart shortcut share one row of the column, never two. */
.buy-group {
  display: flex;
  align-items: stretch;
  gap: 6px;
  width: 100%;
}
.buy-group .buy-btn {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
}
.cart-btn {
  flex: 0 0 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  min-height: 34px;
  border-radius: var(--r1);
  background: var(--raised);
  border: 1px solid var(--line-bright);
  color: var(--ink-dim);
  text-decoration: none;
}
.cart-btn svg {
  width: 15px;
  height: 15px;
}
.cart-btn:hover {
  border-color: var(--ink-dim);
  color: var(--ink);
}
.cart-btn.live {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--gold-ink);
}
.cart-btn.live:hover {
  filter: brightness(1.1);
  color: var(--gold-ink);
}

.limit-badge {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  font-size: 0.72em;
  font-weight: 600;
  background: #8a6200;
  color: #fff;
  white-space: nowrap;
}

.buy-btn svg {
  width: 11px;
  height: 11px;
}
.buy-btn:hover {
  border-color: var(--ink-dim);
  color: var(--ink);
}
.buy-btn.live {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--gold-ink);
}
.buy-btn.live:hover {
  filter: brightness(1.1);
  color: var(--gold-ink);
}
/* Named apart from the filter chip, which was also called .drop.
 *
 * Two unrelated components on one class name, and the chip is declared first
 * and wins the parts this one does not set: 3px 8px of padding, a 1px border,
 * a 999px radius and a raised background. So a price badge in the narrowest
 * column on the page was being drawn as a pill wide enough to burst it.
 * Renaming is the fix; anything else is arithmetic against a rule that was
 * never meant to apply. */
.price-drop {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-top: 3px;
  color: var(--gold);
  font: 600 12px var(--font);
  font-variant-numeric: tabular-nums;
  /* The column is as wide as a price. If the pair will not fit, the struck
     price is the half worth losing - the percentage is the news. */
  flex-wrap: wrap;
  justify-content: flex-end;
}
.price-drop s {
  color: var(--ink-faint);
  font-weight: 400;
  text-decoration: line-through;
}
@media (max-width: 760px) {
  tbody td[data-col="buy"] {
    display: block;
    padding-top: var(--s2);
  }
  .buy-btn {
    width: auto;
  }
}
/* ------------------------------------------------------------- set browse */
.set-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: var(--s3);
}
.set-card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  overflow: hidden;
}
.set-card.has-stock {
  border-color: var(--gold-line);
}
.set-card:hover {
  border-color: var(--line-bright);
}
.set-card.has-stock:hover {
  border-color: var(--gold);
}
.set-open {
  display: grid;
  grid-template-rows: auto auto auto;
  text-decoration: none;
  color: inherit;
  padding: var(--s5) var(--s4) var(--s4);
  gap: var(--s3);
}
/* The single home for wordmark sizing. There used to be a second block further
   down that quietly won, so editing this one did nothing. */
.set-art {
  display: grid;
  place-items: center;
  height: 148px;
  margin-bottom: var(--s3);
}
.set-art img {
  max-height: 148px;
  max-width: 100%;
  object-fit: contain;
  /* Tokenised: a heavy black shadow reads as depth on a near-black ground and
     as grime on parchment, so each theme sets its own. */
  filter: drop-shadow(var(--art-shadow));
}
.set-art.product {
  height: 160px;
}
.set-art.product img {
  max-height: 160px;
}
/* On a phone 160px of artwork plus the card's own padding meant one product
   filled the screen, so choosing between six of them was six screens of
   scrolling. Scoped to the product variant deliberately: a bare .set-art img
   would be out-specified by .set-art.art-pack img anyway, and the pack shots
   on the home page are not the problem. */
@media (max-width: 560px) {
  .set-art.product {
    height: 104px;
  }
  .set-art.product img {
    max-height: 104px;
  }
}
/* A pack shot is a photographed box, not a flat wordmark: it needs no drop
   shadow of its own and reads better a little smaller than a full-bleed mark. */
.set-art.art-pack img {
  max-height: 132px;
  filter: none;
}
.set-name-fallback {
  color: var(--ink-dim);
  font: 700 17px var(--font);
  text-align: center;
}
.set-meta strong {
  display: block;
  font-weight: 700;
  font-size: 17px;
}
.set-sub {
  display: block;
  margin-top: 1px;
  color: var(--ink-faint);
  font-size: 12px;
}
.set-stat {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s2);
  padding-top: var(--s2);
  border-top: 1px solid var(--line);
}
.set-from {
  font: 600 13px var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
}
.fav {
  position: absolute;
  top: var(--s2);
  right: var(--s2);
  z-index: 2;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  padding: 0;
  background: var(--ground);
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--ink-faint);
  cursor: pointer;
}
.fav svg {
  width: 15px;
  height: 15px;
}
.fav:hover {
  color: var(--blue);
  border-color: var(--blue-line);
}
.fav.on {
  color: var(--blue);
  border-color: var(--blue-line);
}
/* ------------------------------------------------------------------ auth */
.account {
  display: flex;
  align-items: center;
  gap: var(--s2);
}
/* A crumb that navigates looks like the button beside it; only its behaviour
   differs. */
a.crumb {
  text-decoration: none;
}
.signin-body {
  padding: var(--s4);
  display: grid;
  gap: var(--s3);
}
.signin-body label {
  display: grid;
  gap: var(--s1);
  color: var(--ink-dim);
  font-size: 12px;
}
.signin-body input {
  padding: var(--s2) var(--s3);
  min-height: 36px;
  background: var(--ground);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  font: inherit;
}
.signin-error {
  margin: 0;
  color: var(--red);
  font-size: 12px;
}
.signin-foot {
  display: flex;
  justify-content: flex-end;
  gap: var(--s2);
  padding: 0 var(--s4) var(--s4);
}
.primary {
  padding: var(--s2) var(--s4);
  min-height: 36px;
  background: var(--gold);
  border: 1px solid var(--gold);
  border-radius: var(--r1);
  color: var(--gold-ink);
  font: 600 13px var(--font);
  cursor: pointer;
}
/* ------------------------------------------------------------ market trend */
.trend {
  margin-bottom: var(--s4);
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r2);
}
.trend:has(> table) {
  overflow-x: auto;
}
.trend td {
  overflow-wrap: anywhere;
}
.trend-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s3);
  margin-bottom: var(--s3);
}
.trend h3 {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.trend-sub {
  color: var(--ink-faint);
  font-size: 12px;
}
.trend-stats {
  display: flex;
  gap: var(--s5);
  margin-left: auto;
}
.trend-stats small {
  display: block;
  color: var(--ink-faint);
  font-size: 11px;
  letter-spacing: 0.04em;
}
.trend-stats strong {
  font: 600 17px var(--mono);
  font-variant-numeric: tabular-nums;
}
.delta.down strong {
  color: var(--gold);
}
.delta.up strong {
  color: var(--red);
}
/* The old rule pinned a 130px height against a 900-unit viewBox, which is why
   it carried preserveAspectRatio="none" - and why the line thickened and the
   text stretched at wide widths. Let it scale uniformly instead. */
.trend-plot {
  position: relative;
}
.trend svg {
  width: 100%;
  height: auto;
  display: block;
  overflow: visible;
}
.trend-line {
  fill: none;
  stroke-width: 2;
  stroke-linejoin: round;
  stroke: var(--gold);
}
.trend-area {
  fill: var(--gold);
  opacity: 0.08;
}
.trend .grid {
  stroke: var(--line);
  stroke-width: 1;
}
.trend .limit {
  stroke: var(--blue);
  stroke-width: 1.4;
  stroke-dasharray: 6 4;
}
/* No font-size here on purpose.
   marketTrend() works the size out from the width and then lays the gutter,
   the height and the baselines out around it, so the two cannot drift apart -
   which is what happened when the stylesheet tripled the type on a phone and
   moved none of the geometry. It arrives as a font-size presentation
   attribute on the <svg> and inherits: an attribute rather than a style
   attribute, because the CSP here forbids inline styles, and a CSS rule would
   win over it and put us back where we started. */
.trend .tick {
  fill: var(--ink-faint);
  font-family: var(--mono);
}
.trend .limit-tag {
  fill: var(--blue);
  /* No font-size: it inherits the one marketTrend() computed, like the ticks,
     so the tag and the axis stay the same size as each other at every width. */
  letter-spacing: 0.04em;
}
.trend .dot {
  fill: var(--gold);
  /* A 2px ring in the panel colour so touching marks stay two marks. */
  stroke: var(--surface);
  stroke-width: 2;
}
.trend .hit {
  fill: transparent;
  cursor: crosshair;
}
.trend .cross {
  stroke: var(--line-bright);
  stroke-width: 1;
  stroke-dasharray: 3 3;
}
.trend .cross-dot {
  fill: var(--gold);
  stroke: var(--surface);
  stroke-width: 2.5;
}
.trend-tip {
  position: absolute;
  z-index: 2;
  display: grid;
  gap: 2px;
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line-bright);
  border-radius: 10px;
  background: var(--raised);
  box-shadow: 0 8px 20px rgb(0 0 0 / 28%);
  pointer-events: none;
  white-space: nowrap;
  font-size: 12px;
  color: var(--ink);
}
.trend-tip strong {
  font: 600 12px var(--font);
  letter-spacing: 0.04em;
  color: var(--ink-dim);
}
.trend-tip .muted {
  color: var(--ink-faint);
  font-size: 12px;
}
/* Two marks means a legend, always - identity is never colour alone. */
.trend-legend {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin: var(--s3) 0 0;
  color: var(--ink-faint);
  font-size: 12px;
}
.trend-legend .key {
  width: 16px;
  height: 3px;
  border-radius: 2px;
  background: var(--gold);
}
.trend-legend .key.band {
  height: 10px;
  margin-left: var(--s3);
  opacity: 0.22;
}
.trend.empty .trend-note {
  margin: 0;
  color: var(--ink-faint);
  font-size: 13px;
}
/* -------------------------------------------------- restored & new bits --
   `.ghost` was lost when the masthead block was rewritten; it dresses the
   sign-in, sign-out and cancel buttons, so those went unstyled. */
.ghost {
  padding: var(--s2) var(--s3);
  min-height: 34px;
  background: transparent;
  color: var(--ink-dim);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  font: 500 12px var(--font);
  cursor: pointer;
}
.ghost:hover {
  border-color: var(--line-bright);
  color: var(--ink);
}
/* ------------------------------------------------------------ product hero */
/* --------------------------------------------------- product hero, v2 ----
   Was a picture, a title and a lot of nothing. The numbers are the point, so
   they get size, and dividers so the eye knows where one ends. */
.product-hero {
  display: grid;
  gap: var(--s4);
  align-items: center;
  background: color-mix(in srgb, var(--raised) 55%, transparent);
  border-bottom: 1px solid var(--line);
  grid-template-columns: 132px minmax(0, 1fr) auto;
  padding: var(--s5) var(--s4);
}
.hero-shot {
  object-fit: contain;
  filter: drop-shadow(0 8px 14px rgb(0 0 0 / 50%));
  width: 132px;
  height: 132px;
}
.hero-shot.noimg {
  border-radius: var(--r2);
  background: var(--ground);
  border: 1px solid var(--line);
}
.hero-set {
  margin: 0;
  color: var(--ink-faint);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
/* Qualified with the tag: .series h2 outranked the bare class, so the product
   name rendered at 15px under figures set at 22. */
h2.hero-name {
  margin: 2px 0 var(--s3);
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.015em;
}
.hero-stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s5);
}
.hero-stats small {
  display: block;
  color: var(--ink-faint);
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: 3px;
}
.hero-stats strong {
  font: 600 17px var(--mono);
  font-variant-numeric: tabular-nums;
}
/* The thresholds are controls, so they look like a control panel rather than
   two inputs adrift in whitespace. */
.hero-controls {
  justify-self: end;
  text-align: right;
  background: var(--ground);
  border: 1px solid var(--line);
  border-radius: var(--r2);
}
.hero-controls-title {
  margin: 0 0 var(--s2);
  color: var(--ink-dim);
  font-size: 12px;
  text-transform: uppercase;
}
.hero-controls .limits {
  justify-content: flex-end;
}
.hero-hint {
  margin: var(--s2) 0 0;
  color: var(--ink-faint);
  font-size: 11px;
}
.spark .since {
  fill: var(--ink-faint);
  font-size: 11px;
  opacity: 0.75;
  letter-spacing: 0.02em;
  display: none;
}
@media (max-width: 860px) {
  .product-hero {
    grid-template-columns: 84px minmax(0, 1fr);
    gap: var(--s3);
  }
  .hero-shot {
    width: 84px;
    height: 84px;
  }
  .hero-controls {
    grid-column: 1 / -1;
    justify-self: stretch;
    text-align: left;
  }
  .hero-controls .limits {
    justify-content: flex-start;
  }
}
.product-hero {
  gap: var(--s5);
}
.hero-set {
  font-size: 11px;
  letter-spacing: 0.1em;
}
h2.hero-name {
  font-size: 22px;
  margin: var(--s1) 0 var(--s4);
}
/* "Tracked since 19 Aug 2026" belongs to the name above it, not to the
   CHEAPEST / US MARKET / IN STOCK row below - which is where it sat, close
   enough to read as a fifth stat with a missing figure. Only when the caption
   is actually there. */
h2.hero-name:has(+ .hero-since) {
  margin-bottom: var(--s1);
}
.hero-stats {
  gap: 0;
}
.hero-stats > span {
  padding: 0 var(--s5) 0 0;
  margin-right: var(--s5);
  border-right: 1px solid var(--line);
}
.hero-stats > span:last-child {
  border-right: 0;
  margin-right: 0;
  padding-right: 0;
}
.hero-stats small {
  font-size: 12px;
  letter-spacing: 0.06em;
}
.hero-stats strong {
  font-size: 22px;
}
.hero-controls {
  padding: var(--s3) var(--s4);
}
.hero-controls-title {
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
}
.hero-controls .limit input {
  width: 96px;
  font-size: 15px;
  min-height: 34px;
}
.hero-controls .limit label {
  font-size: 12px;
}
/* ------------------------------------------------------------- identity -- */
.identity {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: 3px var(--s3) 3px 3px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  text-decoration: none;
  color: var(--ink);
}
.identity:hover {
  border-color: var(--line-bright);
}
.identity-name {
  font: 600 13px var(--font);
}
.avatar {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: 999px;
  background: linear-gradient(135deg, var(--gold) 0%, var(--gold-deep) 100%);
  font: 700 13px var(--font);
  color: var(--gold-ink);
}
.avatar.big {
  width: 56px;
  height: 56px;
  font-size: 22px;
}
.icon-btn.health {
  position: relative;
  color: var(--red);
  border-color: var(--red-line);
}
/* Red is for outages. When every failure is the catalogue drifting from a
   shop's page title - which is the usual case and nobody's emergency - the
   count is still shown, in the quiet grey that "sold out" uses, and the page
   stops looking like something is on fire. */
.icon-btn.health.soft {
  color: var(--ink-dim);
  border-color: var(--line);
}
.icon-btn.health.soft span {
  background: var(--gone);
  color: var(--ground);
}
.icon-btn.health span {
  position: absolute;
  top: -5px;
  right: -5px;
  min-width: 16px;
  padding: 0 3px;
  border-radius: 999px;
  font: 700 11px var(--font);
  line-height: 16px;
  background: var(--red);
  color: var(--ground);
}
/* -------------------------------------------------------------- profile -- */
.profile {
  display: grid;
  gap: var(--s4);
}
.panel {
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r2);
}
.panel h3 {
  margin: 0 0 var(--s3);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.identity-card {
  display: flex;
  align-items: center;
  gap: var(--s4);
}
.identity-card h2 {
  margin: 0 0 var(--s1);
  font-size: 20px;
}
.identity-card .trend-note {
  margin: 0;
  max-width: 46ch;
}
.identity-card button {
  margin-left: auto;
}
.fav-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s2);
}
.fav-list a {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  text-decoration: none;
}
.fav-list a:hover {
  border-color: var(--line-bright);
}
.fav-list img {
  height: 34px;
  width: auto;
  max-width: 90px;
  object-fit: contain;
}
.fav-list small {
  margin-left: auto;
  color: var(--ink-faint);
  font-size: 12px;
}
/* The owner's own pages. Same shape as the favourites list so the profile
   reads as one page rather than as a settings screen with an admin bolted on;
   these used to be three links in the public footer. */
.owner-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s2);
}
.owner-links a {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  text-decoration: none;
}
.owner-links a:hover {
  border-color: var(--line-bright);
}
.owner-links small {
  margin-left: auto;
  color: var(--ink-faint);
  font-size: 12px;
  text-align: right;
}
@media (max-width: 460px) {
  /* On a phone the note wraps under the title rather than fighting it for
     the same line. */
  .owner-links a {
    flex-direction: column;
    gap: 2px;
  }
  .owner-links small {
    margin-left: 0;
    text-align: left;
  }
}
/* The inner column has to be 1180px of CONTENT, not 1180px including its own
   padding - main is 1180px wide inside body's 16px gutter, so a max-width of
   1180 here left the masthead's contents 32px narrower and everything in the
   bar sitting 16px further in than the page under it. The brand started at
   146 where the page started at 130; on a phone the search field ran to the
   screen edge with its rounded corners clipped while the cards were inset.
   The comment above says the intent was for them to share a column. */
.masthead > * {
  max-width: calc(1180px + 2 * var(--s4));
  margin-inline: auto;
  padding-inline: var(--s4);
}
.masthead > .brand,
.masthead > .headline,
.masthead > .utility {
  display: inline-flex;
  vertical-align: middle;
}
.mast-top {
  display: flex;
  align-items: center;
  gap: var(--s4);
}
.mast-tools {
  display: flex;
  align-items: center;
  gap: var(--s3);
  /* padding-block, not the shorthand: `padding: var(--s3) 0` zeroed the
     inline padding that .masthead > * had just set, which is why the nav and
     the search sat on a different left edge from the brand above them. */
  padding-block: var(--s3);
  flex-wrap: wrap;
}
.mast-tools .finder {
  flex: 1 1 320px;
}
/* A group of pills reads as decoration; a segmented control reads as a
   choice. The selected one is filled, the rest share one enclosure. */
.segmented {
  display: inline-flex;
  padding: 3px;
  gap: 2px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
}
.segmented .chip {
  border: 0;
  background: transparent;
  border-radius: 999px;
  min-height: 30px;
  padding: var(--s1) var(--s3);
  color: var(--ink-dim);
}
.segmented .chip:hover {
  color: var(--ink);
}
.segmented .chip[aria-pressed="true"] {
  background: var(--raised);
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--line-bright);
}
.region-row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s5);
  /* The chips are wider than a phone. The row scrolls, not the page. */
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
}
.region-row::-webkit-scrollbar {
  display: none;
}
.region-row > * {
  flex: none;
  scroll-snap-align: start;
}
/* A strip that scrolls has to look like it does: the last visible chip fades
   at the edge instead of being cut. The mask is only applied where the strip
   can actually overflow, so on a wide screen the chips render at full ink. */
@media (max-width: 760px) {
  .region-row {
    mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent);
    -webkit-mask-image: linear-gradient(
      to right,
      #000 calc(100% - 40px),
      transparent
    );
    padding-right: var(--s5);
  }
}
.region-label {
  color: var(--ink-faint);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
thead th {
  padding: var(--s2) var(--s4);
  color: var(--ink-dim);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  background: var(--raised);
  border-bottom: 1px solid var(--line-bright);
}
/* The sortable headers put their label inside a <button>, and a button does
   not take the text-transform from its cell - so five of the six read "Shop",
   "Price", "Shipping" while the one plain header read "HISTORY", in the same
   row. The rest of the site sets its small labels in caps (REGION, CHEAPEST,
   US MARKET), so all six do now. */
thead th button {
  text-transform: inherit;
  letter-spacing: inherit;
}
thead th:first-child {
  border-top-left-radius: var(--r1);
}
thead th:last-child {
  border-top-right-radius: var(--r1);
}
/* Small text had drifted to unreadable: 7px captions, faint greys. Anything
   a person is meant to read is now at least 12px and clears 4.5:1. */
.age,
.set-sub,
.ship,
.over,
.trend-sub,
.trend-note,
.hero-hint {
  font-size: 12px;
  color: var(--ink-dim);
}
.flag svg {
  width: 16px;
  height: 12px;
  border-radius: 2px;
  display: block;
}
.flag > span {
  color: var(--ink-dim);
  font: 600 11px var(--font);
  letter-spacing: 0.05em;
}
/* --------------------------------------------------------------- fixes --- */
.segmented .chip[aria-pressed="true"] .n {
  color: var(--ink-dim);
  opacity: 1;
}
.c-ship {
  width: 104px;
}
.c-store {
  width: auto;
  min-width: 170px;
}
tbody td,
thead th {
  padding-left: var(--s3);
  padding-right: var(--s3);
}
thead th:first-child,
tbody td:first-child {
  padding-left: var(--s4);
}
thead th:last-child,
tbody td:last-child {
  padding-right: var(--s4);
}
/* Only where it is actually needed. At desktop widths the table fits, and an
   always-on scroll container costs the sticky header. */
@media (max-width: 900px) {
  .series {
    overflow-x: auto;
  }
  thead th {
    position: static;
  }
  /* Between the card reflow at 760 and the full table at 900, the row was
     wider than the card that holds it and the Buy button - the only thing in
     the row you can actually click - was sliced by the right edge. Measured at
     834px, the width of an iPad in portrait: 802px available against a row
     asking for 922, so 120px over, and more for an owner because a watch
     column joins it.

     These five columns give the width back. The sparkline is the biggest
     donation and the one that costs least: it is a shape, not a number, and
     the same history is a click away. 788px now, which clears 834 and 900. */
  .c-spark {
    width: 92px;
  }
  .c-ship {
    width: 84px;
  }
  .c-state {
    width: 104px;
  }
  /* Not .c-age: the heading is the same seven letters at every width, so
     narrowing this column only clips it sooner. The store column absorbs the
     difference either way - it is the one with no width of its own. */
  .c-watch {
    width: 52px;
  }
}
.avail.buy svg {
  color: var(--gold-deep);
}
/* A stripe down the row, so the buyable ones are findable without reading a
   single word of the availability column. */
tbody tr.in-stock td:first-child {
  box-shadow: inset 3px 0 0 var(--gold-deep);
}
tbody tr.under-limit td:first-child {
  box-shadow: inset 3px 0 0 var(--gold);
}
tbody tr.in-stock {
  background: var(--gold-bg);
}
tbody tr.in-stock:hover {
  background: var(--gold-bg);
  filter: brightness(1.04);
}
/* Sold out recedes a step further, so the contrast between the two is carried
   by more than the badge. */
tbody tr:not(.in-stock) td.num .amount {
  color: var(--ink-dim);
}
td.num .amount.actionable {
  color: var(--ink);
}
/* Zebra striping: seventeen rows and eight columns is a long way for the eye
   to travel, and a faint band keeps a price attached to its shop. Kept below
   the hover so hovering still reads as the stronger signal. */
tbody tr:nth-child(even) {
  background: color-mix(in srgb, var(--raised) 42%, transparent);
}
tbody tr:hover {
  background: var(--raised);
}
tbody td {
  border-bottom: 0;
}
tbody tr {
  border-bottom: 1px solid color-mix(in srgb, var(--line) 55%, transparent);
}
tbody tr:last-child {
  border-bottom: 0;
}
/* A flag inside a chip has no pill of its own. */
.chip .flag {
  padding: 0;
  background: none;
  border: 0;
  margin-right: 2px;
}
/* A shop result wears its own mark, at the size of a product thumbnail. */
.sugg .store-mark {
  width: 34px;
  height: 34px;
  border-radius: 8px;
  font-size: 15px;
}
.sugg-note .flag {
  vertical-align: middle;
}
.sugg-note .dot {
  margin: 0 var(--s2);
  color: var(--ink-faint);
}
/* ------------------------------------------------------------- releases */
.rel {
  display: flex;
  flex-direction: column;
  gap: var(--s6);
}
.rel-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s5);
  flex-wrap: wrap;
}
.rel-head h2 {
  margin: 0;
  font: 600 22px var(--font);
  letter-spacing: -0.01em;
  color: var(--ink);
}
.rel-sub {
  margin: var(--s2) 0 0;
  color: var(--ink-dim);
  font-size: 13px;
}
/* The one thing worth acting on gets the only saturated surface on the page. */
.rel-next {
  display: grid;
  gap: 2px;
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line-bright);
  border-radius: 12px;
  background: var(--surface);
  text-decoration: none;
  min-width: 220px;
}
.rel-next:hover {
  border-color: var(--gold-deep);
}
.rel-next-label {
  font: 600 11px var(--font);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--gold);
}
.rel-next strong {
  font: 600 15px var(--font);
  color: var(--ink);
}
.rel-next-date {
  font: 400 12px var(--mono);
  color: var(--ink-faint);
}
/* ------ rail ------ */
.rail-wrap {
  position: relative;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
}
/* Fade the cut edges so a card sliced by the frame reads as "more this way"
   rather than as a rendering bug. */
.rail-wrap::before,
.rail-wrap::after {
  content: "";
  position: absolute;
  top: 1px;
  bottom: 1px;
  width: 48px;
  pointer-events: none;
  z-index: 1;
}
.rail-wrap::before {
  left: 1px;
  border-radius: 14px 0 0 14px;
  background: linear-gradient(to right, var(--surface), transparent);
}
.rail-wrap::after {
  right: 1px;
  border-radius: 0 14px 14px 0;
  background: linear-gradient(to left, var(--surface), transparent);
}
/* The strip had month dividers and a today line but no axis, so the dates read
   as a property of each card rather than as positions along a timeline. The
   rule is painted on the scroll container itself, with background-attachment
   local so it travels the full scrollable width rather than stopping at the
   edge of the viewport; each card then drops a tick onto it, directly above
   its own date. Cards are evenly spaced rather than time-proportional, so
   this is an ordered axis, not a scaled one, and the ticks sit at the cards. */
.rail {
  display: flex;
  align-items: stretch;
  gap: var(--s3);
  list-style: none;
  margin: 0;
  padding: var(--s4);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
  --rail-axis: calc(var(--s3) + 104px + var(--s2));
}
/* Each item carries its own segment, widened by the flex gap so the segments
   meet. Painting the rule on the scroll container instead looks right until
   you scroll: a percentage background resolves against the visible width, not
   the scrollable one, so the axis simply stopped after the first screenful. */
.rail > li {
  position: relative;
}
.rail > li::before {
  content: "";
  position: absolute;
  left: 0;
  top: var(--rail-axis);
  width: calc(100% + var(--s3));
  height: 1px;
  background: var(--line-bright);
}
.rail > li:last-child::before {
  width: 100%;
}
.rail-card {
  flex: 0 0 176px;
  scroll-snap-align: center;
  position: relative;
}
.rail-card::after {
  content: "";
  position: absolute;
  left: 50%;
  top: var(--rail-axis);
  width: 1px;
  height: 7px;
  background: var(--gold-deep);
  transform: translate(-50%, -3px);
}
.rail-card.past::after {
  background: var(--line-bright);
}
.rail-card a {
  display: grid;
  gap: var(--s1);
  height: 100%;
  padding: var(--s3);
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--ground);
  text-decoration: none;
}
.rail-card a:hover {
  border-color: var(--gold-deep);
}
.rail-art {
  display: grid;
  place-items: center;
  height: 104px;
  margin-bottom: var(--s2);
}
.rail-art img {
  max-height: 104px;
  max-width: 100%;
  object-fit: contain;
  filter: drop-shadow(var(--art-shadow));
}
/* A pack shot is a photographed box and wants less shadow than a flat mark. */
.rail-art.art-pack img {
  max-height: 96px;
  filter: none;
}
/* No artwork for something we do not stock. A deliberate plate beats an empty
   frame, and it names the kind rather than repeating the title below it.
 *
 * One rule for all three places a release is drawn - the rail, the timeline
 * and the front page's next-up card - because a release with no picture used
 * to look like three different kinds of missing: a plate here, nothing at all
 * in the timeline, and an empty dashed box on the card. */
.art-none {
  display: grid;
  place-items: center;
  gap: var(--s1);
  border-radius: 10px;
  background:
    repeating-linear-gradient(
      135deg,
      var(--raised) 0 6px,
      transparent 6px 12px
    ),
    var(--surface);
  border: 1px solid var(--line);
  color: var(--ink-faint);
}
.art-none svg {
  width: 26px;
  height: 26px;
}
.art-kind {
  padding: 2px 10px;
  border-radius: 999px;
  background: var(--ground);
  color: var(--ink-faint);
  font: 600 11px var(--font);
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
/* The word only where there is room for it. In a 56px slot it would either
   overflow the plate or shrink to something nobody can read, and the glyph
   already says "a box we have no picture of". */
.tl-logo.art-none .art-kind,
.next-up-art.art-none .art-kind {
  display: none;
}
.tl-logo.art-none svg {
  width: 24px;
  height: 24px;
}
.next-up-art.art-none svg {
  width: 20px;
  height: 20px;
}
.rail-date {
  font: 500 11px var(--mono);
  color: var(--gold);
  letter-spacing: 0.02em;
}
.rail-name {
  font: 600 13px/1.35 var(--font);
  color: var(--ink);
}
.rail-foot {
  font-size: 12px;
  color: var(--ink-faint);
}
.rail-card.past a {
  background: transparent;
}
/* Past releases are dimmed, not drained. Eleven of thirteen entries here have
   already shipped, so desaturating them turned almost the whole calendar
   grey; the date and the "out" label already say what is behind us. */
.rail-card.past .rail-art img,
/* The plate recedes with everything else. Without this it was the one thing
   at full strength in a row of dimmed cards, so the release we have no
   picture of was the one the eye went to first. */
.rail-card.past .rail-art.art-none {
  opacity: 0.78;
}
.rail-card.past .rail-date {
  color: var(--ink-faint);
}
.rail-card.past .rail-name {
  color: var(--ink-dim);
}
/* Month and today are rails, not cards: thin, full height, unclickable. */
.rail-month,
.rail-now {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  padding: 0 var(--s1);
}
.rail-month span,
.rail-now span {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font: 600 11px var(--font);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.rail-month {
  border-left: 1px solid var(--line);
}
.rail-now {
  border-left: 2px solid var(--red);
}
.rail-now span {
  color: var(--red);
}
/* ------ vertical timeline ------ */
.tl {
  list-style: none;
  margin: 0;
  padding: 0 0 0 var(--s5);
  position: relative;
}
.tl::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 6px;
  bottom: 6px;
  width: 1px;
  background: var(--line);
}
.tl-month {
  margin: var(--s5) 0 var(--s3);
  font: 600 11px var(--font);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.tl-month:first-child {
  margin-top: 0;
}
.tl-item {
  position: relative;
  margin-bottom: var(--s4);
  scroll-margin-top: 140px;
}
.tl-dot {
  position: absolute;
  left: calc(var(--s5) * -1);
  top: 18px;
  width: 9px;
  height: 9px;
  margin-left: 0;
  border-radius: 999px;
  background: var(--gold);
  box-shadow: 0 0 0 3px var(--ground);
}
.tl-item.past .tl-dot {
  background: var(--line-bright);
}
.tl-card {
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface);
  padding: var(--s4);
}
.tl-item.past .tl-card {
  background: transparent;
}
.tl-top {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
}
.tl-logo {
  width: 56px;
  height: 56px;
  object-fit: contain;
  flex: none;
}
.tl-item.past .tl-logo {
  opacity: 0.8;
}
.tl-title h3 {
  margin: 0;
  font: 600 17px var(--font);
  color: var(--ink);
}
.tl-title h3 a {
  color: inherit;
  text-decoration: none;
}
.tl-title h3 a:hover {
  color: var(--gold);
}
.tl-meta {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  margin: var(--s2) 0 0;
  font-size: 12px;
}
.tl-date {
  font: 500 12px var(--mono);
  color: var(--ink-dim);
}
.tl-kind,
.tl-guess,
.tl-watch {
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid var(--line-bright);
  color: var(--ink-faint);
  font: 500 11px var(--font);
}
.tl-guess {
  border-style: dashed;
}
/* No gold. The stylesheet's own note says gold "is the brand and the only
   call to action", and this chip is neither: it marks a set the site follows,
   which is a fact about the catalogue and not about the reader or anything
   they can do. In gold beside a grey "Expansion" chip it read as a status
   somebody had earned. It keeps the neutral chip styling above. */
.tl-summary {
  margin: var(--s3) 0 0;
  color: var(--ink-dim);
  font-size: 13px;
  line-height: 1.6;
  max-width: 70ch;
}
.tl-waves {
  margin-top: var(--s4);
  border-top: 1px solid var(--line);
  padding-top: var(--s3);
  display: grid;
  gap: var(--s2);
}
.wave-row {
  display: grid;
  grid-template-columns: 110px 1fr;
  gap: var(--s3);
  align-items: baseline;
}
.wave-date {
  font: 500 12px var(--mono);
  color: var(--ink-faint);
}
.wave-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}
.wave-list li {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: 6px;
  font-size: 12px;
  color: var(--ink-dim);
}
.wave-usd {
  font: 500 11px var(--mono);
  color: var(--ink-faint);
}
.rel-source {
  border-top: 1px solid var(--line);
  padding-top: var(--s3);
  color: var(--ink-faint);
  font-size: 12px;
}
.rel-source p {
  margin: 0 0 var(--s2);
}
.rel-source a {
  color: var(--ink-dim);
}
.rel-source-date {
  font-family: var(--mono);
}
.rel-source-note {
  max-width: 80ch;
  line-height: 1.6;
}
@media (max-width: 760px) {
  .wave-row {
    grid-template-columns: 1fr;
  }
}
/* ---------------------------------------------------------------- results */
.results {
  display: flex;
  flex-direction: column;
  gap: var(--s4);
}
.results-head h2 {
  margin: 0;
  font: 600 22px var(--font);
  letter-spacing: -0.01em;
  color: var(--ink);
}
.results-sub {
  margin: var(--s2) 0 0;
  color: var(--ink-dim);
  font-size: 13px;
}
.results-kind {
  margin: var(--s4) 0 0;
  font: 600 11px var(--font);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.shop-result {
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface);
  overflow: hidden;
}
/* No inline padding. The block padding is what separates the header from its
   own hairline; the 16px on the sides pushed the logo and the title inwards
   while that hairline, every set heading below it and every listing card
   stayed on the page's left edge. A set page is the control and it is flush. */
.shop-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding-block: var(--s3);
  border-bottom: 1px solid var(--line);
}
.shop-head strong {
  display: block;
  font: 600 15px var(--font);
  color: var(--ink);
}
.shop-sub {
  font-size: 12px;
  color: var(--ink-faint);
}
.shop-head .flag {
  margin-left: auto;
}
.shop-table {
  width: 100%;
  border-collapse: collapse;
}
.shop-table td {
  padding: var(--s2) var(--s4);
  border-top: 1px solid var(--line);
  font-size: 13px;
  color: var(--ink-dim);
}
.shop-table tr:first-child td {
  border-top: 0;
}
.shop-table a {
  color: var(--ink);
  text-decoration: none;
}
.shop-table a:hover {
  color: var(--gold);
}
/* A pack shot in the timeline is a photograph, not a wordmark. */
.tl-logo.art-pack {
  width: 64px;
  height: 64px;
  border-radius: 8px;
}
/* ------------------------------------------------------ sortable table --- */
thead th button {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
thead th button:hover {
  color: var(--ink);
}
thead th button svg {
  width: 8px;
  height: 10px;
  fill: var(--line-bright);
  flex: none;
}
thead th.sorted button {
  color: var(--ink);
}
thead th.sorted[aria-sort="ascending"] button svg path:first-child,
thead th.sorted[aria-sort="descending"] button svg path:last-child {
  fill: var(--gold);
}
thead th {
  position: sticky;
  top: var(--sticky-top, 0px);
  z-index: 2;
  background: var(--surface);
}
thead th::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  border-bottom: 1px solid var(--line);
}
/* A hint that names an action should be the action. */
.linkish {
  padding: 0;
  border: 0;
  background: none;
  color: var(--gold);
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.linkish:hover {
  color: var(--gold-deep);
}
/* ------------------------------------------------------------ ready page */
.ready-card {
  display: grid;
  grid-template-columns: 72px 1fr auto auto;
  align-items: center;
  gap: var(--s4);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface);
  /* The same stripe the buyable table rows wear, so the two views agree. */
  box-shadow: inset 3px 0 0 var(--gold);
}
.ready-card + .ready-card {
  margin-top: var(--s2);
}
.ready-art img {
  width: 72px;
  height: 72px;
  object-fit: contain;
  display: block;
}
/* Size and shape only; the plate itself is .art-none, shared with the release
   calendar. It was a plain grey rectangle, which reads as an image that
   failed to load rather than as a product nobody has photographed. */
.ready-noart {
  width: 72px;
  height: 72px;
  border-radius: 8px;
}
.ready-noart svg {
  width: 24px;
  height: 24px;
}
.ready-name {
  display: grid;
  gap: 2px;
  text-decoration: none;
}
.ready-set {
  font-size: 12px;
  color: var(--ink-faint);
}
.ready-name strong {
  font: 600 17px var(--font);
  color: var(--ink);
}
.ready-name:hover strong {
  color: var(--gold);
}
.ready-sub {
  margin: var(--s2) 0 0;
  font-size: 12px;
  color: var(--ink-dim);
}
.ready-buy {
  display: grid;
  justify-items: end;
  gap: 2px;
  text-align: right;
}
.ready-price {
  font: 700 22px var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.ready-price small {
  margin-left: 5px;
  font: 500 11px var(--font);
  color: var(--ink-faint);
}
.ready-where {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: 13px;
  color: var(--ink-dim);
}
.ready-ship {
  font-size: 12px;
  color: var(--ink-faint);
}
.ready-go {
  width: auto;
  min-width: 96px;
}
@media (max-width: 760px) {
  .ready-card {
    grid-template-columns: 56px 1fr;
    row-gap: var(--s3);
  }
  .ready-buy,
  .ready-go {
    grid-column: 2;
    justify-self: start;
  }
  .ready-buy {
    justify-items: start;
    text-align: left;
  }
}
/* ---------------------------------------------------------------- footer */
/* The footer used to be three columns of links pushed into the left fifth of
   the page, with four-fifths of empty space beside them and no statement of
   what the thing is or whose artwork it shows. It is the last thing on every
   page, so it may as well say something: what this is, how much of it there
   is, where to go, and the provenance the rest of the page leaves out. */
.footer {
  display: grid;
  gap: var(--s5);
  margin-top: var(--s6);
  padding-top: var(--s5);
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
  font-size: 12px;
}
.footer-top {
  display: grid;
  /* One canyon is worse than none: pushing the brand to one edge and the links
     to the other left six hundred empty pixels down the middle of a wide
     screen. The brand is the first column of four, so the row fills its width
     instead of being held apart by it. */
  grid-template-columns: minmax(15rem, 24rem) repeat(3, minmax(8rem, 1fr));
  gap: var(--s5) var(--s5);
  align-items: start;
  /* The rule belongs to the footer's full width. Putting it on the fine print
     below made it stop where that paragraph's measure stops, which read as a
     rule that had been cut off half way. */
  padding-bottom: var(--s5);
  border-bottom: 1px solid var(--line);
}
.footer-brand {
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr);
  align-items: center;
  gap: var(--s1) var(--s3);
  max-width: 46ch;
}
.footer-brand img {
  grid-row: span 1;
  border-radius: var(--r1);
}
.footer-word {
  margin: 0;
  font: 600 15px var(--font);
  letter-spacing: -0.01em;
  color: var(--ink);
}
/* The two lines under the wordmark run the full width of the block rather
   than sitting in the column beside the mark. */
.footer-what,
.footer-scale {
  grid-column: 1 / -1;
  margin: 0;
  line-height: 1.5;
}
.footer-what {
  color: var(--ink-dim);
  font-size: 13px;
}
.footer-scale {
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.footer-scale:empty {
  display: none;
}
.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
}
.footer-links a {
  color: var(--ink-dim);
  text-decoration: none;
}
.footer-links a:hover {
  color: var(--gold);
  text-decoration: underline;
  text-underline-offset: 3px;
}
/* ----------------------------------------------------------- about pages */
.prose {
  max-width: 68ch;
}
.prose h2 {
  margin: 0 0 var(--s2);
  font: 600 22px var(--font);
  letter-spacing: -0.01em;
  color: var(--ink);
}
.prose-lede {
  margin: 0 0 var(--s5);
  color: var(--ink-dim);
  font-size: 15px;
  line-height: 1.6;
}
.prose section + section {
  margin-top: var(--s5);
}
/* A section heading has to outrank the paragraph under it. At 11px faint it
   was smaller and quieter than the 13px --ink-dim body it introduced, so the
   prose pages read as one undifferentiated column with some small grey lines
   in it. Still uppercase - that is the site's register for a label - but no
   longer the least prominent thing in its own section. */
.prose h3 {
  margin: 0 0 var(--s2);
  font: 600 12px var(--font);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.prose p,
.prose li {
  margin: 0 0 var(--s2);
  color: var(--ink-dim);
  font-size: 13px;
  line-height: 1.65;
}
.prose ul {
  margin: 0;
  padding-left: var(--s4);
}
.prose li::marker {
  color: var(--line-bright);
}
.prose strong {
  color: var(--ink);
  font-weight: 600;
}
.prose-foot {
  margin: var(--s6) 0 0;
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
  font-size: 12px;
}
.ready-art {
  position: relative;
  display: block;
}
.ready-set-mark {
  position: absolute;
  left: -6px;
  bottom: -4px;
  width: 52px;
  max-height: 26px;
  object-fit: contain;
  object-position: left bottom;
  filter: drop-shadow(0 1px 3px rgb(0 0 0 / 35%));
  pointer-events: none;
}
/* --------------------------------------------------- set sort + upcoming */
.sort-row {
  display: flex;
  align-items: center;
  /* Two segmented controls plus a chip per non-default setting does not fit
     one phone line, and this row must never be the thing that makes the page
     scroll sideways. */
  flex-wrap: wrap;
  gap: var(--s3);
  margin-bottom: var(--s4);
}
.sort-label {
  font: 600 11px var(--font);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
/* A set you cannot buy yet is a different thing from one that is sold out, so
   it says so in its own words rather than as a date the eye slides past. */
.upcoming {
  display: inline-flex;
  align-items: center;
  align-self: start;
  margin-top: var(--s2);
  padding: 2px 9px;
  border: 1px dashed var(--blue);
  border-radius: 999px;
  color: var(--blue);
  font: 600 12px var(--font);
}
/* ------------------------------------------------------ market reference */
.market-ref small a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.market-ref strong {
  color: var(--ink-dim);
}
/* ----------------------------------------------------------- pack opener */
.opener {
  display: flex;
  flex-direction: column;
  gap: var(--s5);
}
.opener-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s4);
  flex-wrap: wrap;
}
.opener-eyebrow {
  margin: 0;
  font: 600 11px var(--font);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.opener h2 {
  margin: var(--s2) 0 0;
  font: 600 26px var(--font);
  letter-spacing: -0.015em;
  color: var(--ink);
}
.opener-sub {
  margin: var(--s2) 0 0;
  color: var(--ink-dim);
  font-size: 13px;
}
.opener-odds,
.opener-credit {
  margin: 0;
  color: var(--ink-faint);
  font-size: 12px;
  line-height: 1.6;
}
.opener-credit {
  border-top: 1px solid var(--line);
  padding-top: var(--s3);
  max-width: 78ch;
}
.opener-credit a {
  color: var(--ink-dim);
}
/* ------ the stage ------ */
.pack-stage {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 460px;
  padding: var(--s5) 0;
  border: 1px solid var(--line);
  border-radius: 18px;
  /* A pool of light under the pack, so it reads as an object on a surface
     rather than a rectangle on a background. */
  background:
    radial-gradient(
      120% 80% at 50% 18%,
      color-mix(in oklab, var(--gold) 10%, transparent),
      transparent 60%
    ),
    var(--surface);
  overflow: hidden;
}
/* ------ the sealed pack ------ */
.pack {
  position: relative;
  width: 250px;
  height: 380px;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: linear-gradient(160deg, #1a1b22 0%, #0d0e13 46%, #17181f 100%);
  box-shadow:
    0 30px 60px -20px rgb(0 0 0 / 65%),
    inset 0 1px 0 rgb(255 255 255 / 12%);
  cursor: pointer;
  overflow: hidden;
  animation: float 5.5s ease-in-out infinite;
  transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
}
.pack:hover {
  transform: translateY(-6px) scale(1.02);
}
@keyframes float {
  0%,
  100% {
    transform: translateY(0) rotate(-0.6deg);
  }
  50% {
    transform: translateY(-10px) rotate(0.6deg);
  }
}
/* The foil: two slow gradients at different speeds, which reads as depth
   rather than as one sliding highlight. */
.pack-foil {
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(
      115deg,
      rgb(255 255 255 / 5%) 0 2px,
      transparent 2px 9px
    ),
    conic-gradient(
      from 210deg at 30% 20%,
      #6f4bd8 0deg,
      #2f7fd4 70deg,
      #d8b23c 150deg,
      #c0424a 220deg,
      #4e2f9c 300deg,
      #6f4bd8 360deg
    );
  opacity: 0.32;
  mix-blend-mode: screen;
  animation: foilturn 9s linear infinite;
}
@keyframes foilturn {
  to {
    filter: hue-rotate(360deg);
  }
}
.pack-sheen {
  position: absolute;
  inset: -40%;
  background: linear-gradient(
    78deg,
    transparent 42%,
    rgb(255 255 255 / 42%) 50%,
    transparent 58%
  );
  animation: sweep 4.2s ease-in-out infinite;
}
@keyframes sweep {
  0%,
  62% {
    transform: translateX(-62%) rotate(6deg);
  }
  100% {
    transform: translateX(62%) rotate(6deg);
  }
}
.pack-art {
  position: absolute;
  inset: 46px 18px auto;
  display: grid;
  place-items: center;
}
.pack-art img {
  width: 100%;
  max-height: 150px;
  object-fit: contain;
  filter: drop-shadow(0 6px 16px rgb(0 0 0 / 60%));
}
.pack-name {
  color: #fff;
  font: 700 26px/1.1 var(--font);
  text-align: center;
  text-shadow: 0 2px 12px rgb(0 0 0 / 70%);
}
.pack-hint {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 26px;
  color: rgb(255 255 255 / 72%);
  font: 600 11px var(--font);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  animation: breathe 2.6s ease-in-out infinite;
}
@keyframes breathe {
  0%,
  100% {
    opacity: 0.45;
  }
  50% {
    opacity: 1;
  }
}
/* ------ tearing ------ */
.stage-tearing .pack {
  pointer-events: none;
  animation: recede 900ms 220ms cubic-bezier(0.5, 0, 0.2, 1) forwards;
}
@keyframes peel {
  40% {
    transform: rotateX(-70deg) translateY(-6px);
  }
  100% {
    transform: rotateX(-105deg) translateY(-70px) translateX(40px) rotate(14deg);
    opacity: 0;
  }
}
@keyframes recede {
  100% {
    transform: translateY(26px) scale(0.84);
    opacity: 0;
    filter: blur(6px);
  }
}
/* The cards stay out of the layout until the pack has gone. They used to
   appear, face down, beneath the pack the moment it was tapped - a second
   block of content arriving under the thing you were watching. */
.stage-sealed .fan,
.stage-tearing .fan {
  display: none;
}
.pack-stage {
  scroll-margin-top: calc(var(--sticky-top, 0px) + var(--s3));
}
/* The pack goes, and so does its caption - moving the hint out of the button
   left it sitting above the cards telling you to tap a pack that was no
   longer there. */
.stage-revealing .pack,
.stage-done .pack,
.stage-revealing .pack-hint,
.stage-done .pack-hint {
  display: none;
}
/* The stage was sized to hold one pack; once it holds ten cards it should
   grow to them rather than keep a pack's worth of empty air above. */
.stage-revealing .pack-stage,
.stage-done .pack-stage {
  min-height: 0;
}
/* ------ the cards ------ */
/* Five across, two down. Wrapping by whatever happened to fit gave six and
   then four, which reads as an overflow rather than as a pack laid out - and
   a booster is ten cards, so the arrangement may as well say so. The columns
   step down rather than reflow freely, so it is always two tidy rows. */
.fan {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 168px));
  justify-content: center;
  align-items: start;
  gap: var(--s4);
  list-style: none;
  margin: 0;
  padding: 0 var(--s4);
  perspective: 1400px;
}
/* A pack smaller than the grid was sitting left of centre: five column tracks
   are laid out whether or not anything occupies them, so three cards filled
   the first three and the two empty ones took their share of the width.
   Only the packs smaller than the grid need to say so. */
.fan[data-cards="1"] {
  grid-template-columns: minmax(0, 168px);
}
.fan[data-cards="2"] {
  grid-template-columns: repeat(2, minmax(0, 168px));
}
.fan[data-cards="3"] {
  grid-template-columns: repeat(3, minmax(0, 168px));
}
.fan[data-cards="4"] {
  grid-template-columns: repeat(4, minmax(0, 168px));
}
@media (max-width: 1080px) {
  .fan {
    grid-template-columns: repeat(4, minmax(0, 168px));
  }
}
@media (max-width: 880px) {
  .fan {
    grid-template-columns: repeat(3, minmax(0, 168px));
  }
}
@media (max-width: 640px) {
  .fan,
  .fan[data-cards="4"] {
    grid-template-columns: repeat(2, minmax(0, 168px));
  }
  /* Three stays three even on a phone. Two-up would wrap the third onto a row
     of its own, and one orphaned card beside an empty half is worse to look
     at than three narrow ones in a row. */
  .fan[data-cards="3"] {
    grid-template-columns: repeat(3, minmax(0, 168px));
    gap: var(--s2);
  }
}
/* ------ rarity colour ------ */
/* Every hit used to glow the same gold, which said a card was good without
   saying what kind. The colours follow what the card itself looks like in
   hand rather than an invented ladder: ex cards amber, full arts violet, the
   art rares green, the chase alt-arts magenta, the gold cards gold. */
.card-slot {
  position: relative;
  /* the rarity aura anchors to the card, not the fan */
  width: 168px;
  opacity: 0;
  cursor: pointer;
  transform: translateY(28px) scale(0.94);
  transition:
    opacity 420ms ease,
    transform 620ms cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: calc(var(--i) * 20ms);
  --rare: var(--gold);
}
.card-slot.shown {
  opacity: 1;
  transform: none;
}
.card3d {
  position: relative;
  z-index: 1;
  /* above the aura behind it */
  width: 168px;
  height: 234px;
  transform-style: preserve-3d;
  transform: rotateY(180deg);
  /* No overshoot. The old curve had y1 = 1.1, which carried the rotation past
     flat and sprang it back - the bounce read as a stutter rather than as
     weight. This one decelerates into the stop and holds. */
  transition: transform 620ms cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}
.card-slot.shown .card3d {
  transform: rotateY(0deg);
}
.card-face {
  position: absolute;
  inset: 0;
  border-radius: 9px;
  backface-visibility: hidden;
  overflow: hidden;
  box-shadow: 0 12px 26px -12px rgb(0 0 0 / 70%);
}
.card-face.front {
  background: var(--raised);
}
.card-face.front img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* An original back, not a copy of the printed one. */
.card-face.back {
  transform: rotateY(180deg);
  display: grid;
  place-items: center;
  background:
    radial-gradient(80% 60% at 50% 30%, #2a2f5c, #14162c 70%), #12142a;
  border: 2px solid #3b4076;
}
.back-ring {
  width: 74px;
  height: 74px;
  border-radius: 999px;
  border: 3px solid rgb(255 255 255 / 26%);
  box-shadow: inset 0 0 0 10px rgb(255 255 255 / 7%);
}
.back-core {
  position: absolute;
  width: 26px;
  height: 26px;
  border-radius: 999px;
  background: linear-gradient(150deg, var(--gold), var(--gold-deep));
}
.card-label {
  margin: var(--s2) 0 0;
  text-align: center;
  font-size: 12px;
  line-height: 1.35;
  color: var(--ink-faint);
}
.card-label strong {
  display: block;
  color: var(--ink);
  font: 600 13px var(--font);
}
/* ------ rarity ------ */
.card-slot.reverse .card-face.front::after,
/* The shine sweeps, then rests off the card at both ends.
 *
 * It used to share the pack's keyframes, which stop at translateX(62%) - still
 * partly over the card - so every repeat snapped the band back to the left in
 * one frame. The hop was the loop, not the sweep. Parking it fully clear on
 * both sides means the reset happens where nothing is visible, and the rest
 * between passes is deliberate rather than an accident of timing. */
.card-slot.shiny .card-sheen {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    115deg,
    transparent 38%,
    rgb(255 255 255 / 52%) 50%,
    transparent 62%
  );
  mix-blend-mode: overlay;
  animation: card-sweep 4.6s linear infinite;
  will-change: transform;
  pointer-events: none;
}
@keyframes card-sweep {
  0%,
  52% {
    transform: translateX(-170%) rotate(6deg);
  }
  78%,
  100% {
    transform: translateX(170%) rotate(6deg);
  }
}
.card-slot.shiny .card-face.front {
  box-shadow:
    0 12px 26px -12px rgb(0 0 0 / 70%),
    0 0 0 1.5px color-mix(in oklab, var(--rare) 80%, transparent),
    0 0 38px -4px color-mix(in oklab, var(--rare) 80%, transparent);
}
.card-slot.loud .card-face.front {
  box-shadow:
    0 14px 30px -12px rgb(0 0 0 / 70%),
    0 0 0 2.5px var(--rare),
    0 0 26px -8px color-mix(in oklab, var(--rare) 90%, transparent),
    0 0 64px 0 color-mix(in oklab, var(--rare) 55%, transparent);
}
.card-slot.loud .card-label strong {
  color: var(--rare);
}
.spark-layer {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
}
.spark-layer i {
  position: absolute;
  width: 5px;
  height: 5px;
  border-radius: 999px;
  background: var(--gold);
  animation: spark var(--t) cubic-bezier(0.2, 0.8, 0.3, 1) forwards;
  transform: rotate(var(--a)) translateY(0);
}
@keyframes spark {
  0% {
    opacity: 1;
    transform: rotate(var(--a)) translateY(0) scale(1);
  }
  100% {
    opacity: 0;
    transform: rotate(var(--a)) translateY(calc(var(--d) * -1)) scale(0.3);
  }
}
@media (prefers-reduced-motion: reduce) {
  .pack,
  .pack-sheen,
  .pack-foil,
  .pack-hint,
  .card-slot.shiny .card-sheen,
  .spark-layer i {
    animation: none !important;
  }
  .card3d,
  .card-slot {
    transition-duration: 1ms !important;
  }
}
@media (max-width: 620px) {
  .card-slot,
  .card3d {
    width: 120px;
  }
  .card3d {
    height: 167px;
  }
  .pack {
    width: 200px;
    height: 305px;
  }
}
.open-pack {
  display: flex;
  align-items: center;
  gap: var(--s4);
  margin-bottom: var(--s4);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line-bright);
  border-radius: 14px;
  background: var(--surface);
  text-decoration: none;
}
.open-pack:hover {
  border-color: var(--gold-deep);
}
.open-pack strong {
  display: block;
  color: var(--ink);
  font: 600 15px var(--font);
}
.open-pack small {
  color: var(--ink-faint);
  font-size: 12px;
}
.open-pack-art {
  width: 30px;
  height: 44px;
  flex: none;
  border-radius: 4px;
  background: linear-gradient(160deg, #1a1b22, #0d0e13 55%, #17181f);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 14%),
    0 4px 10px -4px rgb(0 0 0 / 60%);
  position: relative;
  overflow: hidden;
}
.open-pack-art::after {
  content: "";
  position: absolute;
  inset: 0;
  background: conic-gradient(
    from 210deg at 30% 20%,
    #6f4bd8,
    #2f7fd4,
    #d8b23c,
    #c0424a,
    #4e2f9c,
    #6f4bd8
  );
  opacity: 0.4;
  mix-blend-mode: screen;
}
/* ------------------------------------------------- cards you can pick up */
/* This is the rule that actually wins for the flip - it outranks the .card3d
   base, which is why editing that one changed nothing. Same easing as the
   base now: no overshoot, so the card decelerates into flat and stays there
   instead of rotating past and springing back. */
.card-slot .card3d {
  transition:
    transform 620ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 240ms ease;
}
/* Once revealed, the flip is finished and the tilt takes over the transform. */
.card-slot.shown.held .card3d {
  transition:
    transform 90ms ease-out,
    box-shadow 240ms ease;
  transform: perspective(700px) rotateX(var(--tilt-x, 0deg))
    rotateY(var(--tilt-y, 0deg)) translateZ(26px) scale(1.06);
}
.card-slot.shown.held {
  z-index: 5;
}
/* A highlight that follows the pointer, which is what sells the tilt. */
.card-slot.held .card-face.front::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background: radial-gradient(
    circle at var(--gx, 50%) var(--gy, 50%),
    rgb(255 255 255 / 42%),
    transparent 55%
  );
  mix-blend-mode: soft-light;
  pointer-events: none;
}
.card-slot.held .card-face.front {
  box-shadow:
    0 26px 44px -18px rgb(0 0 0 / 80%),
    0 0 0 1px rgb(255 255 255 / 14%);
}
/* ------ the set picker ------ */
.pack-picker {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}
.pack-picker .chip {
  text-decoration: none;
}
/* ------ the detail view ------ */
body.no-scroll {
  overflow: hidden;
}
.inspect {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: grid;
  place-items: center;
  padding: var(--s5);
  background: rgb(6 7 10 / 82%);
  backdrop-filter: blur(6px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 220ms ease;
}
.inspect.open {
  opacity: 1;
  pointer-events: auto;
}
/* The enlarged card, in three dimensions and in its own colour.
   It used to sit flat and glow gold whatever it was - the one view where you
   are actually looking at the card was the one that dropped both. */
/* The rarity palette, for the enlarged card. */
.inspect-card {
  position: relative;
  display: grid;
  gap: var(--s3);
  justify-items: center;
  margin: 0;
  transform: scale(0.94);
  transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1);
  transform-style: preserve-3d;
  will-change: transform;
  --rare: var(--gold);
}
.inspect.open .inspect-card {
  transform: none;
}
/* The artwork already carries the card's own rounded corners as
   transparency - measured off the source, a 31px radius on a 733px card, and
   the back has its own slightly different one. A box-shadow is drawn around
   the element's *box*, so its near-black first layer painted straight into
   those transparent corners: a dark wedge at each corner of every card, only
   visible on the ones with a glow because elsewhere it matched the backdrop.
   drop-shadow follows the alpha instead, so the light hugs the printed edge
   whatever radius the file happens to have. */
.inspect-card img {
  width: min(74vw, 380px);
  height: auto;
  filter: drop-shadow(0 26px 30px rgb(0 0 0 / 78%));
}
.inspect-card figcaption {
  position: relative;
  z-index: 1;
  display: grid;
  gap: 2px;
  text-align: center;
  color: var(--ink-dim);
  font-size: 13px;
}
.inspect-card strong {
  color: var(--ink);
  font: 600 17px var(--font);
}
.inspect-artist {
  color: var(--ink-faint);
  font-size: 12px;
}
.inspect-close {
  position: absolute;
  top: -14px;
  right: -14px;
  width: 34px;
  height: 34px;
  border-radius: 999px;
  border: 1px solid var(--line-bright);
  background: var(--surface);
  color: var(--ink);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.inspect-close:hover {
  border-color: var(--gold);
  color: var(--gold);
}
@media (prefers-reduced-motion: reduce) {
  .card-slot.shown.held .card3d {
    transform: none;
  }
  .inspect,
  .inspect-card {
    transition-duration: 1ms;
  }
}
@media (max-width: 620px) {
  .card-slot,
  .card3d {
    width: 132px;
  }
  .card3d {
    height: 184px;
  }
}
.buy-card > .buy-main {
  display: grid;
  grid-template-columns: 60px minmax(0, 1fr);
  gap: var(--s3);
  align-items: center;
  padding: var(--s3);
  border-radius: var(--r1);
  text-decoration: none;
  color: inherit;
}
.buy-out {
  position: absolute;
  top: 6px;
  right: 6px;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 8px;
  color: var(--ink-faint);
  opacity: 0;
  transition:
    opacity 120ms ease,
    color 120ms ease,
    background 120ms ease;
}
.buy-out svg {
  width: 14px;
  height: 14px;
}
.buy-card:hover .buy-out,
.buy-out:focus-visible {
  opacity: 1;
}
/* --chip and --muted were used here and defined nowhere - not in the token
   block, not set from JS. A var() with no fallback that resolves to nothing is
   invalid at computed-value time, so this button's hover and focus background
   simply never appeared and its resting colour was whatever it inherited. The
   tokens it wanted already existed under other names. */
.buy-out:hover,
.buy-out:focus-visible {
  color: var(--ink);
  background: var(--raised);
}
/* Touch has no hover to reveal it, so it simply stays visible there. */
@media (hover: none) {
  .buy-out {
    opacity: 1;
  }
}
/* The row a buy card sent you to. */
tr.came-from > td {
  background: var(--came-from, rgba(255, 199, 0, 0.09));
}
tr.came-from > td:first-child {
  box-shadow: inset 3px 0 0 var(--accent, #f0b429);
}
/* Next release, on the page you actually land on. It sits above Available Now
   because it answers a different question: that panel is what you can buy
   today, this is what to wait for. */
.next-up {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) auto;
  gap: var(--s3);
  align-items: center;
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  text-decoration: none;
  color: inherit;
}
.next-up:hover {
  border-color: var(--line-bright);
}
.next-up-art {
  width: 44px;
  height: 44px;
  object-fit: contain;
}
.next-up-art.art-none {
  border-radius: var(--r1);
}
.next-up-text {
  display: grid;
  gap: 1px;
  min-width: 0;
}
.next-up-label {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--gold);
}
.next-up-text strong {
  font-size: 15px;
  font-weight: 650;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.next-up-date {
  font-size: 12px;
  color: var(--ink-dim);
}
.next-up-go {
  font-size: 12px;
  color: var(--ink-faint);
  white-space: nowrap;
}
.next-up:hover .next-up-go {
  color: var(--ink-dim);
}
/* A date the sources do not agree on. Saying so beats quietly picking one. */
.tl-disputed {
  font-size: 12px;
  color: var(--ink-faint);
  border-bottom: 1px dotted var(--line-bright);
  cursor: help;
}
@media (max-width: 560px) {
  .next-up {
    grid-template-columns: 36px minmax(0, 1fr);
  }
  .next-up-go {
    display: none;
  }
}
/* ------ the sealed pack, wearing its real wrapper ------ */
/* A real booster is 0.547 as wide as it is tall, which the hand-drawn foil
   never was. With genuine artwork on it the proportion is the first thing
   that would look wrong. */
.pack.real {
  width: 246px;
  height: 450px;
  background: none;
  border-radius: 8px;
  /* The sheen is deliberately oversized so its sweep crosses the whole pack,
     and it is the pack that clips it - opening the overflow to let the hint
     escape sent a pale band across the page instead. The hint is a caption
     for the pack rather than part of it, so it moved outside the button and
     the clipping stays. The shadow is a filter rather than a box-shadow
     because a filter applies after the clip and so still escapes. */
  overflow: hidden;
  box-shadow: none;
  filter: drop-shadow(0 26px 40px rgb(0 0 0 / 62%));
}
.pack.real .pack-wrap {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Cropped to solid artwork, so the only shaping left is the pack's own
     corners - which the photograph no longer carries, having been trimmed
     past them. */
  border-radius: 7px;
}
/* The sheen rides over the artwork rather than under it, and is toned right
   down: a product photograph already carries its own foil highlights, so the
   drawn one only has to suggest movement. */
.pack.real .pack-sheen {
  mix-blend-mode: screen;
  opacity: 0.32;
}
/* Below the pack now, not over its artwork. Matched with the same weight as
   the rule that hides it once the pack is gone, so that one has to out-rank
   this rather than merely come after it. */
.pack-stage > .pack-hint {
  position: static;
  display: block;
  margin-top: var(--s3);
  text-align: center;
}
.stage-revealing .pack-stage > .pack-hint,
.stage-done .pack-stage > .pack-hint {
  display: none;
}
@media (max-width: 560px) {
  .pack.real {
    width: 197px;
    height: 360px;
  }
}
.card-slot[data-rarity="Rare"] {
  --rare: #8fb6da;
}
.card-slot[data-rarity="Double Rare"] {
  --rare: #f0b53f;
}
.card-slot[data-rarity="ACE SPEC Rare"] {
  --rare: #e2574a;
}
.card-slot[data-rarity="Ultra Rare"] {
  --rare: #a97ae6;
}
.card-slot[data-rarity="Illustration Rare"] {
  --rare: #46c39a;
}
.card-slot[data-rarity="Special Illustration Rare"] {
  --rare: #ec5fa4;
}
.card-slot[data-rarity="Shiny Rare"] {
  --rare: #4fd2e6;
}
.card-slot[data-rarity="Shiny Ultra Rare"] {
  --rare: #6fe3d0;
}
.card-slot[data-rarity="Hyper Rare"] {
  --rare: #ffc93c;
}
.card-slot[data-rarity="Mega Hyper Rare"] {
  --rare: #ffe6a3;
}
.card-slot[data-rarity="Black White Rare"] {
  --rare: #dfe9f5;
}
/* The aura sits behind the card, so the colour reads before the eye has
   found the rarity printed underneath it. Only for the ones worth looking
   twice at: a glow on every card is a glow on none. */
.card-slot.shiny::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 44%;
  width: 132%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  width: 150%;
  background: radial-gradient(
    closest-side,
    color-mix(in oklab, var(--rare) 68%, transparent),
    color-mix(in oklab, var(--rare) 22%, transparent) 45%,
    transparent 74%
  );
  opacity: 0;
  transition: opacity 420ms ease 120ms;
  pointer-events: none;
}
.card-slot.loud::before {
  width: 168%;
}
.card-slot.shown::before {
  opacity: 1;
}
.card-slot.loud.shown::before {
  opacity: 1;
}
/* The per-card stagger travelled as an inline custom property, which the
   page's own style-src 'self' quietly dropped, so every delay computed to
   zero. Ten slots is a short enough list to write out. */
.card-slot:nth-child(1) {
  --i: 0;
}
.card-slot:nth-child(2) {
  --i: 1;
}
.card-slot:nth-child(3) {
  --i: 2;
}
.card-slot:nth-child(4) {
  --i: 3;
}
.card-slot:nth-child(5) {
  --i: 4;
}
.card-slot:nth-child(6) {
  --i: 5;
}
.card-slot:nth-child(7) {
  --i: 6;
}
.card-slot:nth-child(8) {
  --i: 7;
}
.card-slot:nth-child(9) {
  --i: 8;
}
.card-slot:nth-child(10) {
  --i: 9;
}
/* Alert history.
   A log reads as rows under a date, not as cards: the eye runs down the left
   edge looking for a product, and down the right edge looking for a time. */
.alerts-page {
  padding-bottom: var(--s5);
}
.alerts-head h2 {
  margin: 0 0 var(--s1);
  font-size: 1.05rem;
}
.alerts-sub {
  margin: 0 0 var(--s4);
  color: var(--ink-faint);
  font-size: 13px;
}
.alert-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-bottom: var(--s4);
}
.alert-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-dim);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.alert-chip small {
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.alert-chip:hover:not(:disabled) {
  border-color: var(--line-bright);
  color: var(--ink);
}
.alert-chip.on {
  border-color: var(--gold-line);
  background: var(--gold-bg);
  color: var(--gold);
}
.alert-chip.on small {
  color: inherit;
}
.alert-chip:disabled {
  opacity: 0.45;
  cursor: default;
}
.alert-day {
  margin-bottom: var(--s4);
}
/* The date sticks so you always know which day you are reading once the list
   is longer than the window. */
.alert-date {
  position: sticky;
  top: var(--sticky-top, 0);
  z-index: 2;
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  margin: 0 0 var(--s2);
  padding: var(--s2) 0;
  background: var(--ground);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.alert-date small {
  color: var(--ink-faint);
  font-weight: 400;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}
.alert-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--line);
  border-radius: var(--r2);
  overflow: hidden;
  background: var(--surface);
}
.alert-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto auto auto;
  gap: var(--s3);
  align-items: center;
  padding: var(--s2) var(--s3);
  border-top: 1px solid var(--line);
}
.alert-row:first-child {
  border-top: 0;
}
/* A withdrawn alert is history explaining itself, so it recedes; a failed one
   is a problem and keeps full contrast plus a red edge. */
.alert-row[data-state="cancelled"] {
  color: var(--ink-faint);
}
.alert-row[data-state="failed"] {
  box-shadow: inset 3px 0 0 var(--red);
}
.alert-main {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s3);
  align-items: center;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}
a.alert-main:hover .alert-title {
  color: var(--gold);
}
.alert-art {
  width: 40px;
  height: 40px;
  object-fit: contain;
  border-radius: var(--r1);
}
.alert-what {
  min-width: 0;
}
.alert-title,
.alert-store {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.alert-title {
  font-size: 13px;
  color: var(--ink);
}
.alert-row[data-state="cancelled"] .alert-title {
  color: var(--ink-dim);
}
.alert-store {
  font-size: 12px;
  color: var(--ink-faint);
}
.alert-price {
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.alert-price .cur {
  margin-left: 0.25em;
  font-size: 11px;
  color: var(--ink-faint);
}
.alert-state {
  display: flex;
  flex-direction: column;
  gap: 0.1em;
  min-width: 8.5rem;
  font-size: 12px;
  color: var(--ink-dim);
}
.alert-state.sent {
  color: var(--gold);
}
.alert-state.failed {
  color: var(--red);
}
.alert-note {
  color: var(--ink-faint);
  font-size: 11px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.alert-note.bad {
  color: var(--red);
}
.alert-time {
  font-size: 12px;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.alert-out {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  color: var(--ink-faint);
}
.alert-out svg {
  width: 13px;
  height: 13px;
}
a.alert-out:hover {
  color: var(--gold);
}
.alert-more {
  display: flex;
  justify-content: center;
  padding: var(--s3) 0;
}
.load-more {
  padding: 0.45rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: var(--r1);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.load-more:hover:not(:disabled) {
  border-color: var(--gold-line);
  color: var(--gold);
}
.load-more:disabled {
  opacity: 0.6;
  cursor: default;
}
.alert-end {
  padding: var(--s3) 0;
  text-align: center;
  color: var(--ink-faint);
  font-size: 12px;
}
@media (max-width: 760px) {
  .alert-row {
    grid-template-columns: minmax(0, 1fr) auto;
    row-gap: var(--s1);
  }
  .alert-state {
    grid-column: 1;
    min-width: 0;
  }
  .alert-price,
  .alert-time {
    justify-self: end;
  }
  .alert-out {
    display: none;
  }
}
/* An image the cache could not supply hides rather than showing the browser's
   broken-file glyph across the middle of a revealed card. */
.art-missing {
  visibility: hidden;
}
/* Divides the browse grid into expansions and everything that is not one. */
.browse-band {
  margin: var(--s5) 0 var(--s3);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.browse-band:first-child {
  margin-top: 0;
}
/* The unit belongs with the number but must not compete with it. */
.trend-stats strong i {
  margin-left: 0.3em;
  font-size: 0.65em;
  font-style: normal;
  font-weight: 500;
  color: var(--ink-faint);
}
/* A limit that sits outside the plotted range is an annotation, not a rule
   line: it says where the threshold is without bending the scale to reach it. */
.trend .limit-tag.off {
  fill: var(--ink-faint);
  font-style: italic;
}
/* The cheapest point ever seen is the one mark worth a second colour. */
.trend .dot.best {
  fill: var(--surface);
  stroke: var(--gold);
  stroke-width: 2.5;
}
.market-ref strong i {
  margin-left: 0.3em;
  font-size: 0.65em;
  font-style: normal;
  font-weight: 500;
  color: var(--ink-faint);
}
/* The real wrapper needs none of the placeholder's fake foil: it is a
   photograph, so it just wants the pack's own proportions and a soft edge. */
.open-pack-art.real {
  width: 30px;
  height: 44px;
  flex: none;
  border-radius: 4px;
  object-fit: cover;
  background: none;
  box-shadow: 0 4px 10px -4px rgb(0 0 0 / 60%);
}
.open-pack-art.real::after {
  content: none;
}
/* ---------------------------------------------------------- pack chooser --- */
.chooser-head {
  margin-bottom: var(--s4);
}
.chooser-head h2 {
  margin: 0 0 var(--s1);
  font-size: 1.15rem;
}
.chooser-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(212px, 1fr));
  gap: var(--s3);
}
.chooser-card {
  display: grid;
  grid-template-columns: 60px minmax(0, 1fr);
  gap: var(--s3);
  align-items: center;
  padding: var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--surface);
  color: inherit;
  text-decoration: none;
}
.chooser-card:hover {
  border-color: var(--gold-line);
}
.chooser-art {
  display: grid;
  place-items: center;
  height: 90px;
}
.chooser-art img {
  max-width: 100%;
  max-height: 90px;
  object-fit: contain;
  border-radius: var(--r1);
}
.chooser-art .art-mark {
  max-height: 54px;
}
/* A photograph of the box, not a wordmark, so it is sized to fill its frame
   rather than to sit inside one. */
.art-shot {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.chooser-fallback {
  font-size: 11px;
  color: var(--ink-faint);
  text-align: center;
}
.chooser-meta strong {
  display: block;
  font-size: 15px;
}
/* The set's size and its date are two facts; on two lines they read as two
   facts, where one line wrapped mid-date read as an accident. */
.chooser-meta small {
  color: var(--ink-faint);
  font-size: 12px;
  display: block;
}
/* ------------------------------------------------------- opening at scale --- */
.opener-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}
.bulk {
  margin-bottom: var(--s5);
}
.bulk-head {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  margin-bottom: var(--s3);
}
.bulk-head h3 {
  margin: 0;
  font-size: 1rem;
}
.bulk-head button {
  margin-left: auto;
}
.bulk-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--s3);
  margin-bottom: var(--s4);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--surface);
}
.bulk-stats span {
  display: flex;
  flex-direction: column;
  gap: 0.15em;
  min-width: 0;
}
.bulk-stats small {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.bulk-stats strong {
  font: 700 20px/1.1 var(--mono);
  color: var(--ink);
}
.bulk-stats strong i {
  font: inherit;
  font-size: 13px;
  font-style: normal;
  color: var(--ink-faint);
}
.bulk-stats em {
  font-style: normal;
  font-size: 12px;
  color: var(--ink-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bulk-stats .best-pull {
  font-size: 15px;
}
.bulk-stats .verdict.up strong {
  color: var(--gold);
}
.bulk-stats .verdict.down strong {
  color: var(--blue);
}
/* A column per rarity, in the order the cards themselves are graded, so the
   shape of the run reads left to right without a legend. Each bar wears its
   own rarity colour - the same one the card gets on reveal. */
.hist {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  align-items: end;
  /* No column gap: the label rules butt together into one continuous
     baseline instead of a row of separate little axes. The bars keep their
     spacing from their own max-width. */
  gap: 0;
  height: 240px;
  padding: var(--s5) var(--s4) var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--surface);
  overflow-x: auto;
}
.hist-col {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  align-items: end;
  justify-items: center;
  height: 100%;
  min-width: 64px;
  padding: 0 var(--s1);
}
.hist-n {
  font: 600 13px var(--mono);
  color: var(--ink);
  margin-bottom: var(--s2);
}
.hist-bar {
  width: 100%;
  max-width: 34px;
  height: var(--h, 0%);
  min-height: 2px;
  align-self: end;
  border-radius: 4px 4px 0 0;
  background: var(--rare, var(--gold));
  transition: height 0.25s ease;
}
/* The labels are the axis. At 10px they were the smallest type on the page
   and they name the thing you are looking at. */
.hist-label {
  margin-top: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--line-bright);
  width: 100%;
  text-align: center;
  font-size: 12px;
  line-height: 1.25;
  color: var(--ink-dim);
  overflow-wrap: anywhere;
}
@media (prefers-reduced-motion: reduce) {
  .hist-bar {
    transition: none;
  }
}
.bulk-best {
  margin-top: var(--s4);
}
.bulk-best h4 {
  margin: 0 0 var(--s2);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.bulk-best ol {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: var(--s2);
  margin: 0;
  padding: 0;
  list-style: none;
}
.bulk-best li {
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr) auto;
  gap: var(--s3);
  align-items: center;
  padding: var(--s2);
  border: 1px solid var(--line);
  border-left: 3px solid var(--rare, var(--gold));
  border-radius: var(--r1);
  background: var(--surface);
}
.bulk-best img {
  border-radius: 3px;
}
.bulk-best strong {
  display: block;
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bulk-best small {
  font-size: 11px;
  color: var(--rare, var(--ink-faint));
}
/* The same rarity colours, for anything that is not a card slot: the
   histogram bar and the chase list read as the card they stand for. */
.hist-col,
.bulk-best li {
  --rare: var(--gold);
}
.hist-col[data-rarity="Rare"],
.bulk-best li[data-rarity="Rare"] {
  --rare: #8fb6da;
}
.hist-col[data-rarity="Double Rare"],
.bulk-best li[data-rarity="Double Rare"] {
  --rare: #f0b53f;
}
.hist-col[data-rarity="ACE SPEC Rare"],
.bulk-best li[data-rarity="ACE SPEC Rare"] {
  --rare: #e2574a;
}
.hist-col[data-rarity="Ultra Rare"],
.bulk-best li[data-rarity="Ultra Rare"] {
  --rare: #a97ae6;
}
.hist-col[data-rarity="Illustration Rare"],
.bulk-best li[data-rarity="Illustration Rare"] {
  --rare: #46c39a;
}
.hist-col[data-rarity="Special Illustration Rare"],
.bulk-best li[data-rarity="Special Illustration Rare"] {
  --rare: #ec5fa4;
}
.hist-col[data-rarity="Shiny Rare"],
.bulk-best li[data-rarity="Shiny Rare"] {
  --rare: #4fd2e6;
}
.hist-col[data-rarity="Shiny Ultra Rare"],
.bulk-best li[data-rarity="Shiny Ultra Rare"] {
  --rare: #6fe3d0;
}
.hist-col[data-rarity="Hyper Rare"],
.bulk-best li[data-rarity="Hyper Rare"] {
  --rare: #ffc93c;
}
.hist-col[data-rarity="Mega Hyper Rare"],
.bulk-best li[data-rarity="Mega Hyper Rare"] {
  --rare: #ffe6a3;
}
.hist-col[data-rarity="Black White Rare"],
.bulk-best li[data-rarity="Black White Rare"] {
  --rare: #dfe9f5;
}
/* A run button names the product and states its own pack count, so the
   arithmetic is on screen rather than assumed. */
.bulk-btn {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  line-height: 1.15;
}
.bulk-btn small {
  font-size: 11px;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
/* The unit rides with the figure; the shop's own price sits under it when the
   two differ, so a conversion never hides what you would actually pay. */
.detail-stats .stat strong i {
  margin-left: 0.25em;
  font-size: 0.6em;
  font-style: normal;
  font-weight: 500;
  color: var(--ink-faint);
}
.detail-stats .stat small {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  color: var(--ink-faint);
}
/* Every card pulled, rarest first. Scrolls rather than running to the length
   of the page: thirty-six packs is a couple of hundred rows. */
.tally {
  margin-top: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--surface);
  min-width: 0;
}
/* The same rarity colours for the tally rows. The base is a neutral rather
   than transparent: the rarity palette starts at Rare, so Commons and
   Uncommons had no colour at all - and since those are exactly the cards you
   pull most of, the longest bars in the list were the invisible ones. */
.tally-list li {
  --rare: var(--ink-faint);
  display: grid;
  grid-template-columns:
    30px minmax(0, 1fr) minmax(0, auto) minmax(40px, 130px)
    auto 2.6em;
  gap: var(--s3);
  align-items: center;
  padding: 3px var(--s3);
  border-top: 1px solid var(--line);
  box-shadow: inset 3px 0 0 var(--rare, transparent);
}
.tally-list li[data-rarity="Rare"] {
  --rare: #8fb6da;
}
.tally-list li[data-rarity="Double Rare"] {
  --rare: #f0b53f;
}
.tally-list li[data-rarity="ACE SPEC Rare"] {
  --rare: #e2574a;
}
.tally-list li[data-rarity="Ultra Rare"] {
  --rare: #a97ae6;
}
.tally-list li[data-rarity="Illustration Rare"] {
  --rare: #46c39a;
}
.tally-list li[data-rarity="Special Illustration Rare"] {
  --rare: #ec5fa4;
}
.tally-list li[data-rarity="Shiny Rare"] {
  --rare: #4fd2e6;
}
.tally-list li[data-rarity="Shiny Ultra Rare"] {
  --rare: #6fe3d0;
}
.tally-list li[data-rarity="Hyper Rare"] {
  --rare: #ffc93c;
}
.tally-list li[data-rarity="Mega Hyper Rare"] {
  --rare: #ffe6a3;
}
.tally-list li[data-rarity="Black White Rare"] {
  --rare: #dfe9f5;
}
/* Best pulls beside the full list rather than under it. The short column
   sticks: eight rows against a hundred and seventy means the alternative is a
   column of empty space and a header you scrolled past ten seconds ago. */
.bulk-detail {
  display: grid;
  grid-template-columns: minmax(230px, 300px) minmax(0, 1fr);
  gap: var(--s4);
  align-items: start;
  margin-top: var(--s4);
}
/* Grid items refuse to shrink below their content unless told to. Without
   this the rarity column - "Special Illustration Rare", nowrap - set a floor
   the column could not meet, and the whole list ran off the right edge
   carrying the value column with it. */
.bulk-detail > * {
  min-width: 0;
}
@media (max-width: 900px) {
  .bulk-detail {
    grid-template-columns: minmax(0, 1fr);
  }
}
.bulk-detail .bulk-best {
  position: sticky;
  top: calc(var(--sticky-top, 0px) + var(--s3));
  margin: 0;
}
.bulk-detail .bulk-best ol {
  grid-template-columns: minmax(0, 1fr);
}
.tally h4,
.bulk-best h4 {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  margin: 0 0 var(--s2);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.tally h4 small {
  margin-left: auto;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
/* No inner scroll: the page scrolls, so nothing is trapped in a 400px window. */
.tally-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--surface);
  overflow: hidden;
}
.tally-list li:first-child {
  border-top: 0;
}
.tally-list img {
  border-radius: 2px;
}
.tally-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
}
.tally-rarity {
  font-size: 11px;
  color: var(--ink-faint);
  white-space: nowrap;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Length is the count, colour is the rarity. The track keeps every row the
   same width so the bars are read against a common baseline. */
.tally-bar {
  display: block;
  height: 8px;
  border-radius: 999px;
  background: var(--raised);
  overflow: hidden;
}
.tally-bar i {
  display: block;
  width: var(--w, 0%);
  height: 100%;
  border-radius: 999px;
  background: var(--rare, var(--gold));
}
.tally-n {
  text-align: right;
  font: 600 12px var(--mono);
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}
@media (max-width: 700px) {
  .tally-list li {
    grid-template-columns: 30px minmax(0, 1fr) auto auto 2.6em;
  }
  .tally-bar {
    display: none;
  }
}
/* Both card lists open the inspector, so they say so. */
.tally-list li[data-card],
.bulk-best li[data-card] {
  cursor: pointer;
}
.tally-list li[data-card]:hover,
.bulk-best li[data-card]:hover {
  background: var(--raised);
}
.tally-list li[data-card]:focus-visible,
.bulk-best li[data-card]:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}
/* Scoped through .inspect.open because that rule sets transform: none with
   three classes of specificity - a bare .inspect-card.held has two and loses,
   which is why the tilt did nothing however correct the maths was. */
.inspect.open .inspect-card.held {
  transition: transform 90ms ease-out;
  transform: perspective(1100px) rotateX(var(--tilt-x, 0deg))
    rotateY(var(--tilt-y, 0deg)) scale(1.02);
}
/* Light, not a stroke.
 *
 * These glows were anchored by a spread with no blur - `0 0 0 3px` - which is
 * a drawn outline, and it read as a hard orange line ruled around the card
 * rather than as the card giving off light. The innermost layer is now blurred
 * too, so the colour is densest at the edge and falls away; the wider layers
 * carry the reach. Nothing here is dimmer than it was, it just has no edge. */
/* A glow that breathes rather than sits there. Two keyframes, each a whole
   filter: a custom property cannot be interpolated without registering it,
   and the static rule above stays as the reduced-motion answer. */
.inspect-card.shiny img {
  filter: drop-shadow(0 26px 30px rgb(0 0 0 / 78%))
    drop-shadow(
      0 0 9px color-mix(in oklab, var(--rare, var(--gold)) 70%, transparent)
    )
    drop-shadow(
      0 0 30px color-mix(in oklab, var(--rare, var(--gold)) 60%, transparent)
    );
  animation: card-breathe 3.4s ease-in-out infinite;
}
@keyframes card-breathe {
  0%,
  100% {
    filter: drop-shadow(0 26px 30px rgb(0 0 0 / 78%))
      drop-shadow(
        0 0 7px color-mix(in oklab, var(--rare, var(--gold)) 55%, transparent)
      )
      drop-shadow(
        0 0 22px color-mix(in oklab, var(--rare, var(--gold)) 42%, transparent)
      );
  }
  50% {
    filter: drop-shadow(0 26px 30px rgb(0 0 0 / 78%))
      drop-shadow(
        0 0 13px color-mix(in oklab, var(--rare, var(--gold)) 85%, transparent)
      )
      drop-shadow(
        0 0 46px color-mix(in oklab, var(--rare, var(--gold)) 72%, transparent)
      );
  }
}
.inspect-card.loud img {
  filter: drop-shadow(0 26px 30px rgb(0 0 0 / 78%))
    drop-shadow(
      0 0 12px color-mix(in oklab, var(--rare, var(--gold)) 85%, transparent)
    )
    drop-shadow(
      0 0 26px color-mix(in oklab, var(--rare, var(--gold)) 75%, transparent)
    )
    drop-shadow(
      0 0 52px color-mix(in oklab, var(--rare, var(--gold)) 45%, transparent)
    );
  /* The chase cards breathe harder and a touch slower. */
  animation: card-breathe-loud 4.2s ease-in-out infinite;
}
@keyframes card-breathe-loud {
  0%,
  100% {
    filter: drop-shadow(0 26px 30px rgb(0 0 0 / 78%))
      drop-shadow(
        0 0 10px color-mix(in oklab, var(--rare, var(--gold)) 70%, transparent)
      )
      drop-shadow(
        0 0 22px color-mix(in oklab, var(--rare, var(--gold)) 58%, transparent)
      )
      drop-shadow(
        0 0 42px color-mix(in oklab, var(--rare, var(--gold)) 32%, transparent)
      );
  }
  50% {
    filter: drop-shadow(0 26px 30px rgb(0 0 0 / 78%))
      drop-shadow(
        0 0 16px color-mix(in oklab, var(--rare, var(--gold)) 95%, transparent)
      )
      drop-shadow(
        0 0 38px color-mix(in oklab, var(--rare, var(--gold)) 88%, transparent)
      )
      drop-shadow(
        0 0 78px color-mix(in oklab, var(--rare, var(--gold)) 58%, transparent)
      );
  }
}
@media (prefers-reduced-motion: reduce) {
  .inspect-card.shiny img,
  .inspect-card.loud img {
    animation: none;
  }
}
.inspect-card[data-rarity="Rare"] {
  --rare: #8fb6da;
}
.inspect-card[data-rarity="Double Rare"] {
  --rare: #f0b53f;
}
.inspect-card[data-rarity="ACE SPEC Rare"] {
  --rare: #e2574a;
}
.inspect-card[data-rarity="Ultra Rare"] {
  --rare: #a97ae6;
}
.inspect-card[data-rarity="Illustration Rare"] {
  --rare: #46c39a;
}
.inspect-card[data-rarity="Special Illustration Rare"] {
  --rare: #ec5fa4;
}
.inspect-card[data-rarity="Shiny Rare"] {
  --rare: #4fd2e6;
}
.inspect-card[data-rarity="Shiny Ultra Rare"] {
  --rare: #6fe3d0;
}
.inspect-card[data-rarity="Hyper Rare"] {
  --rare: #ffc93c;
}
.inspect-card[data-rarity="Mega Hyper Rare"] {
  --rare: #ffe6a3;
}
.inspect-card[data-rarity="Black White Rare"] {
  --rare: #dfe9f5;
}
@media (prefers-reduced-motion: reduce) {
  .inspect-card,
  .inspect.open .inspect-card.held {
    transition: none;
    transform: none;
  }
}
/* Pictures arrive over a placeholder rather than appearing out of nothing.
   The skeleton is the image's own background, so nothing needs wrapping, and
   it is removed the moment the picture has pixels - card art and cut-out
   product shots are transparent and a shimmer left underneath would show
   through them. */
img[src^="/media/"],
img[src^="/cardart/"],
img[src^="/packs/"] {
  background: linear-gradient(
      100deg,
      var(--raised) 28%,
      var(--line-bright) 48%,
      var(--raised) 68%
    )
    0 0 / 320% 100%;
  border-radius: inherit;
  animation: skeleton 1.4s linear infinite;
  opacity: 0;
  transition: opacity 260ms ease;
}
img[src^="/media/"][data-loaded],
img[src^="/cardart/"][data-loaded],
img[src^="/packs/"][data-loaded] {
  background: none;
  animation: none;
  opacity: 1;
}
@keyframes skeleton {
  from {
    background-position: 100% 0;
  }
  to {
    background-position: -100% 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  img[src^="/media/"],
  img[src^="/cardart/"],
  img[src^="/packs/"] {
    animation: none;
    transition: none;
    background: var(--raised);
  }
}
/* The enlarged card turns over. The tilt stays on the figure and the flip
   lives on an inner element, so the two transforms compose instead of
   fighting over the same property. */
.inspect-flip {
  position: relative;
  /* The card is a 3D object and the caption is a flat sibling in the same
     stacking context, so a card mid-turn intersected the name underneath it -
     the text cut through the card. Giving the card its own layer above keeps
     the turn in front of everything it passes. */
  z-index: 2;
  transform-style: preserve-3d;
  /* A full turn wants a steadier curve than a half one: the ease-out that
     made a flip land nicely makes a spin look like it stalls halfway. */
  transition: transform 760ms cubic-bezier(0.4, 0.05, 0.25, 1);
  transform: rotateY(calc(var(--turns, 0) * 1turn));
  cursor: pointer;
}
.inspect-face {
  backface-visibility: hidden;
}
.inspect-face.front {
  display: block;
}
/* The back is laid over the front so the pair share the front's size - the
   artwork decides how big the card is, and the back follows it. */
.inspect-face.back {
  position: absolute;
  inset: 0;
  transform: rotateY(180deg);
  display: grid;
  place-items: center;
  background:
    radial-gradient(80% 60% at 50% 30%, #2a2f5c, #14162c 70%), #12142a;
  border: 2px solid #3b4076;
  /* Follows the back artwork's own corners, for the same reason the front
     does - a box-shadow here drew a pale square outside them. */
  filter: drop-shadow(0 26px 30px rgb(0 0 0 / 78%));
}
.inspect-face.back .back-ring {
  width: 40%;
  height: auto;
  aspect-ratio: 1;
  border-width: 5px;
  box-shadow: inset 0 0 0 18px rgb(255 255 255 / 7%);
}
.inspect-face.back .back-core {
  width: 14%;
  height: auto;
  aspect-ratio: 1;
}
@media (prefers-reduced-motion: reduce) {
  .inspect-flip {
    transition: none;
  }
}
/* An estimated value, and the space it will occupy before it arrives - the
   row must not jump when the number lands. */
.card-value {
  min-width: 3.6em;
  text-align: right;
  font: 600 12px var(--mono);
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
  animation: value-in 260ms ease;
}
.card-value.waiting {
  display: inline-block;
  height: 0.8em;
  border-radius: 3px;
  background: var(--raised);
  animation: none;
}
@keyframes value-in {
  from {
    opacity: 0;
    transform: translateY(2px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.bulk-best li .card-value {
  margin-left: auto;
}
@media (prefers-reduced-motion: reduce) {
  .card-value {
    animation: none;
  }
}
/* A value that is also a way out.
 *
 * The number is somebody else's estimate, so it links to the listing it came
 * from rather than asking to be taken on faith. It is styled as a value first
 * and a link second - underlining every price would turn both the fan and the
 * tally into a page of links. */
a.card-value {
  color: var(--ink-dim);
  text-decoration: none;
  cursor: pointer;
  border-bottom: 1px solid transparent;
}
a.card-value:hover,
a.card-value:focus-visible {
  color: var(--gold);
  border-bottom-color: color-mix(in oklab, var(--gold) 55%, transparent);
}
/* ...under a card in the fan: centred beneath the name, on its own line. */
.card-value.on-card {
  display: block;
  min-width: 0;
  margin: 3px auto 0;
  width: fit-content;
  text-align: center;
  font-size: 12px;
  color: color-mix(in oklab, var(--gold) 78%, var(--ink-dim));
}
.card-value.on-card.waiting {
  width: 3.2em;
  height: 0.75em;
  margin-top: 5px;
}
/* ...and under the enlarged one, where it is the only number on screen. */
.card-value.on-inspect {
  justify-self: center;
  min-width: 0;
  margin-top: 2px;
  text-align: center;
  font-size: 15px;
  letter-spacing: 0.02em;
  color: var(--gold);
}
.card-value.on-inspect.waiting {
  width: 4em;
  height: 0.9em;
}
a.card-value.on-inspect:hover,
a.card-value.on-inspect:focus-visible {
  color: var(--gold);
  border-bottom-color: color-mix(in oklab, var(--gold) 70%, transparent);
}
/* Read, and there is no listing. It keeps the caption's rhythm without
   pretending a number is still on its way. */
.card-value.none {
  display: inline-block;
  min-width: 0;
  height: 0;
  animation: none;
}
/* The real back.
 *
 * This used to be an original drawn in CSS - a ring and a gold core - because
 * the printed back is The Pokemon Company's artwork. It is now the card back
 * itself, held locally and served from our own origin, which is what makes a
 * flipped card read as a card rather than as a placeholder.
 *
 * The image is already rounded and transparent at the corners, so the panel
 * underneath it gets no border and no fill of its own: any it had showed as a
 * ring of navy outside the artwork's own edge. */
.card-face.back,
.inspect-face.back {
  background: url("/card-back.webp") center / 100% 100% no-repeat;
  border: 0;
}
.card-face.back .back-ring,
.card-face.back .back-core,
.inspect-face.back .back-ring,
.inspect-face.back .back-core {
  display: none;
}
/* The product a run stands for, beside the run's own numbers. */
.bulk-product {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin: 0 0 var(--s3);
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--surface);
}
.bulk-product img {
  width: 72px;
  height: 72px;
  object-fit: contain;
  border-radius: var(--r1);
  background: var(--raised);
}
.bulk-product figcaption {
  min-width: 0;
  font-size: 13px;
  color: var(--ink-dim);
  overflow: hidden;
  text-overflow: ellipsis;
}
/* One pack's worth, against what a pack costs. Same shape as the run's stat
   bar, at the smaller weight a single pack deserves. */
.pack-worth {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--s3);
  width: min(100%, 640px);
  margin: var(--s4) auto 0;
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--surface);
}
.pack-worth span {
  display: flex;
  flex-direction: column;
  gap: 0.15em;
  min-width: 0;
}
.pack-worth small {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.pack-worth strong {
  font: 700 17px/1.1 var(--mono);
  color: var(--ink);
}
.pack-worth em {
  font-style: normal;
  font-size: 12px;
  color: var(--ink-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Gold for ahead, blue for behind - the same pair the luck verdict already
   uses. Not red/green: this is an estimate against an asking price, not a
   result, and a red number reads as an error. */
.pack-worth .verdict.up strong {
  color: var(--gold);
}
.pack-worth .verdict.down strong {
  color: var(--blue);
}
/* The floor switch. It sits under the total it governs, because the rule and
   the number it produced have to be read together - a total counting a
   different set of cards than the list below it, with the rule elsewhere on
   the page, is the confusion this is meant to prevent. */
.floor-toggle {
  align-self: start;
  margin-top: 4px;
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--ink-faint);
  font: 500 11px var(--font);
  letter-spacing: 0.01em;
  cursor: pointer;
}
.floor-toggle:hover,
.floor-toggle:focus-visible {
  border-color: var(--line-bright);
  color: var(--ink-dim);
}
.floor-toggle[aria-pressed="true"] {
  border-color: var(--gold-line);
  background: var(--gold-bg);
  color: var(--gold);
}
/* ------------------------------------------------------------ health */
.health-group {
  margin-top: var(--s4);
}
.health-group h4 {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  margin: 0 0 var(--s1);
  font: 600 15px var(--font);
  color: var(--ink);
}
.health-group h4 .n {
  font: 600 12px var(--mono);
  color: var(--ink-dim);
}
.health-note {
  margin: 0 0 var(--s3);
  font-size: 12px;
  color: var(--ink-faint);
  max-width: 70ch;
}
.health-table .c-product {
  width: 26%;
}
.health-table td.error {
  color: var(--ink-dim);
  font-size: 12px;
}
.health-table td.error .over {
  display: block;
  color: var(--ink-faint);
}
/* --------------------------------------------------------- available now */
/* Hides a class the script applies rather than counting children, so NOW_PEEK
   in app.js is the only place the number appears. Conditional on :not(.all),
   which means nothing has to restate what a shown card's display value is. */
.now.many:not(.all) .now-grid > .beyond-peek {
  display: none;
}
.now-more {
  display: none;
  width: 100%;
  grid-column: 1 / -1;
  margin-top: var(--s2);
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  background: transparent;
  color: var(--ink-dim);
  font: 500 13px var(--font);
  cursor: pointer;
}
.now.many .now-more {
  display: block;
}
/* --------------------------------------------------------------- set head */
.set-head {
  display: flex;
  align-items: center;
  gap: var(--s4);
  margin: 0 0 var(--s4);
}
.set-head-mark {
  height: 56px;
  width: auto;
  max-width: 160px;
  object-fit: contain;
  flex: none;
}
.set-head-text {
  min-width: 0;
  flex: 1 1 auto;
}
.set-head h2 {
  margin: 0;
  font: 700 22px/1.15 var(--font);
  letter-spacing: -0.01em;
  color: var(--ink);
}
.set-head-sub {
  margin: var(--s1) 0 0;
  font-size: 13px;
  color: var(--ink-dim);
}
.set-head .fav {
  position: static;
  flex: none;
}
@media (max-width: 560px) {
  .set-head-mark {
    height: 40px;
    max-width: 110px;
  }
  .set-head h2 {
    font-size: 20px;
  }
}

/* ------------------------------------------------- product page, narrow */

/* The shop table stacked every cell into a column a thousand pixels tall per
   shop. Each row is a card now: who and how much on the first line, state and
   the buy button on the second, shipping and freshness on the third with the
   monitor switch. The sparkline goes - it is a tap away on the price. */
/* On a phone, a signed-out visitor was given 222px of alert-limit inputs -
   directly above the shop table, and read-only, so nothing in them could be
   used. Measured on the live page at 390x844: the table started at y=1003,
   which is below the fold, and this panel was most of the reason.

   The panel still says what it says; it just says it in a sentence instead of
   in two number boxes nobody signed in can type into. The boxes come back the
   moment there is an account to attach them to. */
@media (max-width: 760px) {
  .hero-controls-readonly .limits,
  .hero-controls-readonly .hero-controls-title {
    display: none;
  }
  .hero-controls-readonly {
    padding-block: var(--s2);
  }
}
@media (max-width: 760px) {
  /* A table box still lays itself out from its column widths, so the card
     rows were squeezed into the width of the store column. Blocks, not a
     table, on a phone. */
  .listings,
  .listings tbody {
    display: block;
    width: 100%;
    table-layout: auto;
  }
  .listings colgroup,
  .listings thead {
    display: none;
  }
  .listings .store-name {
    max-width: none;
    white-space: normal;
  }
  .listings tbody tr {
    display: grid;
    grid-template-columns: auto auto minmax(0, 1fr) auto;
    /* Two lines, each one full.

       Three lines with one item at the left and one at the right left a void
       down the middle of every row, and nothing said which figure belonged
       with which - it read as elements dropped onto the row rather than
       arranged on it. So: who and how much on the first line, then the state,
       the shipping and the freshness gathered into one phrase on the second
       with the button at its end. The eye reads left to right along a line
       instead of hunting diagonally across three. */
    grid-template-areas:
      "store store store  price"
      "state ship  age    buy"
      "flag  .     .      toggle";
    column-gap: var(--s3);
    row-gap: var(--s2);
    align-items: center;
    padding: var(--s3) var(--s3);
    border-bottom: 1px solid var(--line);
  }
  .listings tbody tr > td {
    display: block;
    padding: 0;
    border: 0;
  }
  .listings tbody tr > td[data-col="store"] {
    grid-area: store;
    padding: 0;
  }
  .listings tbody tr > td[data-col="price"] {
    grid-area: price;
    text-align: right;
  }
  .listings tbody tr > td[data-col="state"] {
    grid-area: state;
  }
  .listings tbody tr > td[data-col="buy"] {
    grid-area: buy;
    padding: 0;
    justify-self: end;
  }
  .listings tbody tr > td[data-col="ship"] {
    grid-area: ship;
  }
  .listings tbody tr > td[data-col="age"] {
    grid-area: age;
    text-align: left;
  }
  .listings tbody tr > td[data-col="toggle"] {
    grid-area: toggle;
    padding: 0;
    border: 0;
    justify-self: end;
  }
  /* This button is the only way into a listing's price history, and hiding it
     on phones did not move the history anywhere else - it removed it.

     It has to be a small chart, though. The sparkline is `width: 100%`, so
     given a 132px column it drew one nearly-flat line the width of the row,
     which read as a slider rather than as a price history - two line-like
     things per row once the price bar is counted. Sized to a thumbnail it
     reads as what it is. */
  /* The sparkline earned its place on a phone only as the way into the price
     history, and at 72px of nearly-flat line it paid for that with a third
     line of its own. The price cell carries `data-detail` instead, so the
     history is one tap on the figure it is the history of - which is where
     someone would look for it anyway. */
  .listings tbody tr > td[data-col="spark"] {
    display: none;
  }
  /* A named home, or it auto-places into whichever cell is free and shoves the
     row about. Bottom left, away from the Buy button it must never be
     mistaken for. */
  .listings tbody tr > td[data-col="flag"] {
    grid-area: flag;
    padding: 0;
    justify-self: start;
  }
  /* A dash under a "Shipping" column heading means "we do not know". With the
     heading hidden, as it is on a phone, it is a dash in the middle of a row
     meaning nothing. */
  .listings .ship.none {
    display: none;
  }
  .listings tbody tr > td[data-col="store"] > a.store-line {
    min-height: 44px;
  }
}

/* The hero's four figures sat in one column beside a large picture, each
   number wrapping onto the next line. Picture on top, figures two by two. */
@media (max-width: 560px) {
  .product-hero {
    grid-template-columns: minmax(0, 1fr);
    justify-items: start;
  }
  .hero-shot {
    width: 96px;
    height: 96px;
  }
  .hero-stats {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--s3) var(--s4);
    width: 100%;
  }
  .hero-stats > span {
    padding: 0;
    margin: 0;
    border-right: 0;
  }
  .hero-stats strong {
    font-size: 17px;
  }
  /* The market strip: four figures in a row that a phone made into four
     narrow columns, "0% about level" breaking three ways. Two by two. */
  .trend-stats {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--s3) var(--s4);
    margin-left: 0;
    width: 100%;
  }
}

/* ------------------------------------------------------------ fan actions */

.fan-actions {
  display: flex;
  justify-content: center;
  gap: var(--s3);
  margin-top: var(--s4);
}
.stage-sealed .fan-actions,
.stage-tearing .fan-actions,
.stage-revealing .fan-actions {
  display: none;
}

/* On a phone the header is two rows and a search box - a fifth of the screen.
   Pinned, it took that fifth from every view and hid the top row of a fan of
   cards. It scrolls away with the page there; the search is one flick up. */
@media (max-width: 760px) {
  .masthead {
    position: static;
  }
}

/* ------------------------------------------------------ inspect stepping */

.inspect-step {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--line-bright);
  background: color-mix(in oklab, var(--ground) 70%, transparent);
  color: var(--ink);
  font: 400 26px/1 var(--font);
  cursor: pointer;
  z-index: 2;
}
.inspect-step.prev {
  left: max(12px, calc(50% - min(37vw, 190px) - 64px));
}
.inspect-step.next {
  right: max(12px, calc(50% - min(37vw, 190px) - 64px));
}
.inspect-step:hover {
  border-color: var(--gold);
  color: var(--gold);
}
.inspect-step:disabled {
  opacity: 0.25;
  cursor: default;
}
.inspect-count {
  position: fixed;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  font: 500 12px var(--mono);
  color: var(--ink-faint);
  letter-spacing: 0.06em;
}
@media (max-width: 560px) {
  /* Thumbs do the stepping on a phone; the arrows would sit on the card. */
  .inspect-step {
    display: none;
  }
}
.bulk {
  scroll-margin-top: calc(var(--sticky-top, 0px) + var(--s3));
}

/* On a phone one set per row was three hundred pixels each, fourteen sets,
   four screens of scrolling to reach the last one. Two per row, art above the
   name, and the whole list fits in two. */
@media (max-width: 640px) {
  .chooser-grid {
    grid-template-columns: 1fr 1fr;
    gap: var(--s3);
  }
  .chooser-card {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
    padding: var(--s3);
    gap: var(--s2);
  }
  .chooser-meta strong {
    font-size: 15px;
  }
}

/* ------------------------------------------------------------ alert queue */

.alert-queue-line {
  margin: 0;
  font-size: 13px;
  color: var(--ink-dim);
  max-width: 70ch;
}
.alert-queue.bad {
  border-color: var(--red-line);
  background: var(--red-bg);
}
.alert-queue.bad .alert-queue-line {
  color: var(--ink);
}
.alert-queue.wait {
  border-color: var(--blue-line);
}

/* --------------------------------------------------------- account panel */

.account-security h3 {
  margin: var(--s4) 0 var(--s2);
  font: 600 15px var(--font);
}
.account-security h3:first-child {
  margin-top: 0;
}
.session-list {
  list-style: none;
  margin: 0 0 var(--s3);
  padding: 0;
  display: grid;
  gap: var(--s2);
}
/* One row per browser you are signed in on, and it has to be readable at a
   glance because the question it answers is "is one of these not me".
 *
 * It was two spans pushed to opposite ends of a flex row: the device on the
 * left, "active now · since 5 Sept" on the right, and at a narrow width they
 * wrapped into two lines with no indication of which belonged to which. Now
 * the shape is fixed - a picture of the kind of device, then what it is, then
 * when it was last used - so the eye reads down a column instead of hunting
 * across a row. */
.sess {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  background: var(--ground);
}
.sess-icon {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: var(--r1);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink-dim);
}
.sess-icon svg {
  width: 18px;
  height: 18px;
}
.sess-main {
  display: grid;
  gap: 2px;
  min-width: 0;
}
.sess-what {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font: 600 14px var(--font);
  color: var(--ink);
}
.sess-meta {
  display: flex;
  align-items: baseline;
  gap: 6px;
  color: var(--ink-faint);
  font-size: 12px;
}
.sess-dot {
  color: var(--line-bright);
}
/* The one you are reading this on. Marked rather than sorted to the top: the
   list is in the order the sessions were opened, and reordering it would make
   a returning reader wonder whether something had changed. */
.sess.current {
  border-color: var(--gold-line);
  background: var(--gold-bg);
}
.sess.current .sess-icon {
  border-color: var(--gold-line);
  color: var(--gold);
}
.sess-here {
  padding: 1px 8px;
  border-radius: 999px;
  border: 1px solid var(--gold-line);
  background: var(--ground);
  color: var(--gold);
  font: 600 11px var(--font);
  font-style: normal;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
/* A live session says so with a dot as well as a word, because "active now"
   and "last seen 4 minutes ago" are two lines of similar length and the
   difference between them is the whole point of the list. */
.sess-live {
  display: flex;
  align-items: center;
  gap: 5px;
  color: var(--ink-dim);
}
/* Quiet until the row is pointed at, because on a list of your own browsers
   the common case is reading it, not acting on it - and a row of End buttons
   at full strength reads as a list of things that need dealing with. Always
   visible on a touch screen, where there is no hover to reveal it. */
.sess-end {
  padding: 5px 11px;
  font-size: 12px;
  opacity: 0.55;
}
.sess:hover .sess-end,
.sess-end:focus-visible {
  opacity: 1;
}
.sess-end:hover {
  border-color: var(--red);
  color: var(--red);
}
.sess-end[disabled] {
  opacity: 0.4;
  cursor: progress;
}
@media (hover: none) {
  .sess-end {
    opacity: 1;
  }
}

.sess-live::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--gold);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 22%, transparent);
}
.password-form {
  display: grid;
  gap: var(--s3);
  max-width: 360px;
}
.password-form label {
  display: grid;
  gap: var(--s1);
  font-size: 12px;
  color: var(--ink-dim);
}
.password-form input {
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  background: var(--ground);
  color: var(--ink);
  font: 500 15px var(--font);
}
.password-form input:focus {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}
.password-form .primary {
  justify-self: start;
}

/* --------------------------------------------------------- sign-in panel */

dialog.signin {
  width: min(420px, 94vw);
}

/* The same dialog on a phone, as a sheet.
 *
 * Centred it is a 420px box holding 419px of content, and a phone with the
 * keyboard up leaves about 380px of visible page - so the box did not fit and
 * the reader had to scroll a dialog, which is the one thing a dialog exists to
 * save them from. Worse, the inputs were 12px, and Safari zooms the whole page
 * whenever a focused field is under 16px: the form got bigger at the exact
 * moment there was less room for it.
 *
 * So: pinned to the bottom, full width, rounded only at the top. The fields
 * sit where the thumb already is and directly above the keyboard, the brand
 * and the buttons stay put, and only the middle scrolls. Bounded in dvh,
 * which is the viewport you can see rather than the one the browser had
 * before its own chrome appeared.
 *
 * [open] on the flex rule, because a bare `display: flex` on a dialog beats
 * the `display: none` the browser gives a closed one, and the sign-in panel
 * would sit at the bottom of every page whether or not anybody asked for it. */
@media (max-width: 560px) {
  dialog.signin {
    width: 100%;
    max-width: none;
    max-height: 92dvh;
    margin: auto auto 0;
    border-bottom: 0;
    border-radius: var(--r2) var(--r2) 0 0;
  }
  dialog.signin[open] {
    display: flex;
    flex-direction: column;
  }
  dialog.signin .signin-body {
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  dialog.signin .signin-foot {
    position: sticky;
    bottom: 0;
    padding: var(--s3) var(--s4) calc(var(--s4) + env(safe-area-inset-bottom));
    background: var(--surface);
    border-top: 1px solid var(--line);
  }
  /* Two full-width buttons rather than two small ones in the corner: this is
     the last thing the thumb has to hit and there is width going spare. */
  dialog.signin .signin-foot button {
    flex: 1;
    min-height: 46px;
  }
  dialog.signin .signin-tabs [role="tab"] {
    min-height: 44px;
  }
}

/* 16px, or Safari zooms.
 *
 * Any focused input under 16px makes iOS scale the page up, and it does not
 * scale back when the field is left. On a form of four fields that is the
 * difference between filling it in and pinching around it. Applied on touch
 * as well as by width, because a tablet is wide and still has a keyboard that
 * covers half the screen. */
@media (max-width: 560px), (hover: none) {
  .signin-body input:not([type="checkbox"]) {
    font-size: 16px;
  }
}
.signin-brand {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s4) var(--s4) var(--s3);
}
.signin-brand img {
  border-radius: 50%;
  box-shadow: 0 0 0 1px var(--line-bright);
}
.signin-brand h2 {
  margin: 0;
  font: 700 17px var(--font);
  letter-spacing: -0.01em;
}
.signin-brand p {
  margin: 2px 0 0;
  color: var(--ink-dim);
  font-size: 12px;
}
.signin-tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  margin: 0 var(--s4);
  padding: 3px;
  gap: 2px;
  background: var(--ground);
  border: 1px solid var(--line);
  border-radius: 999px;
}
.signin-tabs [role="tab"] {
  border: 0;
  background: transparent;
  border-radius: 999px;
  min-height: 32px;
  color: var(--ink-dim);
  font: 500 13px var(--font);
  cursor: pointer;
}
.signin-tabs [role="tab"][aria-selected="true"] {
  background: var(--raised);
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--line-bright);
}
.signin-body small {
  display: block;
  margin-top: 2px;
  color: var(--ink-faint);
  font-size: 11px;
}
/* The panel that replaces the form once the link is on its way. It carries no
   input, so the body would otherwise collapse to the height of two lines and
   the dialog would jump as it swapped. */
#signup-sent .signin-body {
  min-height: 132px;
}
#signup-sent h3 {
  margin: 0;
  font: 600 15px var(--font);
  color: var(--ink);
}
.signin-body input:focus {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}
.reveal-wrap {
  position: relative;
  display: block;
}
.reveal-wrap input {
  width: 100%;
  padding-right: 60px;
}
.reveal {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--ink-dim);
  font: 500 11px var(--font);
  cursor: pointer;
}
.reveal:hover {
  color: var(--ink);
  border-color: var(--line-bright);
}
/* The box sits with the fields it qualifies, not down with the buttons: it is
   part of answering "who is this", not part of confirming it.
 *
 * Qualified by .signin-body because the rules it has to beat are - a label in
 * there is a grid that stacks its input above its text, which is right for a
 * field and turns a checkbox into a gold square with a caption underneath,
 * and every input in there is given the height and border of a text field. */
.signin-body label.signin-keep {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin: 0;
  color: var(--ink-dim);
  font: 500 13px var(--font);
  cursor: pointer;
}
.signin-body .signin-keep input {
  flex: none;
  width: 15px;
  height: 15px;
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  margin: 0;
  accent-color: var(--gold);
  cursor: pointer;
}

.signin-note {
  margin: 0;
  color: var(--ink-faint);
  font-size: 12px;
}
.signin-note a {
  color: var(--ink-dim);
}
.hero-controls input[readonly] {
  color: var(--ink-dim);
  border-style: dashed;
}

/* ------------------------------------------------------------- notices */

.notice {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translate(-50%, 12px);
  padding: var(--s2) var(--s4);
  border: 1px solid var(--line-bright);
  border-radius: 999px;
  background: var(--raised);
  color: var(--ink);
  font: 500 13px var(--font);
  box-shadow: 0 12px 30px -12px rgb(0 0 0 / 60%);
  opacity: 0;
  pointer-events: none;
  transition:
    opacity 160ms ease,
    transform 160ms ease;
  z-index: 60;
}
.notice.show {
  opacity: 1;
  transform: translate(-50%, 0);
}
.notice[data-tone="bad"] {
  border-color: var(--red-line);
  background: var(--red-bg);
  color: var(--ink);
}

/* ----------------------------------------------------------- welcome */

.welcome-strip {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: var(--s5);
  margin: 0 0 var(--s5);
  padding: var(--s4) var(--s5);
  border: 1px solid var(--gold-line);
  border-radius: var(--r2);
  background:
    radial-gradient(
      80% 120% at 0% 0%,
      color-mix(in oklab, var(--gold) 9%, transparent),
      transparent 60%
    ),
    var(--surface);
}
.welcome-strip h2 {
  margin: 0 0 var(--s2);
  font: 700 20px/1.2 var(--font);
  letter-spacing: -0.01em;
}
.welcome-strip p {
  margin: 0;
  color: var(--ink-dim);
  font-size: 13px;
  max-width: 60ch;
}
.welcome-side {
  display: grid;
  align-content: center;
  gap: var(--s3);
}
.welcome-actions {
  display: flex;
  gap: var(--s2);
  flex-wrap: wrap;
}
@media (max-width: 760px) {
  .welcome-strip {
    grid-template-columns: 1fr;
    padding: var(--s4);
  }
}
/* On a phone this cost 325px of the 844 available and stacked two paragraphs
   above the fold, so the first screen of a stock watcher held no stock: no
   product, no price, nothing in stock. The heading still says what the site
   is and the buttons still offer an account - which is the whole job of the
   panel - and the prose is a scroll away rather than in front of the thing
   somebody came for.

   Scoped to .welcome-strip on purpose: .welcome-actions also appears inside
   .trend.welcome on three other views, where there is no grid to collapse
   and nothing to hide. */
@media (max-width: 560px) {
  .welcome-strip > div:first-child > p,
  .welcome-strip .welcome-side > p:not(.welcome-actions) {
    display: none;
  }
  .welcome-strip {
    gap: var(--s3);
  }
}
.trend.welcome .welcome-actions {
  margin-top: var(--s3);
}
.missing code {
  font: 500 12px var(--mono);
  color: var(--ink-dim);
}

/* The checks table on a phone.

   The global phone rule turns every `tbody td` into an inline-block, but the
   <table> itself stayed `display: table` with the <colgroup> the dashboard
   emits - so the cells were laid out inside a fixed 85px first column. Every
   shop name was ellipsised to a single character at 390px and to nothing at
   all at 360px, product titles broke mid-word over five lines, and one row
   stood 442px tall. The table has to leave table layout entirely, the way the
   listings table already does. */
@media (max-width: 760px) {
  .health-table,
  .health-table tbody,
  .health-table tbody tr,
  .health-table tbody td {
    display: block;
    width: auto;
  }
  .health-table {
    table-layout: auto;
  }
  .health-table colgroup {
    display: none;
  }
  .health-table tbody tr {
    padding: var(--s2) 0;
    border-bottom: 1px solid var(--line);
  }
  /* The global phone rule pads every cell 16px each side, which on a stacked
     row becomes vertical air between four lines that belong together. */
  .health-table tbody td {
    padding: 0 var(--s3);
  }
  .health-table tbody tr:last-child {
    border-bottom: 0;
  }
  .health-table .store-name {
    max-width: none;
    overflow: visible;
    text-overflow: clip;
    white-space: normal;
  }
  .health-table td a {
    overflow-wrap: normal;
  }
  /* The head row is hidden on phones, so each value has to say what it is.
     "3d" on its own line means nothing. */
  .health-table td[data-col="error"]::before {
    content: "Last error: ";
    color: var(--ink-faint);
  }
  .health-table td[data-col="age"]::before {
    content: "Checked ";
    color: var(--ink-faint);
  }
  .health-table .store-line {
    display: flex;
    align-items: center;
    min-height: 44px;
  }
}

/* ------------------------------------------------- tap targets, phone */

@media (max-width: 760px) {
  .chip,
  .segmented .chip,
  .pack-picker .chip,
  .bulk-btn,
  .ghost,
  .primary,
  .buy-btn,
  .floor-toggle,
  .fav {
    min-height: 44px;
  }
  .icon-btn,
  .utility .icon-btn {
    width: 44px;
    height: 44px;
  }
  /* Height alone was not enough: several of these are icon buttons with a
     fixed width, so they stayed 26 or 30 pixels across however tall they got.
     A target has two dimensions. */
  .fav {
    min-width: 44px;
  }
  .buy-out {
    top: 0;
    right: 0;
    width: 44px;
    height: 44px;
    border-radius: var(--r1);
  }
  /* ...and the card link underneath has to stop where that target starts, or
     the two share pixels and the wrong one answers the tap. */
  .buy-main {
    padding-right: 44px;
  }
  .finder-clear {
    right: 2px;
    width: 44px;
    height: 44px;
  }
  .finder input {
    padding-right: 48px;
  }
  /* A text link is as tappable as its line box, which for 13px type is
     sixteen pixels. Breadcrumbs are the only way back up on a deep page. */
  .crumbs {
    min-height: 44px;
  }
  .crumb,
  a.crumb {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 4px;
    margin: 0 -4px;
  }
  .footer-group {
    gap: var(--s2);
  }
  .footer-links a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  a.store-line {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  .shop-set-link {
    display: inline-flex;
    align-items: center;
    align-self: center;
    min-height: 44px;
    padding: 0 var(--s2);
    margin-right: calc(var(--s2) * -1);
  }
  .shop-what {
    min-height: 44px;
  }
  /* Every remaining control a thumb has to find. Measured, not guessed:
     the brand mark was 30x30, the headline counters 40 tall, the alert-limit
     field 34, the "Sign in" nudge 18, and the checks table's product link 16. */
  .brand {
    min-width: 44px;
    min-height: 44px;
  }
  .stat {
    min-height: 44px;
  }
  .now-head,
  .now-more,
  .ready-name {
    min-height: 44px;
  }
  .limit input,
  .finder input {
    min-height: 44px;
  }
  .linkish {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  .health-table td a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  /* The PriceCharting attribution under the market figure: an outbound link
     on its own line, at 11px, with nothing near it. */
  .market-ref a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  /* The two alert-limit fields. `.hero-controls .limit input` pins these to
     34px from a more specific selector, so the phone rule has to match it. */
  .hero-controls .limit input {
    min-height: 44px;
  }
  /* Width as well as height: this one is the word "Sign in". */
  .linkish {
    min-width: 44px;
  }
  /* Under 12px is not reading, it is guessing. These carry the market
     comparison, the currency and the per-set stock counts. */
  .cmp small,
  .shop-price small,
  .shop-set-live,
  .shop-set-none,
  .sek,
  .cur {
    font-size: 12px;
  }
  /* iOS zooms the whole page in when a focused field is under 16px, and the
     page never zooms back out. */
  .finder input,
  input[type="text"],
  input[type="email"],
  input[type="password"],
  input[type="number"],
  input[type="search"] {
    font-size: 16px;
  }
}

/* ------------------------------------------------------- personal alerts */

.alerts-panel h3 {
  display: flex;
  align-items: center;
  gap: var(--s2);
}
.on-pill {
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--gold-bg);
  border: 1px solid var(--gold-line);
  color: var(--gold);
  font: 600 11px var(--font);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.alert-steps {
  margin: var(--s2) 0 var(--s3);
  padding-left: 1.4em;
  color: var(--ink-dim);
  font-size: 13px;
  display: grid;
  gap: var(--s2);
}
.topic {
  font: 500 12px var(--mono);
  color: var(--ink);
  background: var(--ground);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  padding: 2px 8px;
  word-break: break-all;
}
.ghost.tiny {
  min-height: 0;
  padding: 2px 8px;
  font-size: 11px;
  margin-left: var(--s1);
}
.fav-watch {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
}
.fav-watch > a {
  flex: 1 1 240px;
  min-width: 0;
}
.fav-limit {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  font-size: 12px;
  color: var(--ink-dim);
}
.fav-limit input,
.limits.personal input {
  width: 92px;
  padding: var(--s1) var(--s2);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  background: var(--ground);
  color: var(--ink);
  font: 600 14px var(--mono);
  text-align: right;
}
.limit-note {
  color: var(--ink-faint);
  font-size: 12px;
  align-self: center;
}

/* The date on a policy page. Small, and directly under the lede, because it
   is the first thing somebody checks when they want to know whether the page
   they are reading is the one they agreed to. */
.prose-stamp {
  margin: calc(var(--s3) * -1) 0 var(--s5);
  color: var(--ink-faint);
  font-size: 12px;
}

/* A footnote under a table, for the thing the column cannot say itself. */
.table-note {
  margin: 0;
  padding: var(--s3) var(--s4) var(--s4);
  color: var(--ink-faint);
  font-size: 12px;
  line-height: 1.5;
  max-width: 76ch;
}

/* The browse/sort row is wider than a phone; it scrolls itself, like the
   region strip, instead of widening the page. */
@media (max-width: 760px) {
  .sort-row {
    overflow-x: auto;
    scrollbar-width: none;
    flex-wrap: nowrap;
    -webkit-overflow-scrolling: touch;
    mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent);
    -webkit-mask-image: linear-gradient(
      to right,
      #000 calc(100% - 32px),
      transparent
    );
    padding-right: var(--s5);
  }
  .sort-row::-webkit-scrollbar {
    display: none;
  }
  .sort-row > * {
    flex: none;
  }
  .limits {
    flex-wrap: wrap;
  }
  .limit label {
    white-space: nowrap;
  }
}

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

/* The nav is not a box of its own any more - its three groups are columns of
   the footer's own grid, which is what lets them space evenly across it. */
.footer-links {
  display: grid;
  grid-column: 2 / -1;
  grid-template-columns: subgrid;
}
/* Named for a screen reader, which navigates by heading, and not drawn. */
.footer-map {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.footer-group {
  display: grid;
  /* align-content: start so a column of two links does not stretch its gaps
     to match a column of four - which is what made the old one look broken. */
  align-content: start;
  gap: 6px;
}
.footer-head {
  margin: 0 0 2px;
  font: 600 11px var(--font);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
/* Which of a shop's several this row is. Only appears when a shop has more
   than one listing in the group, so it is never noise on a normal row. */
.store-variant {
  display: block;
  margin-left: calc(26px + var(--s3));
  color: var(--ink-faint);
  font: 500 11px var(--font);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
@media (max-width: 760px) {
  .store-variant {
    margin-left: calc(34px + var(--s3));
    font-size: 12px;
  }
}
/* The start of the record, said where the record is shown. */
.hero-since {
  margin: 2px 0 0;
  color: var(--ink-faint);
  font-size: 12px;
}
.footer-bottom {
  display: grid;
  gap: var(--s2);
}
.footer-fine {
  max-width: 78ch;
  margin: 0;
  line-height: 1.6;
  color: var(--ink-faint);
}
.footer-rights {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2) var(--s4);
  margin: 0;
  color: var(--ink-faint);
}
.footer-rights a {
  color: var(--ink-dim);
}
.footer-rights a:hover {
  color: var(--gold);
}

@media (max-width: 1080px) {
  .footer-top {
    grid-template-columns: minmax(0, 1fr) repeat(3, minmax(6rem, auto));
  }
}
@media (max-width: 760px) {
  .footer-top {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .footer-brand {
    grid-column: 1 / -1;
  }
  .footer-links {
    grid-column: 1 / -1;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s5) var(--s4);
  }
}
.loading-note {
  margin: var(--s5) 0;
  color: var(--ink-faint);
  font-size: 13px;
  text-align: center;
}

/* --------------------------------------------------------- primary nav */

.mast-tools {
  flex-wrap: wrap;
}
.primary-nav {
  display: flex;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  flex: none;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}
/* The scrollbar is hidden, so on a phone the last item was sliced flat by the
   pill's edge with nothing to say there was more to the right. A word that
   dissolves reads as continuing; a word cut square reads as broken.
   The class is set from JS because CSS cannot ask whether a box overflows -
   container scroll-state queries style a container's DESCENDANTS, and this is
   the container. Without the class nothing changes, so a browser that never
   runs it sees exactly what it saw before. */
.primary-nav.more-right {
  mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
}
.primary-nav.more-left {
  mask-image: linear-gradient(to right, transparent, #000 20px);
}
.primary-nav.more-left.more-right {
  mask-image: linear-gradient(
    to right,
    transparent,
    #000 20px calc(100% - 20px),
    transparent
  );
}
.primary-nav::-webkit-scrollbar {
  display: none;
}
.primary-nav a {
  flex: none;
  padding: 0 var(--s3);
  min-height: 30px;
  display: inline-flex;
  align-items: center;
  border-radius: 999px;
  color: var(--ink-dim);
  font: 500 13px var(--font);
  text-decoration: none;
  white-space: nowrap;
}
.primary-nav a:hover {
  color: var(--ink);
}
.primary-nav a[aria-current="page"] {
  background: var(--raised);
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--line-bright);
}
.mast-tools .finder {
  flex: 1 1 260px;
}
@media (max-width: 760px) {
  .primary-nav {
    order: 2;
    width: 100%;
  }
  .primary-nav a {
    min-height: 44px;
  }
  /* Two pixels between tabs is one tap landing on the wrong one. The nav
     already scrolls horizontally, so the extra gaps cost no page width. */
  .primary-nav {
    gap: var(--s2);
  }
  .mast-tools .finder {
    order: 1;
    flex-basis: 100%;
  }
}

/* ------------------------------------------------------------- one shop */

.shop-head .shop-mark {
  border-radius: var(--r1);
  object-fit: contain;
  background: var(--raised);
  flex: none;
}
.shop-standing {
  margin: var(--s1) 0 0;
  font-size: 13px;
  color: var(--ink-dim);
}
.shop-set {
  margin-bottom: var(--s5);
}
.shop-set h3 {
  display: flex;
  align-items: baseline;
  /* Four things now sit on this line - the name, the release date, the stock
     count and the link - and at 360px they do not all fit. Wrapping is the
     difference between a second line and a page that scrolls sideways. */
  flex-wrap: wrap;
  gap: var(--s3);
  margin: 0 0 var(--s2);
  font: 600 15px var(--font);
}
/* The sections are ordered newest set first. Saying when each one came out is
   what makes that order legible instead of merely correct. */
.shop-set-when {
  font: 500 12px var(--font);
  color: var(--ink-faint);
}
.shop-set-link {
  margin-left: auto;
  font: 500 12px var(--font);
  color: var(--ink-faint);
}
.shop-set-link:hover {
  color: var(--gold);
}
.shop-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--surface);
  overflow: hidden;
}
.shop-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 210px) auto auto;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  border-top: 1px solid var(--line);
}
.shop-row:first-child {
  border-top: 0;
}
/* On a phone this header is a flex row that never wrapped, so a 48px logo and
   a 134px toggle held their width while the text column - the name, the
   summary line and the standing - was squeezed to 82px and wrapped over
   eight lines. The header ate 39% of the screen to say what fits on two.
   Wrap it, and give the toggle its own row. */
@media (max-width: 760px) {
  .set-head.shop-head {
    flex-wrap: wrap;
  }
  .shop-head .set-head-text {
    flex: 1 1 calc(100% - 64px);
    min-width: 0;
  }
  .shop-head .shop-sold {
    flex: 0 0 100%;
    margin-left: 0;
    justify-content: center;
  }
}
/* A shop name is one long word often enough - "Samlargrottan", "Musen og
   Slottet" - and it was painting outside its own box rather than breaking. */
.shop-head h2 {
  overflow-wrap: anywhere;
}
/* The sold-out toggle sits at the far end of the shop header, away from the
   name and the standing, because it changes what the list below shows rather
   than saying anything about the shop. */
.shop-sold {
  margin-left: auto;
  flex: none;
}
.shop-empty {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
  margin: 0 0 var(--s5);
  padding: var(--s4);
  border: 1px dashed var(--line);
  border-radius: var(--r2);
  color: var(--ink-dim);
}
/* The same treatment an in-stock row gets in every table on the site: the
   tinted ground, the gold rail down the left edge, and a sold-out price
   stepped back so the difference is carried by more than the badge. A shop
   page where everything was sold out and everything looked identical was the
   complaint, and it was right - this page had a weaker tint than the tables
   and no rail at all. */
.shop-row.buy {
  background: var(--gold-bg);
  box-shadow: inset 3px 0 0 var(--gold-deep);
}
.shop-row:not(.buy) .shop-price {
  color: var(--ink-dim);
}
.shop-price.none {
  font: 500 12px var(--font);
}
.noprice {
  color: var(--ink-faint);
  font-weight: 500;
  white-space: nowrap;
}
/* How many of this set you could actually buy here, said once at the top of
   the section rather than only discoverable by reading every row. */
.shop-set-live,
.shop-set-none {
  align-self: center;
  font: 600 11px/1 var(--font);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  padding: 4px 8px;
  border-radius: 999px;
}
.shop-set-live {
  background: var(--gold-bg);
  border: 1px solid var(--gold-line);
  color: var(--gold-deep);
}
.shop-set-none {
  color: var(--ink-faint);
  border: 1px solid var(--line);
}
/* Arriving from a product page, the set that product belongs to is scrolled
   to and marked, so the click lands somewhere visible instead of at the top
   of a shop with twenty-five products. */
.shop-set {
  scroll-margin-top: calc(var(--sticky-top, 0px) + var(--s4, 16px));
}
.shop-set.came-from > h3 {
  box-shadow: inset 3px 0 0 var(--gold-deep);
  padding-left: var(--s3);
}
.shop-set.came-from .shop-list {
  border-color: var(--gold-line);
}
.shop-what {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-width: 0;
  color: inherit;
  text-decoration: none;
  font-weight: 500;
}
.shop-what img,
.shop-noart {
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: var(--r1);
}
/* At 34px the hatching would be three stripes and the glyph would fill the
   tile, so this one keeps the plate's border and ink and drops the pattern. */
.shop-noart.art-none {
  background: var(--surface);
}
.shop-noart svg {
  width: 15px;
  height: 15px;
}
.shop-what span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.shop-what:hover span {
  color: var(--gold);
}
.shop-price {
  font: 600 15px var(--mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.shop-price small {
  margin-left: 3px;
  color: var(--ink-faint);
  font: 500 11px var(--font);
}
.shop-cmp {
  display: grid;
  gap: 1px;
  min-width: 0;
}
.cmp {
  font: 600 12px var(--font);
}
.cmp.under {
  color: var(--gold);
}
.cmp.over {
  color: var(--blue);
}
.cmp.level,
.cmp.none {
  color: var(--ink-faint);
}
.shop-cmp small {
  color: var(--ink-faint);
  font-size: 11px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
a.store-line {
  color: inherit;
  text-decoration: none;
}
a.store-line:hover .store-name {
  color: var(--gold);
}
@media (max-width: 760px) {
  .shop-row {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "what  price"
      "cmp   cmp"
      "state buy";
    row-gap: var(--s2);
  }
  .shop-what {
    grid-area: what;
  }
  .shop-price {
    grid-area: price;
    text-align: right;
  }
  .shop-cmp {
    grid-area: cmp;
  }
  .shop-state {
    grid-area: state;
  }
  .shop-row > .buy-btn {
    grid-area: buy;
    justify-self: end;
  }
}

/* A listing the next scan has not reached: quiet, not alarming. */
.avail.new {
  color: var(--ink-faint);
  border-color: var(--line);
  background: transparent;
}

/* ================================================== phone type sizes, last

   These have to sit at the end of the sheet. Each of them is declared with a
   `font:` shorthand in its own section further down, and at equal specificity
   the later rule wins - an override placed with the other phone rules simply
   did nothing. Only the figures that carry data are raised: the market
   comparison, the currency lines and the per-set stock counts. The
   letterspaced uppercase micro-labels stay at 11px, which is what they are
   designed for. */
@media (max-width: 760px) {
  .cmp small,
  .shop-price small,
  .shop-set-live,
  .shop-set-none,
  .sek,
  .cur {
    font-size: 12px;
  }
}

/* ============================================== keeping cards ==============

   A card can go two places: favourites, which is the unnamed pile everyone
   gets, or a collection, which is a binder somebody named. The sheet lists
   the binders that exist and lets a new one be named on the spot, because
   sending someone elsewhere to make one first is how a save gets abandoned. */
.inspect-save {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 3;
  min-height: 44px;
  padding: 0 var(--s4);
  border-radius: 999px;
  border: 1px solid var(--gold-line);
  background: var(--gold-bg);
  color: var(--gold-deep);
  font: 600 13px var(--font);
  cursor: pointer;
}
.inspect-save:hover {
  background: var(--gold);
  color: var(--gold-ink);
}
.save-sheet {
  position: absolute;
  left: 50%;
  bottom: var(--s4);
  transform: translateX(-50%);
  z-index: 4;
  width: min(92vw, 340px);
  max-height: 60vh;
  overflow-y: auto;
  display: grid;
  gap: var(--s2);
  padding: var(--s4);
  border-radius: var(--r2);
  border: 1px solid var(--line-bright);
  background: var(--raised);
  box-shadow: 0 30px 60px -20px rgb(0 0 0 / 70%);
}
.save-head {
  margin: 0 0 var(--s1);
  color: var(--ink-dim);
  font-size: 13px;
}
.save-head strong {
  color: var(--ink);
}
.save-row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: 44px;
  padding: 0 var(--s3);
  border-radius: var(--r1);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  font: 500 14px var(--font);
  text-align: left;
  cursor: pointer;
}
.save-row:hover {
  border-color: var(--gold-line);
}
.save-row[aria-pressed="true"] {
  border-color: var(--gold-line);
  background: var(--gold-bg);
  color: var(--gold-deep);
}
.save-row small {
  margin-left: auto;
  color: var(--ink-faint);
  font-size: 12px;
}
.save-star {
  width: 1.1em;
  text-align: center;
}
.save-none {
  margin: 0;
  color: var(--ink-faint);
  font-size: 13px;
}
.save-new {
  display: flex;
  gap: var(--s2);
}
.save-new input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 44px;
  padding: 0 var(--s3);
  border-radius: var(--r1);
  border: 1px solid var(--line);
  background: var(--ground);
  color: var(--ink);
  font: 500 14px var(--font);
}
.save-close {
  min-height: 44px;
  border: 0;
  border-radius: var(--r1);
  background: transparent;
  color: var(--ink-dim);
  font: 600 13px var(--font);
  cursor: pointer;
}

/* The shelves. Art first: a kept card is recognised by its picture long
   before its name, so the name sits under it and stays quiet. */
.kept-shelf {
  margin: 0 0 var(--s5);
}
.kept-shelf h3 {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin: 0 0 var(--s3);
  font: 600 15px var(--font);
}
.kept-shelf h3 small {
  color: var(--ink-faint);
  font: 500 12px var(--font);
}
.kept-drop {
  margin-left: auto;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 4px var(--s3);
  min-height: 32px;
  background: transparent;
  color: var(--ink-faint);
  font: 500 12px var(--font);
  cursor: pointer;
}
.kept-drop:hover {
  border-color: var(--red);
  color: var(--red);
}
.kept-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: var(--s4);
  list-style: none;
  margin: 0;
  padding: 0;
}
.kept-card {
  display: grid;
  gap: var(--s2);
}
.kept-art {
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  line-height: 0;
}
.kept-art img {
  width: 100%;
  height: auto;
  filter: drop-shadow(0 10px 14px rgb(0 0 0 / 55%));
  transition: transform 160ms ease;
}
.kept-art:hover img {
  transform: translateY(-3px);
}
.kept-name {
  margin: 0;
  text-align: center;
  font: 500 13px var(--font);
  color: var(--ink);
}
.kept-name small {
  display: block;
  color: var(--ink-faint);
  font-size: 11px;
}
@media (max-width: 760px) {
  .kept-grid {
    grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
    gap: var(--s3);
  }
  .kept-drop {
    min-height: 44px;
  }
}

/* ================================================ cookie notice ===========

   Anchored to the bottom of the viewport and deliberately not modal: nothing
   here needs agreeing to before the page can be read, so nothing here blocks
   reading it. It sits above the content, holds its own width, and goes away
   for good on the one button it has. */
.cookie-note {
  position: fixed;
  z-index: 60;
  left: 50%;
  bottom: var(--s4);
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: var(--s4);
  width: min(calc(100vw - 2 * var(--s4)), 62rem);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line-bright);
  border-radius: var(--r2);
  background: var(--raised);
  box-shadow: 0 24px 50px -20px rgb(0 0 0 / 65%);
}
.cookie-note p {
  margin: 0;
  color: var(--ink-dim);
  font-size: 12px;
  line-height: 1.55;
}
.cookie-note a {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.cookie-note a:hover {
  color: var(--gold);
}
.cookie-ok {
  flex: none;
  min-height: 40px;
  padding: 0 var(--s4);
  border-radius: 999px;
  border: 1px solid var(--gold-line);
  background: var(--gold-bg);
  color: var(--gold-deep);
  font: 600 13px var(--font);
  cursor: pointer;
}
.cookie-ok:hover {
  background: var(--gold);
  color: var(--gold-ink);
}
@media (max-width: 760px) {
  .cookie-note {
    flex-direction: column;
    align-items: stretch;
    gap: var(--s3);
    bottom: var(--s2);
  }
  .cookie-ok {
    min-height: 44px;
  }
  /* Three link groups stacked two-up made a long tail on a phone. One wrapped
     row of links says the same thing in a third of the height, and the group
     names carry no information once they are all in view together. */
  .footer-links {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2) var(--s4);
  }
  .footer-group {
    display: contents;
  }
  .footer-head {
    display: none;
  }
  .footer-links a {
    min-height: 36px;
  }
}

/* ================================================ contact ================= */

.contact-form {
  display: grid;
  gap: var(--s4);
  max-width: 52rem;
  margin-top: var(--s4);
}
.contact-field {
  display: grid;
  gap: var(--s2);
}
.contact-field > span {
  font: 600 12px var(--font);
  letter-spacing: 0.02em;
  color: var(--ink-dim);
}
.contact-field > span small {
  margin-left: var(--s2);
  color: var(--ink-faint);
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0;
}
.contact-form textarea,
.contact-form input[type="text"] {
  width: 100%;
  padding: var(--s3);
  border-radius: var(--r1);
  border: 1px solid var(--line);
  background: var(--ground);
  color: var(--ink);
  font: 500 14px var(--font);
  line-height: 1.5;
  resize: vertical;
}
.contact-form textarea:focus,
.contact-form input[type="text"]:focus {
  border-color: var(--focus);
  outline: none;
}
/* Off-screen rather than display:none: a bot reading the markup fills it in,
   and a screen reader is told to skip it by the label's aria-hidden. */
.contact-trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
.contact-actions {
  display: flex;
  align-items: flex-start;
  gap: var(--s4);
  flex-wrap: wrap;
}
.contact-note {
  flex: 1 1 22rem;
  margin: 0;
  color: var(--ink-faint);
  font-size: 12px;
  line-height: 1.6;
}

/* ------------------------------------------------ the contact log --------- */

.msg-list {
  display: grid;
  gap: var(--s3);
  list-style: none;
  margin: 0;
  padding: 0;
}
.msg {
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--surface);
}
.msg.done {
  opacity: 0.62;
}
.msg-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s2) var(--s3);
  margin-bottom: var(--s3);
  font: 500 12px var(--font);
  color: var(--ink-faint);
}
.msg-who {
  color: var(--ink-dim);
}
.msg-who.anon,
.msg-reply.none {
  font-style: italic;
}
.msg-reply {
  color: var(--ink);
  font-family: var(--mono);
}
.msg-done {
  margin-left: auto;
}
.msg-body {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  line-height: 1.6;
  color: var(--ink);
}

/* ================================================ insights ================ */

.insight-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--s4);
}
.insight-stat {
  display: grid;
  gap: 2px;
}
.insight-stat small {
  color: var(--ink-faint);
  font: 600 11px var(--font);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.insight-stat strong {
  font: 700 22px var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.rank {
  display: grid;
  gap: var(--s2);
  list-style: none;
  margin: 0;
  padding: 0;
}
.rank-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(4rem, 12rem) auto;
  align-items: center;
  gap: var(--s3);
}
.rank-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  color: var(--ink);
}
.rank-name small {
  margin-left: var(--s2);
  color: var(--ink-faint);
  font-size: 12px;
}
/* The bar is the comparison; the number is the fact. Both, because a bar
   alone cannot be read off and a number alone cannot be scanned. */
.rank-bar {
  display: block;
  height: 6px;
  border-radius: 3px;
  background: var(--line);
  overflow: hidden;
}
.rank-bar i {
  display: block;
  width: var(--w, 0%);
  height: 100%;
  border-radius: 3px;
  background: var(--gold);
}
.rank-n {
  font: 600 13px var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
}
@media (max-width: 560px) {
  .rank-row {
    grid-template-columns: minmax(0, 1fr) auto;
  }
  .rank-bar {
    grid-column: 1 / -1;
  }
}

/* ============================================ reporting a listing ========= */

/* Quiet until wanted. A flag on every row that shouted would make a table of
   working listings look like a table of broken ones, so it is faint until the
   row is hovered - and never faint when it already carries a report. */
/* 52, not 40. The pill is 29px empty and 41px once it carries a count, and
   the cell's own 12px of padding left 16px for it - so a reported listing put
   its flag hard against the Buy button, one pixel from touching it. The
   padding goes and the column is as wide as the widest thing in it. */
.c-flag {
  width: 52px;
}
tbody td[data-col="flag"] {
  padding-left: 0;
  padding-right: 0;
  text-align: center;
}
/* The report control.
   Quiet until the row is hovered or it is focused, because on a page of
   thirty shops it is the least important thing in every row - right up to the
   moment somebody needs it. It carries a flag rather than a glyph so that
   what it does is legible without a column header, and it only turns red once
   a report actually exists, so an untouched row never looks broken. */
.flag-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-width: 28px;
  min-height: 28px;
  padding: 0 7px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--ink-faint);
  font: 600 12px var(--font);
  opacity: 0.4;
  cursor: pointer;
  transition:
    opacity 0.12s ease,
    color 0.12s ease,
    border-color 0.12s ease;
}
.flag-btn svg {
  width: 13px;
  height: 13px;
  flex: none;
}
tbody tr:hover .flag-btn,
.flag-btn:focus-visible {
  opacity: 1;
  border-color: var(--line);
}
.flag-btn:hover {
  border-color: var(--red);
  color: var(--red);
}
/* A row somebody has already reported stays visible without being loud: the
   count is the message, and the flag beside it says what the count counts. */
.flag-btn.flagged {
  opacity: 1;
  color: var(--red);
  border-color: var(--red-line);
  background: var(--red-bg);
}
.flag-btn.flagged:hover {
  border-color: var(--red);
}
.flag-n {
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
@media (prefers-reduced-motion: reduce) {
  .flag-btn {
    transition: none;
  }
}
.report-sheet {
  position: fixed;
  left: 50%;
  bottom: var(--s5);
  transform: translateX(-50%);
  z-index: 70;
}
.report-note {
  display: grid;
  gap: var(--s2);
}
.report-note > span {
  font: 600 12px var(--font);
  color: var(--ink-dim);
}
.report-note > span small {
  margin-left: var(--s2);
  color: var(--ink-faint);
  font-weight: 500;
}
.report-note textarea {
  width: 100%;
  padding: var(--s2) var(--s3);
  border-radius: var(--r1);
  border: 1px solid var(--line);
  background: var(--ground);
  color: var(--ink);
  font: 500 13px var(--font);
  resize: vertical;
}
.msg-kind {
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--ink);
}
.msg-sub {
  margin: 0 0 var(--s2);
  font-size: 12px;
  color: var(--ink-faint);
}
.msg-sub a {
  color: var(--ink-dim);
}
.msg-fetch {
  font-family: var(--mono);
  color: var(--red);
}
.msg-fetch.ok {
  color: var(--ink-faint);
}
@media (max-width: 760px) {
  .flag-btn {
    min-width: 44px;
    min-height: 44px;
    opacity: 1;
  }
}

/* A button that reads as a link. Used for "Forgotten your password?", which
   moves between panels of the open dialog rather than going anywhere - so it
   must not be an anchor, and must not look like the two real buttons under
   it either. */
.linkish {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--ink-dim);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.linkish:hover {
  color: var(--ink);
}

/* Rows past the eighth on the ready-to-buy page. Hidden by a class rather than
   by nth-child so the number lives in one place - READY_PEEK in app.js - and
   changing it does not mean changing a selector to match. */
.results:not(.all) .beyond-peek {
  display: none;
}
.ready-more {
  display: block;
  width: 100%;
  margin-top: var(--s2);
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  background: transparent;
  color: var(--ink-dim);
  font: 500 13px var(--font);
  cursor: pointer;
}
.ready-more:hover {
  color: var(--ink);
  border-color: var(--line-bright);
}

/* ----------------------------------------------------------------- deals */
/* A deal row is a ready-to-buy card with one extra column: what makes it a
   deal. The card's own grid is declared here rather than inherited, because
   .ready-card is four columns and this is five. */
.deal-card {
  grid-template-columns: 72px 1fr auto auto auto;
}
.deal-cut {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  padding-left: var(--s3);
  border-left: 1px solid var(--line);
}
.deal-pct {
  font: 700 17px var(--font);
  color: var(--gold);
  font-variant-numeric: tabular-nums;
}
.deal-saved {
  font-size: 11px;
  color: var(--ink-faint);
  white-space: nowrap;
}
.deal-cuts {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  margin-top: var(--s3);
}
.deal-cuts-label {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
/* Under 620px the card is already stacking; a fifth column would wrap into a
   lone number with no label beside it, so the margin moves under the price. */
@media (max-width: 620px) {
  .deal-cut {
    padding-left: 0;
    border-left: 0;
    align-items: flex-start;
  }
}

/* ------------------------------------------------------------ bought mark */
/* A tick beside the Buy button, on every listing row, for a signed-in reader.
   Quiet until it is on: an unpressed control here is one of eight in a row
   and must not compete with the price. */
/* A tick with a word next to it, under the button it is about.
 *
 * It was a 26px square carrying a bare checkmark, sitting above the Buy
 * button because the column is the width of that button and the two would not
 * share a line. Nothing said what it was for. Worse, the same row ends in the
 * owner's monitor switch, which is also a tick - so the row offered two
 * checkmarks meaning different things and labelled neither.
 *
 * The word does the work. It is 10px, which is small, but it is a label on a
 * control rather than something to read, and it only has to be legible at the
 * moment somebody wonders what the box is. */
.bought-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  width: 100%;
  min-height: 24px;
  margin-top: var(--s1);
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink-faint);
  font: 600 10px var(--font);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
}
/* Drawn as a checkbox because it behaves as one, and because the switch at the
   end of the row is drawn as a switch: two controls that do different things
   should not look the same. */
.bought-box {
  display: grid;
  place-items: center;
  flex: none;
  width: 13px;
  height: 13px;
  border: 1px solid var(--line-bright);
  border-radius: 3px;
}
.bought-btn svg {
  width: 9px;
  height: 9px;
  /* The tick is in the markup either way, so the box does not change size
     when it is checked - only whether the tick is drawn. */
  opacity: 0;
}
.bought-btn:hover {
  color: var(--ink-dim);
}
.bought-btn:hover .bought-box {
  border-color: var(--ink-dim);
}
.bought-btn:hover svg {
  opacity: 0.35;
}
.bought-btn:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
  border-radius: var(--r1);
}
/* Grey, not gold.
 *
 * Gold is the one call to action on this page - the Buy button, the "Buy now"
 * state, the stripe down a buyable row. A gold tick under a gold button said
 * the opposite of what it means: a shop you have bought from is the one shop
 * that has nothing left to offer you this drop, and it was drawn as the most
 * inviting thing in the row.
 *
 * --gone is the colour this palette already uses for sold out, which is
 * exactly what a bought listing is from where the reader stands. */
.bought-btn[aria-pressed="true"] {
  color: var(--gone);
}
.bought-btn[aria-pressed="true"] .bought-box {
  color: var(--ground);
  background: var(--gone);
  border-color: var(--gone);
}
.bought-btn[aria-pressed="true"] svg,
.bought-btn[aria-pressed="true"]:hover svg {
  opacity: 1;
}
/* The row itself recedes: it is still there to be found and undone, but it is
   no longer an offer, and it should not read like one. */
tr.bought td {
  opacity: 0.55;
}
/* The cell stays at full strength, because the mark has to be findable to be
   undone - and because a control drawn at 55% looks broken rather than quiet. */
tr.bought td[data-col="buy"] {
  opacity: 1;
}
/* The button loses its gold with it. Same reasoning as the tick above: the
   only gold thing left in a bought row would be the button saying buy, and it
   is still a link worth having - the shop page, the order you just placed -
   but it is no longer the thing to do next. */
tr.bought .buy-btn.live {
  background: var(--raised);
  border-color: var(--line-bright);
  color: var(--ink-dim);
  filter: none;
}
tr.bought .buy-btn.live:hover {
  border-color: var(--ink-dim);
  color: var(--ink);
  filter: none;
}

/* --------------------------------------------------------------- the wall */
/* Rows past the third, for a reader without an account. Blurred rather than
   removed so the shape of what is there stays visible - the point is that
   there is more, not that there is nothing. */
.wall-row td {
  padding: var(--s5) var(--s4);
  text-align: center;
  background: var(--raised);
  border-top: 1px solid var(--line-bright);
}
.wall-row strong {
  display: block;
  font: 600 15px var(--font);
  color: var(--ink);
}
.wall-sub {
  display: block;
  max-width: 46ch;
  margin: var(--s2) auto 0;
  color: var(--ink-dim);
  font-size: 13px;
  line-height: 1.5;
}
.wall-actions {
  display: flex;
  justify-content: center;
  gap: var(--s2);
  margin-top: var(--s3);
}

/* A box run offered to somebody without an account: still there, so the toy
   shows what an account buys, but plainly not a button that will just work. */
.bulk-btn.needs-account {
  opacity: 0.5;
  border-style: dashed;
}

/* A withheld listing, drawn from a decoy the server invented. Illegible on
   purpose, though nothing is protected by it: the row underneath is nonsense,
   so this exists to make the table look like a table rather than to hide
   anything.
 *
 * Blurred with a text-shadow rather than `filter: blur()`, which is a paint
 * and not a layer. The filter version put one composited layer on every cell
 * - 256 of them on a single product page, thirty-two rows of eight - and a
 * compositor asked for 256 blurred surfaces is the sort of thing that turns a
 * page from instant into visibly slow, worst on the machines with the least
 * GPU to spare. The shadow costs a blur radius per glyph run and no layer at
 * all.
 *
 * The trick is `color: transparent` with a shadow behind it: the glyphs are
 * invisible, their soft shadow is all that shows, and it reads as blurred
 * text. Children need it spelled out because several of them set their own
 * colour and would otherwise stay sharp. */
tr.walled td,
tr.walled td * {
  color: transparent;
  text-shadow: 0 0 7px var(--ink-dim);
  user-select: none;
}
tr.walled td {
  opacity: 0.6;
}
/* The two decoy elements that are shapes rather than text: left sharp, they
   are a crisp pill and a crisp circle in a row of fog. Flattened instead. */
tr.walled .buy-btn {
  background: transparent;
  border-color: var(--line);
}
tr.walled .store-mark {
  background: var(--line);
  box-shadow: none;
}
/* The fade is what says "and more", so the last rows go quietly rather than
   stopping at a hard edge. */
tr.walled:nth-last-child(1) td {
  opacity: 0.2;
}
tr.walled:nth-last-child(2) td {
  opacity: 0.3;
}
tr.walled:nth-last-child(3) td {
  opacity: 0.42;
}

/* ------------------------------------------------- detail chart, redrawn */
/* The fill is what makes a flat series read as a price holding steady rather
   than as a stray rule across an empty box. */
.series-fill {
  stroke: none;
}
.series-fill.live {
  fill: var(--gold);
  opacity: 0.16;
}
.series-fill.dead {
  fill: var(--ink-faint);
  opacity: 0.09;
}
/* Today's price, as a tab on the axis the line runs into. */
.now-dot {
  fill: var(--gold);
}
.now-tab {
  fill: var(--gold);
}
.now-tab-text {
  fill: var(--gold-ink);
  font: 600 12px var(--font);
  font-variant-numeric: tabular-nums;
}
.low-label {
  fill: var(--ink-faint);
}
.hit {
  fill: transparent;
}
.hit:hover {
  fill: var(--ink);
  opacity: 0.05;
}
.chart-readout {
  min-height: 1.4em;
  margin: var(--s2) 0 0;
  color: var(--ink-dim);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
/* Which way it has gone, in a line - the thing a price tracker leads with. */
.trend-card {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  margin: 0 var(--s4) var(--s3);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r1);
  background: var(--raised);
}
.trend-arrow {
  font-size: 18px;
  line-height: 1;
}
.trend-card.down .trend-arrow {
  color: #5fbf7f;
}
.trend-card.up .trend-arrow {
  color: #d98b6a;
}
.trend-card.flat .trend-arrow {
  color: var(--ink-faint);
}
.trend-word {
  font: 600 15px var(--font);
  color: var(--ink);
}
.trend-word small {
  font-weight: 500;
  color: var(--ink-dim);
}
.trend-range {
  margin-left: auto;
  color: var(--ink-faint);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------- skeletons */
/* The shape of the page before its data arrives.
 *
 * In index.html rather than drawn by script, so it is on screen with the 5 KB
 * of HTML instead of after the 111 KB of JavaScript that replaces it. It
 * matches the real set-card grid - same columns, same card height, same three
 * rows inside - so the layout does not jump when the real cards land.
 *
 * A shimmer rather than a pulse: a pulse on nine cards is nine things blinking
 * at you, while one sweep reads as a single thing loading. One animation on
 * one gradient, and `prefers-reduced-motion` turns it into a flat block. */
.sk-card {
  display: grid;
  grid-template-rows: auto auto auto auto;
  gap: var(--s3);
  padding: var(--s5) var(--s4) var(--s4);
}
.sk-art,
.sk-line,
.sk-pill {
  display: block;
  border-radius: var(--r1);
  background: linear-gradient(
    100deg,
    var(--surface) 30%,
    var(--raised) 50%,
    var(--surface) 70%
  );
  background-size: 300% 100%;
  animation: sk-sweep 1.4s linear infinite;
}
.sk-art {
  height: 148px;
  margin-bottom: var(--s3);
  border-radius: var(--r2);
}
.sk-line {
  height: 13px;
}
.sk-pill {
  height: 24px;
  width: 45%;
  border-radius: 999px;
}
.sk-w30 {
  width: 30%;
}
.sk-w35 {
  width: 35%;
}
.sk-w40 {
  width: 40%;
}
.sk-w45 {
  width: 45%;
}
.sk-w50 {
  width: 50%;
}
.sk-w55 {
  width: 55%;
}
.sk-w60 {
  width: 60%;
}
.sk-w65 {
  width: 65%;
}
.sk-w70 {
  width: 70%;
}
@keyframes sk-sweep {
  to {
    background-position: -300% 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .sk-art,
  .sk-line,
  .sk-pill {
    animation: none;
    background: var(--raised);
  }
}

/* The kind of an alert as a small badge before its subject, and the
   chooser on the alerts panel that decides which kinds reach a phone. */
.alert-kind {
  display: inline-block;
  margin-right: 0.45em;
  padding: 0.05em 0.5em;
  border-radius: 999px;
  border: 1px solid var(--line);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink-dim);
  vertical-align: 0.1em;
  white-space: nowrap;
}
.alert-kind.k-in_stock,
.alert-kind.k-preorder,
.alert-kind.k-new_listing {
  color: var(--gold);
  border-color: color-mix(in srgb, var(--gold) 50%, transparent);
}
.alert-kind.k-great_price {
  color: var(--bg);
  background: var(--gold);
  border-color: var(--gold);
}
.alert-kind.k-mail {
  color: var(--ink);
  border-style: dashed;
}
.alert-kind.k-page_changed,
.alert-kind.k-new_page {
  color: var(--ink);
  border-color: var(--ink-dim);
}
.alert-kind.k-shop_blocked,
.alert-kind.k-monitor_health {
  color: var(--red);
  border-color: color-mix(in srgb, var(--red) 50%, transparent);
}
.alert-kinds {
  margin: var(--s3) 0 0;
  padding: var(--s2) var(--s3) var(--s3);
  border: 1px solid var(--line);
  border-radius: 10px;
}
.alert-kinds legend {
  padding: 0 0.4em;
  font-weight: 600;
}
.alert-kinds .trend-note {
  margin-top: 0;
}
.alert-kinds-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: var(--s2) var(--s3);
}
.alert-kind-choice {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr);
  gap: 0.5em;
  align-items: center;
  font-size: 13px;
  cursor: pointer;
}
.alert-kind-choice .alert-kind {
  margin-right: 0;
}
.alert-kind-help {
  color: var(--ink-faint);
  font-size: 12px;
}
