/* styles.css — Red Ink */

:root {
  --bg:      #0b0a1a;
  --panel:   #17143333;
  --line:    #2e2857;
  --ink:     #e8e4ff;
  --muted:   #9a93c4;
  --dim:     #837cb2;

  --gold:    #e8b64c;
  --gold-2:  #f7d98a;
  --good:    #4ad991;
  --bad:     #ff6b7a;

  --r-common:   #8e9bb5;
  --r-uncommon: #6fd3a8;
  --r-rare:     #5aa9f0;
  --r-super:    #b07ef0;
  --r-legend:   #e8b64c;
  --r-epic:     #ff8a4c;
  --r-ench:     #ff5fa8;
  --r-iconic:   #58f0e0;

  --seal-h:   26px;   /* the crimped strip that tears away */
  --stack-step: 26px; /* how much of each buried card peeks out */
  --lift:       10px; /* how far a hovered card rises — a selection cue, not a
                         reveal; the viewer is for actually looking at a card */
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }

body {
  background:
    radial-gradient(1100px 620px at 15% -8%, rgba(168,116,232,.15), transparent 60%),
    radial-gradient(900px 520px at 88% 2%,  rgba(74,163,232,.12),  transparent 60%),
    var(--bg);
  background-attachment: fixed;
  color: var(--ink);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  font-size: 15px;
  line-height: 1.5;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;

  /* Smoke billows well past the panel, and it is meant to — but nothing may
     widen the page. So the clip lives HERE rather than on `main`.
     `main` was the obvious place and it was the wrong one: its box is inset
     from the window, so the clip edge sat at a visible x position and sliced
     the cloud off against a hard vertical line on any viewport wide enough for
     the panel to be centred. On `body` the clip edge IS the window edge, so the
     cloud can spill the whole width and is only ever cut where it is already
     off-screen.
     `clip` rather than `hidden`, still: `hidden` would make this a scroll
     container and the sticky rail would stop sticking. */
  overflow-x: clip;
}

/* ── Page backdrop ──────────────────────────────────────────────
   A fixed layer at z-index -1: behind everything in flow, but still in front of
   the body's own gradients, so a supplied image simply covers them. Hidden
   until one actually loads, which is what keeps the gradients as the fallback. */

.page-bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  display: none;
  background-image: var(--page-bg);
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
  filter: blur(var(--page-blur, 0px));
  /* Blur samples transparent pixels past the edges and feathers the frame;
     scaling up slightly pushes that artefact off-screen. */
  transform: scale(calc(1 + var(--page-blur-scale, 0)));
}
body.has-bg .page-bg { display: block; }

/* With artwork behind them, the small labels need their own separation from it —
   a scrim dark enough to carry 9px uppercase on its own would flatten the image
   to mud. A shadow buys the contrast back locally instead of globally. */
body.has-bg .meta .sl,
body.has-bg .meta .nm,
body.has-bg .meta .pr,
body.has-bg .stat .sk,
body.has-bg .stat .sv,
body.has-bg .status,
body.has-bg .snd-txt,
body.has-bg #setSelect,
/* Both of these used to sit on the panel and needed no help. They sit on the
   artwork now. */
body.has-bg .rip-label,
body.has-bg .stack-label,
body.has-bg footer { text-shadow: 0 1px 3px rgba(3, 2, 10, .92); }

/* Just enough to hold the serif off busy artwork — not a glow. */
body.has-bg .title { text-shadow: 0 1px 12px rgba(3, 2, 10, .75); }

/* Scrim. Small type over photographic artwork needs it. */
.page-bg::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(11, 10, 26, var(--page-dim, .72));
}

/* ── Header ─────────────────────────────────────────────────────── */

/* Title and set logo sit on one line. Stacked, they cost 236px of header before
   a single card — a quarter of a 1000px viewport — on a page that already has
   to scroll to show one pack. Side by side they also read as the hierarchy they
   actually are: the site, then the set you are opening. */
/* Split masthead: the site flush left, the current set flush right — the form a
   publication uses for its name and its edition. Both are given the panel's own
   max-width and padding, so the title's left edge lands on the rail and the set
   logo's right edge lands on the grid's far column. Centred, the title had no
   relationship to anything below it, and was not even truly centred: the
   title-and-logo GROUP was, leaving the title itself 80px off the centre line. */
.top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 26px;
  max-width: min(1180px, calc(100vw - 48px));
  margin: 0 auto;
  padding: 26px 24px 22px;
}

/* "Red ink" is a phrase from accounting and the financial press, so the wordmark
   is set like a masthead rather than a logo: an editorial serif, tight, at one
   weight, in two colours because the name contains its own joke.
   Deliberately NOT gradient-filled text — clipping a gold-to-red gradient into
   a heavy sans is the single most generic treatment on the web, and it read as
   a template rather than a decision. A serif masthead over Oswald data labels
   is a pairing with a long history behind it. */
.title {
  font-family: 'Instrument Serif', 'Iowan Old Style', Georgia, serif;
  font-weight: 400;
  font-size: clamp(40px, 6.2vw, 70px);
  /* Display type gets tighter, not looser. The old +1.5px tracking was working
     against the size. */
  letter-spacing: -0.015em;
  white-space: nowrap;
  margin: 0;
  line-height: .95;
  color: #f4eee1;                 /* warm off-white — ink on paper, not #fff */
}
/* <em> marks the half of the name that is doing the work. Oxblood rather than a
   bright red: this is spilled ink, not an error state. */
.title em {
  font-style: italic;
  color: #c0413f;
  /* The italic d already leans into the word-space, so a full space reads as a
     hole. Optical correction — the gap should look even, not measure even. */
  margin-right: -.05em;
}

/* The set is a parameter, not a co-brand. At full size its logo measured 1.31x
   the area of the site's own title — and in saturated green against a restrained
   serif, so it out-shouted the thing it belongs to. Halved and set behind a rule,
   it reads as "Red Ink | current set", which is the actual hierarchy. */
/* The right-hand end of the masthead carries two different kinds of thing: an
   action, and the edition indicator. Kept on one line so the header stays the
   115px it was — stacking them would put the height straight back. */
.masthead-right {
  display: flex;
  align-items: center;
  gap: 22px;
}

/* An action, so it is shaped like one rather than set as another label. */
.support {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 13px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-family: 'Oswald', sans-serif;
  font-size: 10px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
  transition: color .15s ease, border-color .15s ease, background-color .15s ease;
}
.support svg {
  width: 12px;
  height: 12px;
  fill: currentColor;
  flex: 0 0 auto;
}
.support:hover,
.support:focus-visible {
  color: var(--gold-2);
  border-color: var(--gold);
  background: rgba(232, 182, 76, .1);
}

/* No divider now that the two sit at opposite ends — a rule only reads as a
   separator between things that are adjacent. */
.setpick {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

/* Set logo, when one is supplied for this set. Hidden until it has actually
   decoded, which is what leaves the typeset name in place if there is no file. */
.set-logo {
  display: none;
  margin: 0 auto;
  max-width: min(430px, 84vw);
  max-height: var(--logo-h, 66px);
  width: auto;
  height: auto;
  object-fit: contain;
}
.setpick.has-logo .set-logo { display: block; }

/* With one set on offer the picker is only a label, so the logo replaces it
   outright. With several it still has to be operable, so it stays underneath. */
.setpick.has-logo.single #setSelect { display: none; }
.setpick.has-logo:not(.single) #setSelect { margin-top: 10px; }

#setSelect {
  font-family: 'Oswald', sans-serif;
  font-size: 15px;
  letter-spacing: 2.4px;
  text-transform: uppercase;
  color: var(--muted);
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--line);
  padding: 5px 26px 5px 8px;
  cursor: pointer;
  text-align: center;
  text-align-last: center;
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--dim) 50%),
                    linear-gradient(135deg, var(--dim) 50%, transparent 50%);
  background-position: right 10px center, right 5px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  transition: color .15s ease, border-color .15s ease;
}
#setSelect:hover, #setSelect:focus { color: var(--gold-2); border-color: var(--gold); outline: none; }

/* Only one set on offer: drop the caret and the interactivity, so it reads as
   the label it actually is rather than a menu that goes nowhere. */
.setpick.single #setSelect {
  pointer-events: none;
  background-image: none;
  padding-right: 8px;
}
.setpick.single #setSelect:hover { color: var(--muted); border-color: var(--line); }
#setSelect option { background: #1a1740; color: var(--ink); }

/* ── Layout ─────────────────────────────────────────────────────
   Pack rail on the left, card grid on the right. The rail sticks, so the
   booster stays reachable however far down the grid you are.            */

/* The content sits on its own surface rather than straight on the artwork.
   Small type — slot labels, prices, stat labels — cannot hold its own against
   photographic art. A panel buys that contrast locally, which lets the global
   scrim be lighter so the backdrop still shows around the edges. */
main {
  /* The panel needs a gutter at every width. With max-width alone it ran flush
     to both screen edges on any viewport between the six-column breakpoint and
     the max, putting its rounded corners and side borders off-screen. */
  max-width: min(1180px, calc(100vw - 48px));
  margin: 0 auto 34px;
  padding: 26px 24px 30px;
  display: grid;
  grid-template-columns: 208px minmax(0, 1fr);
  gap: 30px;
  align-items: start;
  /* No panel. The cards sit straight on the page artwork rather than on a slab
     laid over it — the backdrop is the set's own illustration, and covering it
     with 55% black to make a container was hiding the better background. The
     grid's own cards give the region all the edges it needs. */
  background: none;
  border: 0;
  box-shadow: none;
}

.rail {
  position: sticky;
  top: 26px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* ── Board ──────────────────────────────────────────────────────
   Three cards get billing — the foil slot and the two rare-or-better slots,
   which between them hold essentially all of a pack's value. The nine commons
   and uncommons are bulk, so they are stacked like the pile they are. Any card
   can be clicked to open full size. */

/* One row: three featured cards and two piles, five items across. Kept as two
   stacked rows, the three featured cards left 662px of the board empty at 2000px
   while the page still had to scroll — the same starved-of-height,
   swimming-in-width contradiction as before. `display: contents` lets the two
   groups keep their own markup while their children share a single row. */
.board {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 26px 18px;
}
.featured,
.stacks { display: contents; }

.featured > .slot,
.stacks > .stack { flex: 1 1 170px; max-width: 300px; }

.board > .status { flex: 1 0 100%; }

/* A pile is one card tall plus a sliver for every card behind it. */
.stack { position: relative; }
.stack-label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
  font-size: 9.5px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--dim);
}
.stack-val { font-variant-numeric: tabular-nums; color: var(--muted); }

/* Every card is absolutely placed EXCEPT the front one, which stays in flow so
   the pile takes its height from a real rendered card. Deriving the height from
   a calc over a fixed card width instead would pin the cards to that width and
   stop them flexing to fill the row.
   The offset is PADDING on the container, not a margin on the front card: the
   front card is the only in-flow child, so its top margin collapsed straight
   through the parent and positioned nothing. Padding also leaves the absolutely
   placed cards alone, since they resolve against the padding box's top edge. */
.stack-cards {
  position: relative;
  /* One --lift of headroom at the top, so even the most buried card has
     somewhere to rise into rather than climbing over its own label. */
  padding-top: calc(var(--lift, 10px) + (var(--n) - 1) * var(--stack-step, 26px));
}
.stack-cards .slot {
  position: absolute;
  left: 0;
  width: 100%;
  top: calc(var(--lift, 10px) + var(--i) * var(--stack-step, 26px));
  z-index: var(--i);
  transition: top .18s cubic-bezier(.3, .8, .3, 1);
}
.stack-cards .slot:last-child {
  position: relative;
  top: 0;
}

/* Nudge up on hover to show which card is under the cursor. Deliberately small:
   lifting far enough to read the card competes with the viewer.
   Note there is no z-index change here, on purpose. Raising the hovered card
   above its neighbours also lifts its whole body over the cards in front of it,
   so the pile collapses into one full card the moment the cursor lands. Because
   the card moves UP, into space owned by cards it already paints over, the
   extra sliver shows without touching the stacking order at all. */
.stack-cards .slot:hover {
  top: calc(var(--i) * var(--stack-step, 26px));
}
/* The front card is in flow, so its `top` is an offset from where it already
   sits rather than a position within the pile. */
.stack-cards .slot:last-child:hover {
  top: calc(-1 * var(--lift, 10px));
}
/* Only the front card of a stack shows its caption; the rest are slivers. */
.stack-cards .slot .meta { display: none; }

.slot.clickable { cursor: pointer; }
.slot.clickable:hover .face.front { filter: brightness(1.14); }

.slot { perspective: 1400px; }

.flip {
  position: relative;
  aspect-ratio: 5 / 7;
  transform-style: preserve-3d;
  transition: transform .62s cubic-bezier(.25,.8,.3,1);
  transition-delay: var(--d, 0ms);
}
.slot.flipped .flip { transform: rotateY(180deg); }

/* Suppress the flip while we silently reset the board between packs. */
.board.no-anim .flip,
.board.no-anim .meta { transition: none !important; }

.face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  border-radius: 11px;
  overflow: hidden;
}

.face.front { transform: rotateY(180deg); background: #0d0b1e; border: 1px solid var(--line); }
.face.front img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* When a real card back is supplied it replaces the drawn one wholesale — every
   drawn ring, sweep and mark comes off, since the photograph carries its own
   printing and its own edge. Applied only once the file has decoded, so a
   missing one silently leaves the drawn back rather than an empty card. */
body.has-cardback .face.back {
  background:
    var(--card-back) center / cover no-repeat,
    #0d0b1e;

  /* The halo was two things. Most of it was this border, which I had set to a
     9%-white outline — a light ring drawn around a card that already has its
     own black edge. It is now dark.
     The rest is in the artwork: the top three rows average RGB ~[94,95,92]
     before dropping to [3,3,3] by row four, a pale band along that edge only.
     The inset shadow knocks it back, keyed to the card's own near-black rather
     than to the page, so it reads as the card's edge and not as a vignette. */
  border-color: rgba(4, 3, 9, .85);
  box-shadow:
    inset 0 0 0 2px rgba(4, 3, 9, .92),
    inset 0 0 9px 2px rgba(4, 3, 9, .55);
}
body.has-cardback .face.back::before,
body.has-cardback .face.back::after,
body.has-cardback .face.back .mark { display: none; }

/* Card back — an original ink-swirl design, not a reproduction of the printed
   Lorcana back. Every ring is sized by width with aspect-ratio:1 and centred by
   transform; using `inset` instead would stretch each circle into an egg,
   because the card itself is 5:7. */
.face.back {
  background:
    radial-gradient(circle at 50% 42%, rgba(232,182,76,.13), transparent 62%),
    linear-gradient(158deg, #2b2359 0%, #1b1742 46%, #120f2e 100%);
  border: 1px solid rgba(232,182,76,.28);
}
.face.back::before,
.face.back::after {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
}
.face.back::before { width: 74%; aspect-ratio: 1; border: 1px solid rgba(232,182,76,.20); }
.face.back::after  { width: 60%; aspect-ratio: 1; border: 1px solid rgba(168,116,232,.16); }

.mark {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: 46%;
  aspect-ratio: 1;
}
.mark::before, .mark::after {
  content: '';
  position: absolute;
  border-radius: 50%;
}
/* Outer gold sweep */
.mark::before {
  inset: 0;
  background: conic-gradient(from 208deg,
    transparent 0deg, rgba(232,182,76,.78) 128deg, transparent 208deg);
  -webkit-mask: radial-gradient(circle, transparent 57%, #000 60%);
          mask: radial-gradient(circle, transparent 57%, #000 60%);
}
/* Inner amethyst sweep, counter-set so the two read as one swirl */
.mark::after {
  inset: 26%;
  background: conic-gradient(from 28deg,
    transparent 0deg, rgba(168,116,232,.85) 128deg, transparent 208deg);
  -webkit-mask: radial-gradient(circle, transparent 40%, #000 45%);
          mask: radial-gradient(circle, transparent 40%, #000 45%);
}

/* ── Foil ───────────────────────────────────────────────────────
   Real holographic stock does three things at once, and the old single
   two-tint wipe did none of them: it shows MANY spectral bands across the
   surface simultaneously rather than one travelling tint, it runs the full hue
   circle in order rather than picking two, and a separate specular glare slides
   over the top of that at a different rate. Two layers, moving independently,
   is what stops it reading as a gradient sweep.                              */

.face.front.foil::before,
.face.front.foil::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity .3s ease;
}
.slot.flipped .face.front.foil::before,
.slot.flipped .face.front.foil::after { opacity: 1; }

/* Spectral banding — the diffraction itself. Repeating, so bands cover the
   whole card at once the way real foil does. */
.face.front.foil::before {
  z-index: 2;
  background: repeating-linear-gradient(
    104deg,
    hsla(346, 100%, 66%, .16)  0%,
    hsla( 32, 100%, 62%, .13)  3.5%,
    hsla( 56, 100%, 62%, .12)  7%,
    hsla(140,  90%, 60%, .13) 10.5%,
    hsla(190, 100%, 62%, .16) 14%,
    hsla(232, 100%, 70%, .14) 17.5%,
    hsla(286,  95%, 70%, .16) 21%,
    hsla(346, 100%, 66%, .16) 24.5%
  );
  background-size: 260% 100%;
  /* `overlay`, not `color-dodge`. Dodge brightens toward white so fast that at
     these saturations the artwork disappears under a rainbow — foil tints what
     is underneath, it does not replace it. Overlay keeps the card's own
     luminance and lets the spectrum ride on top. */
  mix-blend-mode: overlay;
  animation: foilBands 7s ease-in-out infinite;
}

/* Specular glare — the bright smear that tracks the light source. Runs at a
   different rate to the banding, which is what makes the surface look like it
   is being tilted rather than cross-faded. */
.face.front.foil::after {
  z-index: 3;
  background: linear-gradient(100deg,
    transparent 34%,
    rgba(255, 255, 255, .06) 44%,
    rgba(255, 255, 255, .20) 50%,
    rgba(255, 255, 255, .06) 56%,
    transparent 66%);
  background-size: 300% 100%;
  mix-blend-mode: screen;
  animation: foilGlare 4.4s ease-in-out infinite;
}

@keyframes foilBands {
  0%, 100% { background-position:   0% 50%; }
  50%      { background-position: 100% 50%; }
}
@keyframes foilGlare {
  0%, 100% { background-position: 100% 50%; }
  50%      { background-position:   0% 50%; }
}

/* Enchanted and Iconic are printed on far louder stock than an ordinary foil
   common, so they get the banding harder and a slower, wider glare. */
.slot[data-r="Enchanted"] .face.front.foil::before,
.slot[data-r="Iconic"]    .face.front.foil::before,
.slot[data-r="Epic"]      .face.front.foil::before {
  filter: saturate(1.35);
  background-size: 175% 100%;      /* tighter bands = more of them */
  animation-duration: 9s;
}
.slot[data-r="Enchanted"] .face.front.foil::after,
.slot[data-r="Iconic"]    .face.front.foil::after {
  animation-duration: 5.6s;
}

/* Rarity ring on the revealed face */
.slot[data-r="Uncommon"]   .face.front { border-color: var(--r-uncommon); }
.slot[data-r="Rare"]       .face.front { border-color: var(--r-rare); }
.slot[data-r="Super_rare"] .face.front { border-color: var(--r-super);  box-shadow: 0 0 16px rgba(176,126,240,.3); }
.slot[data-r="Legendary"]  .face.front { border-color: var(--r-legend); box-shadow: 0 0 20px rgba(232,182,76,.34); }
.slot[data-r="Epic"]       .face.front { border-color: var(--r-epic);   box-shadow: 0 0 22px rgba(255,138,76,.38); }
.slot[data-r="Enchanted"]  .face.front { border-color: var(--r-ench);   box-shadow: 0 0 28px rgba(255,95,168,.44); }
.slot[data-r="Iconic"]     .face.front { border-color: var(--r-iconic); box-shadow: 0 0 32px rgba(88,240,224,.48); }

/* ── Card caption ───────────────────────────────────────────────── */

.meta {
  margin-top: 9px;
  opacity: .34;                                   /* dimmed while face-down */
  transition: opacity .32s ease;
  transition-delay: calc(var(--d, 0ms) + 260ms);
}
.slot.flipped .meta { opacity: 1; }

.meta .sl {
  font-size: 9.5px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.meta .row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-top: 2px;
}
.meta .nm {
  flex: 1;
  min-width: 0;
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.meta .pr {
  font-size: 12.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--muted);
}
.meta .pr.hot { color: var(--gold-2); }

.status { text-align: center; color: var(--muted); font-size: 13.5px; min-height: 20px; margin: 22px 0 0; }
.status.error { color: var(--bad); }

/* ── Sticky bar ─────────────────────────────────────────────────── */

.bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 20;
}
/* Only the stats strip is frosted; the pack sits proud of it. */
.bar-foot {
  background: rgba(11,10,26,.92);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-top: 1px solid var(--line);
  max-width: none;
}
.bar-inner {
  max-width: 980px;
  margin: 0 auto;
  padding: 0 20px;
  display: flex;
  align-items: flex-end;
}
.bar-foot {
  padding: 11px max(20px, calc((100% - 980px) / 2 + 20px)) 13px;
  display: flex;
  align-items: center;
  gap: 22px;
}

/* ── The pack ───────────────────────────────────────────────────
   A whole sealed booster, shown end to end at roughly the proportions of a
   real one (a shade narrower than a card). Crimped and serrated at both ends;
   only the top seal tears. Original artwork — no Disney or Ravensburger marks
   are reproduced.

     .pack-mouth   interior + card edges, revealed as the seal comes away
     .pack-body    the wrapper, bulging around twelve cards
     .pack-strip   the top seal, clipped away as it tears                 */

.rip {
  position: relative;
  width: 100%;
  transition: opacity .2s ease;
}
/* Dimmed while the set is loading, but NOT during an opening — the sequence is
   nothing but motion and light, and greying it out fights the whole effect. */
.rip.busy { pointer-events: none; }
.rip.busy:not(.opening) { opacity: .6; }

.pack {
  position: relative;
  width: 100%;
  aspect-ratio: 2 / 3;
  border-radius: 3px;
  filter: drop-shadow(0 14px 26px rgba(0,0,0,.55));
  transition: transform .34s cubic-bezier(.3,.7,.3,1),
              opacity .34s ease,
              filter .28s ease;
}
/* The spent wrapper is no longer dropped out of frame — it is conjured away.
   See .poof further down. All that is left here is the arrival: the next pack
   does not slide up from below, it MATERIALISES, small and faint, and swells
   into place out of the smoke that ate the last one. */
.pack.incoming { transform: scale(.8) translateY(8%); opacity: 0; transition: none; }

/* Interior */
/* What is behind the seal once it is gone.
 *
 * Tearing the crimp off REMOVES that material, so most of this band has to be
 * see-through — whatever is behind the pack, page artwork included, shows
 * through it. Only the bottom of the band is dark, where you are looking down
 * into the open mouth of the wrapper at the cards inside. Filling the whole
 * band with an interior colour, as this did at first, reads as a black hole
 * punched in the page. */
.pack-mouth {
  position: absolute;
  left: 0; right: 0; top: 0;
  height: var(--seal-h);
  background: linear-gradient(180deg,
    transparent 0%,
    transparent 42%,
    rgba(6, 5, 16, .38) 62%,
    rgba(6, 5, 16, .84) 84%,
    rgba(14, 11, 34, .96) 100%);
}

/* ── The contents ───────────────────────────────────────────────
   The cards, which leave through the opening once the seal is gone. Their DOM
   position does all the occlusion work: after .pack-mouth but before
   .pack-body, so the wrapper's own front panel hides them until they clear
   --seal-h. No z-index involved.

   They are ONLY ever visible in flight and the animation's `both` holds them at
   opacity 0 afterwards. A static card-edge element in this position is what
   previously read as a stray dashed line, so that matters. */
.pack-cards {
  position: absolute;
  left: 7%; right: 7%; top: 0;
  aspect-ratio: 5 / 7;
  pointer-events: none;
}
.pack-cards i {
  position: absolute;
  inset: 0;
  border-radius: 5px;
  opacity: 0;
  /* The drawn ink-swirl back, for when no photographed back is supplied. */
  background:
    radial-gradient(circle at 50% 42%, rgba(232,182,76,.13), transparent 62%),
    linear-gradient(158deg, #2b2359 0%, #1b1742 46%, #120f2e 100%);
  box-shadow: 0 6px 18px rgba(0, 0, 0, .6);
}
body.has-cardback .pack-cards i {
  background: var(--card-back) center / cover no-repeat, #0d0b1e;
}

/* Pushed out, not lifted away. Straight up the centre line: there is no X term
   and no rotation in this animation at all, rather than zeroed ones — a value
   that must always be zero is better absent than seeded. Cards differ only in
   how far they travel, how much they grow, and when they go. */
.pack.bursting .pack-cards i {
  animation: flyOut var(--burst-ms, 560ms) cubic-bezier(.16, .72, .3, 1)
             var(--dly, 0ms) both;
}
/* The middle stops name opacity only, on purpose: transform is left to
   interpolate across the whole run so the arc stays smooth. */
@keyframes flyOut {
/* Held opaque almost the whole way. These cards leave the top of the window
   rather than dissolving on the way up, so an early fade would be a card
   vanishing in mid-air — the exit is the travel, not the fade. The last stretch
   still lands on 0 so that if the geometry is ever short, the card ends hidden
   instead of parked on screen. */
  0%   { opacity: 0; transform: translateY(20%) scale(.94); }
  5%   { opacity: 1; }
  88%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(var(--dy, -260%)) scale(var(--sc, 1)); }
}

/* Wrapper */
.pack-body {
  position: absolute;
  left: 0; right: 0; top: var(--seal-h); bottom: 0;
  overflow: hidden;
  background:
    repeating-linear-gradient(97deg, rgba(255,255,255,.035) 0 1px, transparent 1px 8px),
    linear-gradient(163deg,
      #1d1642 0%, #573a9e 14%, #191333 29%, #7b4fc2 47%,
      #1c1540 64%, #62409f 82%, #171232 100%);
}

/* A real pack is not flat — it bulges around the cards. Darkening both edges
   is what reads as that curve. */
.pack-bulge {
  position: absolute; inset: 0;
  background:
    linear-gradient(90deg,
      rgba(0,0,0,.55) 0%, rgba(0,0,0,.24) 4%, rgba(0,0,0,.05) 12%,
      rgba(255,255,255,.05) 38%, rgba(255,255,255,.08) 48%, rgba(255,255,255,.04) 58%,
      rgba(0,0,0,.06) 88%, rgba(0,0,0,.26) 96%, rgba(0,0,0,.58) 100%);
  pointer-events: none;
}
/* Travelling foil sheen */
.pack-body::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(168deg,
    transparent 36%, rgba(255,255,255,.13) 46%, rgba(205,255,248,.09) 51%,
    rgba(255,215,255,.12) 56%, transparent 66%);
  background-size: 100% 260%;
  animation: packSheen 7.5s ease-in-out infinite;
  pointer-events: none;
}
@keyframes packSheen {
  0%, 100% { background-position: 0 0%; }
  50%      { background-position: 0 100%; }
}

/* Printed face */
.pack-art {
  position: absolute;
  left: 0; right: 0; top: 0; bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 0 16px;
  text-align: center;
}
.pack-mark {
  width: 46px; height: 46px;
  border-radius: 50%;
  background: conic-gradient(from 208deg,
    transparent 0deg, rgba(255,228,158,.95) 128deg, transparent 208deg);
  -webkit-mask: radial-gradient(circle, transparent 54%, #000 58%);
          mask: radial-gradient(circle, transparent 54%, #000 58%);
  filter: drop-shadow(0 2px 5px rgba(0,0,0,.5));
}
.pack-name {
  font-family: 'Oswald', sans-serif;
  font-size: 13px;
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: 2.2px;
  color: rgba(255,255,255,.95);
  text-shadow: 0 1px 4px rgba(0,0,0,.65);
}
.pack-rule {
  width: 40px; height: 1px;
  background: rgba(255,255,255,.4);
}
.pack-count {
  font-family: 'Oswald', sans-serif;
  font-size: 9.5px;
  letter-spacing: 2.4px;
  color: rgba(255,255,255,.66);
}

/* Sealed bottom crimp — same construction as the top, minus the tearing. */
.pack-seam-bottom {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: calc(var(--seal-h) - 4px);
  background:
    repeating-linear-gradient(90deg, rgba(0,0,0,.26) 0 1px, transparent 1px 4px),
    linear-gradient(0deg, #453775 0%, #2b2258 52%, #3a2e68 100%);
  box-shadow: inset 0 5px 8px -6px rgba(0,0,0,.85),
              inset 0 1px 0 rgba(255,255,255,.10);
}
.pack-seam-bottom i {
  position: absolute;
  left: 0; right: 0; bottom: 0; height: 4px;
  background: var(--bg);
  -webkit-mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 7px);
          mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 7px);
}

/* ── Photographed wrapper ───────────────────────────────────────
   When a set has real pack artwork, one image dresses both the seal and the
   body. Both use `background-size: 100% auto`, so the image is scaled to the
   pack's width and its own aspect carries the height; the body then shifts the
   image up by exactly the seal height. The two halves line up into one
   continuous pack, and the seal still tears away from the top of it.        */

.pack.has-img .pack-strip,
.pack.has-img .pack-shred,
.pack.has-img .pack-body {
  background-image: var(--pack-img);
  background-repeat: no-repeat;
  background-size: 100% auto;
  background-color: transparent;
}
.pack.has-img .pack-strip,
.pack.has-img .pack-shred { background-position: 0 0; }
/* Bottom-aligned rather than offset by -seal-h. Because the frame takes the
   image's own aspect ratio, the scaled image is exactly pack-height tall, so
   aligning its bottom edge puts its top exactly one seal-height above the body.
   Same result, but it holds whatever unit --seal-h is in — and for a photo it
   is a percentage, so the tear line tracks the real crimp at every pack size. */
.pack.has-img .pack-body  { background-position: 0 100%; }

/* A photographed pack keeps its own proportions instead of being squeezed into
   the drawn pack's 2:3. Real wrappers are taller than that. */
.pack.has-img { aspect-ratio: var(--pack-ar, 2 / 3); }

/* The photo brings its own printing, crimps and edge, so the drawn ones go. */
.pack.has-img .pack-art,
.pack.has-img .pack-bulge,
.pack.has-img .pack-teeth,
.pack.has-img .pack-notch,
.pack.has-img .pack-seam-bottom { display: none; }

/* Keep a trace of moving sheen — a still photo of foil looks dead next to the
   drawn one — but far weaker, since the photo already carries its own lighting. */
.pack.has-img .pack-body::after { opacity: .3; }

/* On a photographed pack the prompt moves off the artwork entirely and sits
   underneath. Printed wrappers already carry their own text at the bottom, and
   overlaying ours on top of it made both hard to read. */
.rip:has(.pack.has-img) { padding-bottom: 22px; }
.pack.has-img .rip-label {
  bottom: -18px;
  color: var(--dim);
}

/* The crimped top seal — the piece that tears off */
.pack-strip,
.pack-shred {
  position: absolute;
  left: 0; right: 0; top: 0;
  height: var(--seal-h);
  background:
    repeating-linear-gradient(90deg, rgba(0,0,0,.26) 0 1px, transparent 1px 4px),
    linear-gradient(180deg, #453775 0%, #2b2258 52%, #3a2e68 100%);
  border-bottom: 1px solid rgba(0,0,0,.6);
  box-shadow: inset 0 -5px 8px -6px rgba(0,0,0,.85),
              inset 0 -1px 0 rgba(255,255,255,.10);
  will-change: clip-path;
}
.pack-teeth {
  position: absolute;
  left: 0; right: 0; top: 0; height: 4px;
  background: var(--bg);
  -webkit-mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 7px);
          mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 7px);
}
.pack-notch {
  position: absolute;
  left: 10px; top: 4px;
  width: 8px; height: 8px;
  background: var(--bg);
  transform: rotate(45deg);
  border-radius: 1px;
}

/* The torn-away piece, hinged at the tear front — which moves as you pull, so
   the hinge has to move with it. It lifts clear of the opening rather than
   lying over it (an earlier, gentler peel just sat on the hole looking like a
   ghost of the seal), then is thrown up and clear when the tear commits. */
.pack-shred {
  z-index: 3;
  transform-origin: var(--tear, 0%) 100%;
  pointer-events: none;
  will-change: transform, clip-path;
  transition: transform .16s ease-out;
  filter: drop-shadow(-3px 4px 6px rgba(0, 0, 0, .6));
}
/* Straining, before the seam goes. */
.rip.tearing .pack-shred { transform: rotate(5deg) translateY(-2px); }
/* Thrown. Must stay AFTER the rule above — both classes are on the element at
   once during the cascade, and at equal specificity source order decides. */
.rip.committing .pack-shred {
  transition: transform .68s cubic-bezier(.2,.6,.3,1), opacity .68s ease-in .1s;
  transform: translate(-46px, -128px) rotate(46deg) scale(.86);
  opacity: 0;
}

/* Foil pales and tightens immediately ahead of a tear. A hairline at the tear
   front is what makes the seal read as failing rather than merely disappearing. */
.pack-stress {
  position: absolute;
  top: 0;
  left: var(--tear, 0%);
  width: 14px;
  margin-left: -7px;
  height: var(--seal-h);
  z-index: 4;
  pointer-events: none;
  opacity: 0;
  transition: opacity .12s ease;
  background: linear-gradient(90deg,
    transparent 0%, rgba(255,255,255,.08) 34%, rgba(255,255,255,.5) 50%,
    rgba(255,255,255,.08) 66%, transparent 100%);
}
/* Keyed to `.tearing` and nothing else, which is the ONLY state where a tear
   front exists. It used to be driven by two classes that outlived the tear, and
   the hairline then parked at --tear's final 100% as a bright white bar down
   the pack's right edge. One class that is removed the instant the seal is gone
   leaves nothing to park. */
.rip.tearing .pack-stress { opacity: .85; }

/* Prompt, printed across the wrapper */
.rip-label {
  position: absolute;
  left: 0; right: 0;
  bottom: calc(var(--seal-h) + 10px);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Oswald', sans-serif;
  font-size: 10px;
  letter-spacing: 2.2px;
  text-transform: uppercase;
  color: rgba(255,255,255,.8);
  text-shadow: 0 1px 4px rgba(0,0,0,.8);
  pointer-events: none;
  z-index: 3;
  transition: opacity .18s ease;
}
.rip.tearing .rip-label { opacity: .25; }

/* Seam gives: the pack jolts once. */
.rip.torn .pack { animation: seam .19s ease-out; }
@keyframes seam {
  0%   { transform: translateX(0); }
  30%  { transform: translateX(-3px) rotate(-.5deg); }
  65%  { transform: translateX(2px) rotate(.3deg); }
  100% { transform: translateX(0); }
}

.rip.committing .pack { filter: drop-shadow(0 14px 26px rgba(0,0,0,.55))
                                drop-shadow(0 0 18px rgba(232,182,76,.5)); }

/* The target. The whole wrapper is the button — a pack is not a widget with a
   control on it, it is a thing you pick up — so this is an invisible button
   laid edge to edge over it, present for the keyboard and the screen reader. */
.pack-hit {
  position: absolute;
  inset: 0;
  z-index: 5;
  padding: 0;
  border: 0;
  background: transparent;
  border-radius: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.pack-hit:focus-visible { outline: 2px solid var(--gold); outline-offset: 4px; }
.pack-hit:disabled { cursor: default; }

/* Answers the pointer. Lifting and warming the pack is the only thing telling
   you it can be picked up, now that there is no handle to grab. */
.rip:not(.busy) .pack:has(.pack-hit:hover) {
  transform: translateY(-5px) scale(1.015);
  filter: drop-shadow(0 20px 32px rgba(0, 0, 0, .62))
          drop-shadow(0 0 16px rgba(232, 182, 76, .3));
}
.rip:not(.busy) .pack:has(.pack-hit:active) {
  transform: translateY(-1px) scale(.995);
  transition-duration: .07s;
}

/* ── The poof ───────────────────────────────────────────────────
   The spent wrapper is not dropped, it is dismissed: a sorcerer's puff of smoke
   swallows it, and the next pack swells out of the same cloud while it is still
   thick — so the smoke reads as delivering the pack, not merely as clearing.

   A SIBLING of .pack rather than a child, deliberately. The pack is reset and
   re-dressed in the middle of the poof; anything parented to it would be wiped
   out halfway through the effect. */
.poof {
  position: absolute;
  left: -55%; right: -55%; top: -32%;
  height: 156%;
  z-index: 6;
  pointer-events: none;
  opacity: 0;
  /* Screen, so the cloud lights the page instead of sitting on it as a grey
     patch. It was conjured; it is not soot. */
  mix-blend-mode: screen;
}
.rip.poofing .poof { opacity: 1; }

/* Smoke. One puff is a soft sphere; the cloud is nine of them overlapping,
   leaving on different vectors at different moments. */
.poof i {
  position: absolute;
  left: var(--x); top: var(--y);
  width: var(--s); aspect-ratio: 1;
  margin: calc(var(--s) / -2);
  border-radius: 50%;
  opacity: 0;
  background: radial-gradient(circle at 42% 38%,
    rgba(255, 255, 255, 1) 0%,
    rgba(240, 231, 255, .96) 22%,
    rgba(186, 156, 250, .82) 42%,
    rgba(126,  88, 214, .52) 60%,
    rgba( 70,  44, 142, .22) 76%,
    transparent 86%);
  filter: blur(5px);
}
.rip.poofing .poof i {
  animation: puff var(--poof-ms, 820ms) cubic-bezier(.14, .68, .26, 1)
             var(--dly, 0ms) both;
}
/* Every var() here carries a fallback, and that is not decoration. An
   unresolved var() invalidates the WHOLE transform declaration, which then
   computes to `none` — so one unseeded custom property does not degrade the
   motion, it silently deletes it. A missing --sc is exactly how the smoke
   ended up swelling in place instead of billowing outward. */
@keyframes puff {
  0%   { opacity: 0; transform: translate(0, 0) scale(.16); }
  16%  { opacity: .94; }
  100% { opacity: 0;
         transform: translate(var(--dx, 0px), var(--dy, -40px)) scale(var(--sc, 2.6)); }
}

/* Sparks. Four-point stars, because that is the shape stage magic has always
   used — a round glint reads as a lens flare, not as a conjuring. */
.poof b {
  position: absolute;
  left: var(--x); top: var(--y);
  width: var(--s); aspect-ratio: 1;
  margin: calc(var(--s) / -2);
  opacity: 0;
  background: linear-gradient(160deg, #fff6dc, var(--gold-2) 55%, var(--gold));
  clip-path: polygon(50% 0%, 58% 42%, 100% 50%, 58% 58%,
                     50% 100%, 42% 58%, 0% 50%, 42% 42%);
  filter: drop-shadow(0 0 5px rgba(232, 182, 76, .95));
}
.rip.poofing .poof b {
  animation: spark var(--poof-ms, 820ms) cubic-bezier(.1, .74, .28, 1)
             var(--dly, 0ms) both;
}
/* Every stop carries the WHOLE transform. A keyframe naming only part of it
   drops the rest, and the star would snap back to the origin mid-flight. */
@keyframes spark {
  0%   { opacity: 0;
         transform: translate(0, 0) scale(.2) rotate(0deg); }
  18%  { opacity: 1;
         transform: translate(calc(var(--dx, 0px) * .2), calc(var(--dy, -40px) * .2))
                    scale(1.12) rotate(calc(var(--rot, 0deg) * .18)); }
  100% { opacity: 0;
         transform: translate(var(--dx, 0px), var(--dy, -40px))
                    scale(.28) rotate(var(--rot, 0deg)); }
}

/* The wrapper being consumed. It shrinks INTO the cloud rather than fading
   where it stands, so the smoke reads as the cause and not as decoration. */
.pack.vanishing { animation: vanish 300ms ease-in both; }
@keyframes vanish {
  0%   { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(.8); }
}

/* ── The tally ──────────────────────────────────────────────────
   Set as a ledger, not a dashboard. Four identical ruled rows with wide-tracked
   caps is the default stat-block pattern and it treats every number as a peer —
   but these are not peers. Three are inputs and running counts; the fourth is
   the conclusion the whole site is arguing for. So the minor lines group
   tightly with no rules between them, a single rule closes the column the way
   a total is ruled off in a book, and the net is set in the masthead's serif
   and carries the wordmark's own oxblood when you are down.                  */

.stats { display: flex; flex-direction: column; }

.stat {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 3px 0;
}
.stat .sk {
  font-size: 9px;
  /* Wide tracking on small caps is the tell. Enough to space them, not enough
     to look like a chart legend. */
  letter-spacing: .9px;
  text-transform: uppercase;
  color: var(--dim);
  white-space: nowrap;
}
.stat .sv {
  font-family: 'Oswald', sans-serif;
  font-size: 17px;
  font-weight: 300;
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
  white-space: nowrap;
  color: var(--muted);
}

/* Rule off the column, then the total below it. */
.stat-net {
  margin-top: 9px;
  padding-top: 11px;
  border-top: 1px solid rgba(232, 182, 76, .22);
  align-items: baseline;
}
.stat-net .sk { color: var(--muted); }
.stat-net .sv {
  font-family: 'Instrument Serif', 'Iowan Old Style', Georgia, serif;
  font-weight: 400;
  font-size: 30px;
  letter-spacing: -.01em;
  color: var(--ink);
}
.stat-net .sv.neg { color: #c0413f; }   /* the wordmark's oxblood */
.stat-net .sv.pos { color: var(--good); }

label.stat { cursor: text; }
.sv.money { display: inline-flex; align-items: baseline; }
#packPrice {
  width: 52px;
  background: transparent;
  border: none;
  border-bottom: 1px dashed var(--line);
  color: var(--muted);
  font: inherit;
  font-family: 'Oswald', sans-serif;
  font-size: 17px;
  font-weight: 300;
  padding: 0 0 1px 1px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
#packPrice:focus { outline: none; border-bottom-color: var(--gold); color: var(--ink); }
#packPrice::-webkit-outer-spin-button,
#packPrice::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
#packPrice { -moz-appearance: textfield; }

/* A minor control, so it stops being shaped like a call to action. The bordered
   pill gave a mute toggle the same weight as the support button in the header. */
.snd {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 2px 0;
  border: none;
  background: transparent;
  color: var(--dim);
  cursor: pointer;
  transition: color .15s ease;
}
.snd:hover { color: var(--muted); }
.snd:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }
.snd.on { color: var(--gold); }
.snd svg {
  width: 16px; height: 16px;
  flex: 0 0 auto;
  fill: none; stroke: currentColor;
  stroke-width: 1.7; stroke-linecap: round;
}
.snd .spk { fill: currentColor; stroke: none; }
.snd .wave { display: none; }
.snd.on .wave { display: block; }
.snd.on .slash { display: none; }
.snd-txt {
  font-size: 9px;
  letter-spacing: .9px;
  text-transform: uppercase;
  border-bottom: 1px dotted currentColor;
  padding-bottom: 1px;
  opacity: .75;
}

/* ── Footer ─────────────────────────────────────────────────────
   Three lines doing three different jobs: an ask, a credit, and boilerplate.
   Set at the same size they read as one undifferentiated block, so each tier
   steps down in size and brightness. The text is also held to a column width —
   at 1720px the disclaimer stretched the full panel and became unreadable. */

footer {
  text-align: center;
  color: var(--dim);
  padding: 34px 20px 40px;
}
footer p {
  margin: 0 auto;
  max-width: 60ch;
  /* Distribute the words across lines instead of filling greedily. Without it
     the credit line dropped "stolen." on to a line of its own. */
  text-wrap: balance;
}
footer a { color: var(--muted); }
footer a:hover { color: var(--gold-2); }

/* Section break, carrying the same ink-swirl mark as the card backs and the
   drawn wrapper — the one piece of ornament this page owns. */
.footer-rule {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  max-width: 340px;
  margin: 0 auto 20px;
}
.footer-rule::before,
.footer-rule::after {
  content: '';
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(232, 182, 76, .3));
}
.footer-rule::after { transform: scaleX(-1); }

.footer-mark {
  flex: 0 0 auto;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: conic-gradient(from 208deg,
    transparent 0deg, rgba(232, 182, 76, .85) 128deg, transparent 208deg);
  -webkit-mask: radial-gradient(circle, transparent 52%, #000 56%);
          mask: radial-gradient(circle, transparent 52%, #000 56%);
}

/* Tier 1 — the only line worth acting on. */
.support-line {
  color: var(--ink);
  font-size: 13.5px;
  margin-bottom: 9px;
}
.support-line a {
  color: var(--gold-2);
  text-decoration: none;
  border-bottom: 1px solid rgba(232, 182, 76, .4);
  padding-bottom: 1px;
}
.support-line a:hover { border-bottom-color: var(--gold); }

/* Tier 2 — a credit, not fine print: this site is someone else's idea. */
.thanks {
  color: var(--muted);
  font-size: 12px;
}

/* Tier 3 — boilerplate, set apart so it stops competing with the two above. */
.legal {
  margin-top: 16px;
  font-size: 10.5px;
  color: #6b648f;
  letter-spacing: .2px;
}
.legal a { color: #857daf; }

/* ── Responsive ─────────────────────────────────────────────────── */

/* No room for a side rail: the pack sits beside the numbers above the grid. */
/* Wide screens were 620px of dead width beside a grid that had to scroll to show
   one pack. Six columns spends that width to buy back the height, so the whole
   pack lands in view at once — the moment the reveal is actually for.
   The breakpoint is 1400 rather than 1500 because 1440x900 is an extremely
   common laptop, and it fits six comfortably (170px cards, ~880px of page). */
@media (min-width: 1400px) {
  main {
    max-width: min(1720px, calc(100vw - 64px));
    grid-template-columns: 248px minmax(0, 1fr);
    gap: 34px;
  }
  /* Match the panel so the masthead's two ends stay locked to it. */
  .top { max-width: min(1720px, calc(100vw - 64px)); }
  :root { --stack-step: 30px; }
}

@media (max-width: 700px) {
  /* Too narrow to split: stack and centre. */
  .top { flex-direction: column; gap: 10px; padding: 24px 16px 18px; }
  .masthead-right { flex-direction: column-reverse; gap: 12px; }
  .setpick { align-items: center; }
}

@media (max-width: 860px) {
  main {
    grid-template-columns: minmax(0, 1fr);
    gap: 22px;
    padding: 20px 20px 24px;
  }
  .rail {
    position: static;
    flex-direction: row;
    align-items: center;
    gap: 20px;
  }
  .rip { flex: 0 0 122px; }
  .stats { flex: 1 1 auto; }
  .snd { flex: 0 0 auto; align-self: center; padding: 8px; }
  .snd-txt { display: none; }
  .pack-name { font-size: 10px; letter-spacing: 1.4px; }
  .pack-mark { width: 30px; height: 30px; }
  .pack-count, .pack-rule { display: none; }
  .rip-label { font-size: 8.5px; letter-spacing: 1.4px; }
}

/* Four across stops being legible on a phone; two keeps the name and price
   readable, which is the point of the caption. */
@media (max-width: 560px) {
  /* Pack + stats + sound need ~351px of row and there are only ~293. Rather
     than crushing the stats, the sound toggle wraps to its own line and sits
     right. It was overflowing the panel entirely before — running off the
     right-hand edge of the screen. */
  .rail { flex-wrap: wrap; gap: 14px; }
  .rip { flex: 0 0 104px; }
  .snd { margin-left: auto; align-self: auto; }

  .set-logo { max-height: calc(var(--logo-h, 84px) * .8); }
  main { padding: 18px 16px 22px; border-radius: 14px; margin-bottom: 24px; }
  .pack-name { display: none; }
  /* Two up. Without an explicit basis the flex items grow to fill the whole
     board and land one enormous card per row. */
  /* The cloud spills far past the pack either side, which at this width would
     be most of the screen. Reined in so it stays over the content. */
  .poof { left: -22%; right: -22%; top: -20%; height: 132%; }

  .board { gap: 20px 14px; }
  .featured > .slot,
  .stacks > .stack { flex: 1 1 calc(50% - 7px); max-width: none; }
  :root { --stack-step: 22px; }
  .stat .sv, #packPrice { font-size: 17px; }
}

@media (prefers-reduced-motion: reduce) {
  .flip { transition-duration: .01ms; }
  .meta { transition-duration: .01ms; }
  .slot.flipped .face.front.foil::before,
  .slot.flipped .face.front.foil::after { animation: none; }
  .rip.torn .pack { animation: none; }
  .stack-cards .slot { transition-duration: .01ms; }
  /* The shred still has to leave, just without the flight. */
  .pack-shred,
  .rip.committing .pack-shred { transition-duration: .01ms; }
  .pack-body::after { animation: none; }
  /* Every stage still has to REACH its end state — a card frozen half out of
     the pack, or smoke left hanging over it, is worse than either extreme. All
     of these hold their last keyframe, so collapsing the duration lands them. */
  .pack.bursting .pack-cards i,
  .rip.poofing .poof i,
  .rip.poofing .poof b,
  .pack.vanishing { animation-duration: .01ms; }
  .rip:not(.busy) .pack:has(.pack-hit:hover) { transform: none; }
}

/* ── Card viewer ────────────────────────────────────────────────
   Stacked cards show only a sliver, so there has to be somewhere to actually
   look at one. Any card opens here at full size. */
.modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgba(4, 3, 12, .82);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.modal[hidden] { display: none; }

.modal-inner {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  max-width: min(420px, 88vw);
}
/* In the viewer the foil is driven by the POINTER rather than a timer, because
   that is what real foil does — the bands move because your viewing angle
   moves, not because time passes. A looping animation is only a stand-in for
   the grid, where nothing is being pointed at. */
.modal-card {
  position: relative;
  width: 100%;
  border-radius: 14px;
  transform:
    perspective(1000px)
    rotateY(calc((var(--fx, .5) - .5) * 17deg))
    rotateX(calc((.5 - var(--fy, .5)) * 17deg));
  transition: transform .12s ease-out;
  will-change: transform;
}
.modal-img {
  width: 100%;
  border-radius: 14px;
  display: block;
  box-shadow: 0 30px 70px -20px rgba(0, 0, 0, .9);
}

.modal-card.foil::before,
.modal-card.foil::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 14px;
  pointer-events: none;
}
.modal-card.foil::before {
  background: repeating-linear-gradient(
    104deg,
    hsla(346, 100%, 66%, .20)  0%,
    hsla( 32, 100%, 62%, .16)  3.5%,
    hsla( 56, 100%, 62%, .15)  7%,
    hsla(140,  90%, 60%, .16) 10.5%,
    hsla(190, 100%, 62%, .20) 14%,
    hsla(232, 100%, 70%, .17) 17.5%,
    hsla(286,  95%, 70%, .20) 21%,
    hsla(346, 100%, 66%, .20) 24.5%
  );
  background-size: 230% 100%;
  background-position: calc(var(--fx, .5) * 100%) 50%;
  mix-blend-mode: overlay;
}
.modal-card.foil::after {
  background: radial-gradient(
    58% 44% at calc(var(--fx, .5) * 100%) calc(var(--fy, .5) * 100%),
    rgba(255, 255, 255, .40) 0%,
    rgba(255, 255, 255, .10) 42%,
    transparent 70%);
  mix-blend-mode: screen;
}

/* The loud stock again. */
.modal-inner[data-r="Enchanted"] .modal-card.foil::before,
.modal-inner[data-r="Iconic"]    .modal-card.foil::before,
.modal-inner[data-r="Epic"]      .modal-card.foil::before {
  filter: saturate(1.4);
  background-size: 155% 100%;
}
.modal-meta { text-align: center; }
.modal-slot {
  font-size: 9.5px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--dim);
}
.modal-name {
  font-family: 'Oswald', sans-serif;
  font-size: 19px;
  margin-top: 3px;
  color: var(--ink);
}
.modal-price {
  font-family: 'Oswald', sans-serif;
  font-size: 24px;
  margin-top: 4px;
  font-variant-numeric: tabular-nums;
  color: var(--gold-2);
}
.modal-close {
  position: absolute;
  top: -14px; right: -14px;
  width: 34px; height: 34px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: #14112e;
  color: var(--muted);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.modal-close:hover { color: var(--bad); border-color: var(--bad); }

/* Rarity treatment carries into the viewer. */
.modal-inner[data-r="Super_rare"] .modal-img { box-shadow: 0 0 40px rgba(176,126,240,.45); }

.modal-inner[data-r="Legendary"]  .modal-img { box-shadow: 0 0 46px rgba(232,182,76,.5); }
.modal-inner[data-r="Epic"]       .modal-img { box-shadow: 0 0 50px rgba(255,138,76,.55); }
.modal-inner[data-r="Enchanted"]  .modal-img { box-shadow: 0 0 58px rgba(255,95,168,.6); }
.modal-inner[data-r="Iconic"]     .modal-img { box-shadow: 0 0 64px rgba(88,240,224,.65); }

/* ── Debug panel ────────────────────────────────────────────────
   Deliberately unstyled-looking: monospace, square, no polish, so it can never
   be mistaken for part of the page. Off unless ?debug or Shift+D.
   Kept last in the file on purpose — it is the only fixed-position block here,
   and splicing it into another rule's selector list once was enough. */
.debug {
  position: fixed;
  right: 14px;
  bottom: 14px;
  z-index: 40;
  width: 190px;
  padding: 9px 10px 10px;
  background: #0c0a18;
  border: 1px solid #3b3470;
  border-radius: 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10px;
  color: #9a93c4;
  box-shadow: 0 10px 30px rgba(0,0,0,.6);
}
.debug[hidden] { display: none; }

.debug-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  letter-spacing: .5px;
  text-transform: uppercase;
  color: #6f689a;
  margin-bottom: 7px;
}
.debug-head button {
  background: none; border: none; cursor: pointer;
  color: #6f689a; font-size: 14px; line-height: 1; padding: 0 2px;
}
.debug-head button:hover { color: var(--bad); }

.debug-btns { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
.debug-btns button {
  font: inherit;
  padding: 5px 0;
  background: #17143a;
  border: 1px solid #2e2857;
  border-radius: 3px;
  color: #b9b2dd;
  cursor: pointer;
}
.debug-btns button:hover:not(:disabled) { border-color: var(--gold); color: var(--gold-2); }
.debug-btns button[aria-pressed="true"] {
  background: var(--gold); border-color: var(--gold); color: #1a1430; font-weight: 700;
}
.debug-btns button:disabled { opacity: .3; cursor: not-allowed; }

.debug-note { margin-top: 7px; color: #6f689a; }
.debug-note.armed { color: var(--gold-2); }
